mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
Keep chat thinking before its answer across history loads (#33628)
* fix(chat): preserve transcript source order across history windows * test(chat): use template literal for mobile transcript fixture * test(chat): place composed transcript contracts across project boundaries * test(chat): move reader-to-phone contract across project boundaries * test(chat): reuse transcript input setup and caller contracts * test(chat): align mobile recordings with source-order capability * test(chat): scope phone transforms and await history snapshot
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { normalizePath, transformWithOxc, type Plugin } from 'vite'
|
||||
import { configDefaults, defaultExclude, defineConfig } from 'vitest/config'
|
||||
import { UNIT_INCLUDE, UNIT_EXCLUDE, UNIT_SETUP_FILES } from './scripts/ci-unit-files.mjs'
|
||||
import TimingSequencer from './scripts/ci-unit-sequencer.mjs'
|
||||
@@ -18,8 +19,32 @@ const measurementFile = 'src/main/foreign-sqlite-readers/foreign-sqlite-reader-e
|
||||
const paletteMeasurementFile =
|
||||
'src/renderer/src/lib/palette-match/palette-match-performance.test.ts'
|
||||
const measurementFiles = [measurementFile, paletteMeasurementFile]
|
||||
const phoneContractModules = [
|
||||
'mobile/src/session/mobile-native-chat-stream-frame.ts',
|
||||
'mobile/src/session/mobile-native-chat-render-data.ts'
|
||||
].map((file) => normalizePath(resolve(file)))
|
||||
const phoneContractPattern = new RegExp(
|
||||
`^(?:${phoneContractModules.map((file) => file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(?:\\?.*)?$`
|
||||
)
|
||||
const transforms = {
|
||||
oxc: { exclude: [/\.js$/, phoneContractPattern] },
|
||||
plugins: [
|
||||
{
|
||||
// These plain TS consumers need no Expo app configuration in root-only installs.
|
||||
name: 'phone-contract-oxc',
|
||||
enforce: 'pre',
|
||||
async transform(source, id) {
|
||||
const filename = normalizePath(id.split('?')[0])
|
||||
if (!phoneContractModules.includes(filename)) {
|
||||
return null
|
||||
}
|
||||
const result = await transformWithOxc(source, filename, { tsconfig: false })
|
||||
for (const warning of result.warnings) {
|
||||
this.warn(warning)
|
||||
}
|
||||
return { code: result.code, map: result.map }
|
||||
}
|
||||
} satisfies Plugin,
|
||||
{
|
||||
// Unit tests have no asset server; retain real catalog parsing through fetch(data:).
|
||||
name: 'locale-catalog-test-assets',
|
||||
|
||||
+527
-491
File diff suppressed because it is too large
Load Diff
+180
-174
@@ -4,11 +4,6 @@
|
||||
"family": "session.native-chat-page",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"08ef53d83e8c": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.readSession\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":60,\"beforeOffset\":1200,\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"0943a7b4b434": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
@@ -27,6 +22,11 @@
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"18a95418eb6d": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.readSession\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":60,\"capabilities\":{\"transcriptSourceOrder\":1},\"beforeOffset\":1200,\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"197fa03857dd": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
@@ -40,82 +40,12 @@
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"2a5d879ed94b": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"3e14b1ec5bde": {
|
||||
"name": "nativeChat.unsubscribe#2",
|
||||
"ordinal": 6,
|
||||
"json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"456781b97acc": {
|
||||
"name": "nativeChat.subscribe#1",
|
||||
"ordinal": 1,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"663babae1f1d": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7b237e9824c8": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7e8788afab15": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": true,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7eedfcf76fd3": {
|
||||
"name": "nativeChat.unsubscribe#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"813c942b745a": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": "outer refused",
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"8f91342d7840": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": false,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-1", "m-2", "m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"911b3f485409": {
|
||||
"512a0c61c2d8": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
@@ -128,6 +58,9 @@
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"capabilities": {
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
@@ -181,11 +114,86 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"6138b52f75d7": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"70a858645636": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7b237e9824c8": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7cbe5e7d1f66": {
|
||||
"name": "nativeChat.subscribe#1",
|
||||
"ordinal": 1,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7e8788afab15": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": true,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7eedfcf76fd3": {
|
||||
"name": "nativeChat.unsubscribe#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"813c942b745a": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": "outer refused",
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"8f91342d7840": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": false,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-1", "m-2", "m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"96abfc4cef7e": {
|
||||
"name": "nativeChat.unsubscribe#1",
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"9d464dc9697a": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"9ec06f18374b": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
@@ -213,11 +221,6 @@
|
||||
"status": "error",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"b84b4297f28e": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"c5cc569a5dda": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
@@ -251,37 +254,6 @@
|
||||
"status": "error",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"e88c218171fa": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "nativeChat.readSession"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"budgetSpansConnect": true,
|
||||
"timeoutMs": 15000
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
@@ -327,6 +299,40 @@
|
||||
"frame": "nativeChat.subscribe#1"
|
||||
}
|
||||
},
|
||||
"f1dfb69b0f6f": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "nativeChat.readSession"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"capabilities": {
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"budgetSpansConnect": true,
|
||||
"timeoutMs": 15000
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"fb84e5e8aefe": {
|
||||
"name": "nativeChat.unsubscribe#2",
|
||||
"ordinal": 5,
|
||||
@@ -340,7 +346,7 @@
|
||||
"id": "native-chat-page-earlier.prelude:subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -352,7 +358,7 @@
|
||||
"id": "native-chat-page-earlier.normal:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -363,8 +369,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.normal:paging",
|
||||
"observation": {
|
||||
"sender": ["e88c218171fa"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c"],
|
||||
"sender": ["f1dfb69b0f6f"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -376,8 +382,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.normal:paged",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -389,8 +395,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.normal:re-subscribed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -403,8 +409,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.normal:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -417,11 +423,11 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.normal:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"3e14b1ec5bde"
|
||||
],
|
||||
@@ -439,7 +445,7 @@
|
||||
"id": "native-chat-page-earlier.result-absent:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -451,7 +457,7 @@
|
||||
"id": "native-chat-page-earlier.result-absent:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -464,7 +470,7 @@
|
||||
"id": "native-chat-page-earlier.result-absent:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -477,7 +483,7 @@
|
||||
"id": "native-chat-page-earlier.result-absent:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "b84b4297f28e", "96abfc4cef7e"],
|
||||
"payloads": ["7cbe5e7d1f66", "70a858645636", "96abfc4cef7e"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -491,7 +497,7 @@
|
||||
"id": "native-chat-page-earlier.result-absent:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "b84b4297f28e", "96abfc4cef7e"],
|
||||
"payloads": ["7cbe5e7d1f66", "70a858645636", "96abfc4cef7e"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -505,7 +511,7 @@
|
||||
"id": "native-chat-page-earlier.result-absent:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "b84b4297f28e", "96abfc4cef7e", "fb84e5e8aefe"],
|
||||
"payloads": ["7cbe5e7d1f66", "70a858645636", "96abfc4cef7e", "fb84e5e8aefe"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -520,7 +526,7 @@
|
||||
"id": "native-chat-page-earlier.result-null:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -532,7 +538,7 @@
|
||||
"id": "native-chat-page-earlier.result-null:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -545,7 +551,7 @@
|
||||
"id": "native-chat-page-earlier.result-null:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -558,7 +564,7 @@
|
||||
"id": "native-chat-page-earlier.result-null:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "b84b4297f28e", "96abfc4cef7e"],
|
||||
"payloads": ["7cbe5e7d1f66", "70a858645636", "96abfc4cef7e"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -572,7 +578,7 @@
|
||||
"id": "native-chat-page-earlier.result-null:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "b84b4297f28e", "96abfc4cef7e"],
|
||||
"payloads": ["7cbe5e7d1f66", "70a858645636", "96abfc4cef7e"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -586,7 +592,7 @@
|
||||
"id": "native-chat-page-earlier.result-null:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "b84b4297f28e", "96abfc4cef7e", "fb84e5e8aefe"],
|
||||
"payloads": ["7cbe5e7d1f66", "70a858645636", "96abfc4cef7e", "fb84e5e8aefe"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -601,7 +607,7 @@
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -613,7 +619,7 @@
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -626,7 +632,7 @@
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -639,7 +645,7 @@
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -653,7 +659,7 @@
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -667,7 +673,7 @@
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5", "1693563703b9"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5", "1693563703b9"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -682,7 +688,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -694,7 +700,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -707,7 +713,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -720,7 +726,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -734,7 +740,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -748,7 +754,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5", "1693563703b9"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5", "1693563703b9"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -763,7 +769,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -775,7 +781,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -788,7 +794,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -801,7 +807,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -815,7 +821,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -829,7 +835,7 @@
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5", "1693563703b9"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5", "1693563703b9"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -844,7 +850,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -856,7 +862,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -869,7 +875,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -882,7 +888,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -896,7 +902,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -910,7 +916,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -925,7 +931,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -937,7 +943,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -950,7 +956,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -963,7 +969,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -977,7 +983,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -991,7 +997,7 @@
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -1006,7 +1012,7 @@
|
||||
"id": "native-chat-page-earlier.method-not-found:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -1018,7 +1024,7 @@
|
||||
"id": "native-chat-page-earlier.method-not-found:paging",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -1031,7 +1037,7 @@
|
||||
"id": "native-chat-page-earlier.method-not-found:paged",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -1044,7 +1050,7 @@
|
||||
"id": "native-chat-page-earlier.method-not-found:re-subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -1058,7 +1064,7 @@
|
||||
"id": "native-chat-page-earlier.method-not-found:replayed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -1072,7 +1078,7 @@
|
||||
"id": "native-chat-page-earlier.method-not-found:unmounted",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc", "2a5d879ed94b", "a2b8cf4aa6d5"],
|
||||
"payloads": ["7cbe5e7d1f66", "9d464dc9697a", "a2b8cf4aa6d5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
|
||||
+165
-159
@@ -4,11 +4,6 @@
|
||||
"family": "session.native-chat-page",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"08ef53d83e8c": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.readSession\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":60,\"beforeOffset\":1200,\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"0943a7b4b434": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
@@ -22,6 +17,11 @@
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"18a95418eb6d": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.readSession\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":60,\"capabilities\":{\"transcriptSourceOrder\":1},\"beforeOffset\":1200,\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"2614fb8bb258": {
|
||||
"name": "stream-listener-crash",
|
||||
"ordinal": 6,
|
||||
@@ -39,73 +39,7 @@
|
||||
"ordinal": 6,
|
||||
"json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"456781b97acc": {
|
||||
"name": "nativeChat.subscribe#1",
|
||||
"ordinal": 1,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"6252ebf3de79": {
|
||||
"name": "stream-listener-crash",
|
||||
"ordinal": 6,
|
||||
"value": {
|
||||
"error": {
|
||||
"category": "TypeError",
|
||||
"isRpcDeliveryUnknown": false,
|
||||
"message": "Cannot read properties of undefined (reading 'type')"
|
||||
},
|
||||
"frame": "nativeChat.subscribe#2"
|
||||
}
|
||||
},
|
||||
"663babae1f1d": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7b237e9824c8": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7e8788afab15": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": true,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7eedfcf76fd3": {
|
||||
"name": "nativeChat.unsubscribe#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"8f91342d7840": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": false,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-1", "m-2", "m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"911b3f485409": {
|
||||
"512a0c61c2d8": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
@@ -118,6 +52,9 @@
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"capabilities": {
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
@@ -171,6 +108,72 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"6138b52f75d7": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"6252ebf3de79": {
|
||||
"name": "stream-listener-crash",
|
||||
"ordinal": 6,
|
||||
"value": {
|
||||
"error": {
|
||||
"category": "TypeError",
|
||||
"isRpcDeliveryUnknown": false,
|
||||
"message": "Cannot read properties of undefined (reading 'type')"
|
||||
},
|
||||
"frame": "nativeChat.subscribe#2"
|
||||
}
|
||||
},
|
||||
"7b237e9824c8": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7cbe5e7d1f66": {
|
||||
"name": "nativeChat.subscribe#1",
|
||||
"ordinal": 1,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7e8788afab15": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": true,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7eedfcf76fd3": {
|
||||
"name": "nativeChat.unsubscribe#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"8f91342d7840": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": false,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-1", "m-2", "m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"a5609c6d15fc": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
@@ -209,37 +212,6 @@
|
||||
"ordinal": 7,
|
||||
"json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"e88c218171fa": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "nativeChat.readSession"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"budgetSpansConnect": true,
|
||||
"timeoutMs": 15000
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
@@ -260,6 +232,40 @@
|
||||
"messageIds": [],
|
||||
"status": "loading",
|
||||
"transcriptLoading": true
|
||||
},
|
||||
"f1dfb69b0f6f": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "nativeChat.readSession"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"capabilities": {
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"budgetSpansConnect": true,
|
||||
"timeoutMs": 15000
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -269,7 +275,7 @@
|
||||
"id": "native-chat-page-earlier.prelude:subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -281,7 +287,7 @@
|
||||
"id": "native-chat-page-earlier.prelude:snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -292,8 +298,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.prelude:paging",
|
||||
"observation": {
|
||||
"sender": ["e88c218171fa"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c"],
|
||||
"sender": ["f1dfb69b0f6f"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -305,8 +311,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.prelude:paged",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -318,8 +324,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.prelude:re-subscribed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -332,8 +338,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.normal:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -346,11 +352,11 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.normal:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"3e14b1ec5bde"
|
||||
],
|
||||
@@ -367,8 +373,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.result-absent:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -381,11 +387,11 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.result-absent:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"e4483b4ab66e"
|
||||
],
|
||||
@@ -402,8 +408,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.result-null:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -416,11 +422,11 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.result-null:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"e4483b4ab66e"
|
||||
],
|
||||
@@ -437,8 +443,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -451,11 +457,11 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.inner-ok-missing:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"3e14b1ec5bde"
|
||||
],
|
||||
@@ -472,8 +478,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -486,11 +492,11 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.inner-false-string-error:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"3e14b1ec5bde"
|
||||
],
|
||||
@@ -507,8 +513,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -521,11 +527,11 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.inner-false-object-error:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"3e14b1ec5bde"
|
||||
],
|
||||
@@ -542,8 +548,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.outer-refused:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -556,8 +562,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.outer-refused:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -571,8 +577,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -585,8 +591,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.outer-refused-no-message:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -600,8 +606,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.method-not-found:replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -614,8 +620,8 @@
|
||||
{
|
||||
"id": "native-chat-page-earlier.method-not-found:unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
"family": "session.native-chat-page",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"08ef53d83e8c": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.readSession\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":60,\"beforeOffset\":1200,\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"0943a7b4b434": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
@@ -22,66 +17,17 @@
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"18a95418eb6d": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 3,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.readSession\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":60,\"capabilities\":{\"transcriptSourceOrder\":1},\"beforeOffset\":1200,\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"3e14b1ec5bde": {
|
||||
"name": "nativeChat.unsubscribe#2",
|
||||
"ordinal": 6,
|
||||
"json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"456781b97acc": {
|
||||
"name": "nativeChat.subscribe#1",
|
||||
"ordinal": 1,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"663babae1f1d": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7b237e9824c8": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7e8788afab15": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": true,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7eedfcf76fd3": {
|
||||
"name": "nativeChat.unsubscribe#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"8f91342d7840": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": false,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-1", "m-2", "m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"911b3f485409": {
|
||||
"512a0c61c2d8": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
@@ -94,6 +40,9 @@
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"capabilities": {
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
@@ -147,36 +96,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"e88c218171fa": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "nativeChat.readSession"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"budgetSpansConnect": true,
|
||||
"timeoutMs": 15000
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
"6138b52f75d7": {
|
||||
"name": "nativeChat.subscribe#2",
|
||||
"ordinal": 4,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7b237e9824c8": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7cbe5e7d1f66": {
|
||||
"name": "nativeChat.subscribe#1",
|
||||
"ordinal": 1,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.subscribe\",\"params\":{\"agent\":\"claude\",\"sessionId\":\"session-1\",\"limit\":40,\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\",\"capabilities\":{\"transcriptPending\":1,\"transcriptSourceOrder\":1},\"transcriptPath\":\"/work/feature/.claude/session-1.jsonl\"}}"
|
||||
},
|
||||
"7e8788afab15": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": true,
|
||||
"loadingEarlier": true,
|
||||
"messageIds": ["m-3", "m-4"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"7eedfcf76fd3": {
|
||||
"name": "nativeChat.unsubscribe#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"nativeChat.unsubscribe\",\"params\":{\"subscriptionId\":\"claude:session-1:00000000-0000-4000-8000-000000000001\"}}"
|
||||
},
|
||||
"8f91342d7840": {
|
||||
"crash": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"error": {
|
||||
"$rpc": "null"
|
||||
},
|
||||
"hasMore": false,
|
||||
"loadingEarlier": false,
|
||||
"messageIds": ["m-1", "m-2", "m-3", "m-4", "m-5"],
|
||||
"status": "ready",
|
||||
"transcriptLoading": false
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
@@ -198,6 +170,40 @@
|
||||
"messageIds": [],
|
||||
"status": "loading",
|
||||
"transcriptLoading": true
|
||||
},
|
||||
"f1dfb69b0f6f": {
|
||||
"name": "nativeChat.readSession#1",
|
||||
"ordinal": 2,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "nativeChat.readSession"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"agent": "claude",
|
||||
"beforeOffset": 1200,
|
||||
"capabilities": {
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"limit": 60,
|
||||
"sessionId": "session-1",
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"budgetSpansConnect": true,
|
||||
"timeoutMs": 15000
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -207,7 +213,7 @@
|
||||
"id": "subscribed",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -219,7 +225,7 @@
|
||||
"id": "snapshot",
|
||||
"observation": {
|
||||
"sender": [],
|
||||
"payloads": ["456781b97acc"],
|
||||
"payloads": ["7cbe5e7d1f66"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -230,8 +236,8 @@
|
||||
{
|
||||
"id": "paging",
|
||||
"observation": {
|
||||
"sender": ["e88c218171fa"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c"],
|
||||
"sender": ["f1dfb69b0f6f"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -243,8 +249,8 @@
|
||||
{
|
||||
"id": "paged",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a"
|
||||
@@ -256,8 +262,8 @@
|
||||
{
|
||||
"id": "re-subscribed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -270,8 +276,8 @@
|
||||
{
|
||||
"id": "replayed",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"page": "eb79a9b3682a",
|
||||
@@ -284,11 +290,11 @@
|
||||
{
|
||||
"id": "unmounted",
|
||||
"observation": {
|
||||
"sender": ["911b3f485409"],
|
||||
"sender": ["512a0c61c2d8"],
|
||||
"payloads": [
|
||||
"456781b97acc",
|
||||
"08ef53d83e8c",
|
||||
"663babae1f1d",
|
||||
"7cbe5e7d1f66",
|
||||
"18a95418eb6d",
|
||||
"6138b52f75d7",
|
||||
"7eedfcf76fd3",
|
||||
"3e14b1ec5bde"
|
||||
],
|
||||
|
||||
@@ -21128,7 +21128,8 @@
|
||||
"limit": 40,
|
||||
"subscriptionId": "claude:session-1:00000000-0000-4000-8000-000000000001",
|
||||
"capabilities": {
|
||||
"transcriptPending": 1
|
||||
"transcriptPending": 1,
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
},
|
||||
@@ -21184,6 +21185,9 @@
|
||||
"agent": "claude",
|
||||
"sessionId": "session-1",
|
||||
"limit": 60,
|
||||
"capabilities": {
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"beforeOffset": 1200,
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
},
|
||||
@@ -21239,7 +21243,8 @@
|
||||
"limit": 40,
|
||||
"subscriptionId": "claude:session-1:00000000-0000-4000-8000-000000000001",
|
||||
"capabilities": {
|
||||
"transcriptPending": 1
|
||||
"transcriptPending": 1,
|
||||
"transcriptSourceOrder": 1
|
||||
},
|
||||
"transcriptPath": "/work/feature/.claude/session-1.jsonl"
|
||||
},
|
||||
|
||||
@@ -91,7 +91,13 @@ describe('the native chat older-history page', () => {
|
||||
})
|
||||
expect(client.sendRequest).toHaveBeenCalledExactlyOnceWith(
|
||||
'nativeChat.readSession',
|
||||
{ agent: 'claude', sessionId: 'session', limit: 60, beforeOffset: 100 },
|
||||
{
|
||||
agent: 'claude',
|
||||
sessionId: 'session',
|
||||
limit: 60,
|
||||
beforeOffset: 100,
|
||||
capabilities: { transcriptSourceOrder: 1 }
|
||||
},
|
||||
{ timeoutMs: expect.any(Number), budgetSpansConnect: true }
|
||||
)
|
||||
expect(state?.olderPages).toBe('failed')
|
||||
|
||||
@@ -153,6 +153,7 @@ describe('mobile transcript tool result paging', () => {
|
||||
agent: 'claude',
|
||||
sessionId: 'remote-session',
|
||||
limit: hasCursor ? 60 : 100,
|
||||
capabilities: { transcriptSourceOrder: 1 },
|
||||
...(hasCursor ? { beforeOffset: 700 } : {})
|
||||
},
|
||||
expect.objectContaining({ budgetSpansConnect: true })
|
||||
|
||||
@@ -145,7 +145,13 @@ describe('useMobileNativeChatSession', () => {
|
||||
expect(state?.hasMore).toBe(false)
|
||||
expect(sendRequest).toHaveBeenLastCalledWith(
|
||||
'nativeChat.readSession',
|
||||
{ agent: 'claude', sessionId: 'session', limit: 40, beforeOffset: 50 },
|
||||
{
|
||||
agent: 'claude',
|
||||
sessionId: 'session',
|
||||
limit: 40,
|
||||
beforeOffset: 50,
|
||||
capabilities: { transcriptSourceOrder: 1 }
|
||||
},
|
||||
expect.objectContaining({ budgetSpansConnect: true })
|
||||
)
|
||||
await act(async () => state?.loadEarlier())
|
||||
@@ -167,7 +173,13 @@ describe('useMobileNativeChatSession', () => {
|
||||
expect(sendRequest).toHaveBeenCalledTimes(requestsAtCap + 1)
|
||||
expect(sendRequest).toHaveBeenLastCalledWith(
|
||||
'nativeChat.readSession',
|
||||
{ agent: 'claude', sessionId: 'session', limit: 60, beforeOffset: 500 },
|
||||
{
|
||||
agent: 'claude',
|
||||
sessionId: 'session',
|
||||
limit: 60,
|
||||
beforeOffset: 500,
|
||||
capabilities: { transcriptSourceOrder: 1 }
|
||||
},
|
||||
expect.objectContaining({ budgetSpansConnect: true })
|
||||
)
|
||||
}
|
||||
@@ -295,7 +307,12 @@ describe('useMobileNativeChatSession', () => {
|
||||
|
||||
expect(sendRequest).toHaveBeenCalledWith(
|
||||
'nativeChat.readSession',
|
||||
{ agent: 'claude', sessionId: 'session', limit: 100 },
|
||||
{
|
||||
agent: 'claude',
|
||||
sessionId: 'session',
|
||||
limit: 100,
|
||||
capabilities: { transcriptSourceOrder: 1 }
|
||||
},
|
||||
expect.objectContaining({ budgetSpansConnect: true })
|
||||
)
|
||||
})
|
||||
@@ -462,7 +479,12 @@ describe('useMobileNativeChatSession', () => {
|
||||
|
||||
expect(sendRequest).toHaveBeenLastCalledWith(
|
||||
'nativeChat.readSession',
|
||||
{ agent: 'claude', sessionId: 'session', limit: 130 },
|
||||
{
|
||||
agent: 'claude',
|
||||
sessionId: 'session',
|
||||
limit: 130,
|
||||
capabilities: { transcriptSourceOrder: 1 }
|
||||
},
|
||||
expect.objectContaining({ budgetSpansConnect: true })
|
||||
)
|
||||
})
|
||||
@@ -673,7 +695,7 @@ describe('useMobileNativeChatSession transcriptLoading', () => {
|
||||
|
||||
expect(subscribe).toHaveBeenCalledWith(
|
||||
'nativeChat.subscribe',
|
||||
expect.objectContaining({ capabilities: { transcriptPending: 1 } }),
|
||||
expect.objectContaining({ capabilities: { transcriptPending: 1, transcriptSourceOrder: 1 } }),
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(renders.at(-1)).toMatchObject({
|
||||
|
||||
@@ -141,7 +141,7 @@ export function useMobileNativeChatSession(args: {
|
||||
limit: limitRef.current,
|
||||
// Use a distinct token per screen so another viewer cannot evict this host feed.
|
||||
subscriptionId: buildNativeChatSubscriptionId(agent, sessionId, randomV4Uuid()),
|
||||
capabilities: { transcriptPending: 1 },
|
||||
capabilities: { transcriptPending: 1, transcriptSourceOrder: 1 },
|
||||
...(transcriptPath ? { transcriptPath } : {})
|
||||
},
|
||||
(raw) => {
|
||||
@@ -224,6 +224,7 @@ export function useMobileNativeChatSession(args: {
|
||||
agent,
|
||||
sessionId,
|
||||
limit: beforeOffset === null ? nextLimit : pageLimit,
|
||||
capabilities: { transcriptSourceOrder: 1 },
|
||||
...(beforeOffset === null ? {} : { beforeOffset }),
|
||||
...(transcriptPath ? { transcriptPath } : {})
|
||||
},
|
||||
|
||||
@@ -156,6 +156,7 @@ describe('nativeChat subscribe lifecycle', () => {
|
||||
|
||||
subscribe(renderer.sender, 'unmount')
|
||||
expect(setupSignal(0).aborted).toBe(false)
|
||||
expect(subscribeTranscript.mock.calls[0]?.[0]).toMatchObject({ sourceOrder: true })
|
||||
expect(_getNativeChatPendingSubscriptionCountForTest()).toBe(1)
|
||||
unsubscribe(renderer.sender, 'unmount')
|
||||
expect(setupSignal(0).aborted).toBe(true)
|
||||
|
||||
@@ -115,6 +115,10 @@ describe('nativeChat:readSession handler', () => {
|
||||
}
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.messages).toHaveLength(2)
|
||||
expect(result.messages).toMatchObject([
|
||||
{ transcriptOrder: 0 },
|
||||
{ transcriptOrder: expect.any(Number) }
|
||||
])
|
||||
} finally {
|
||||
if (previousHome === undefined) {
|
||||
delete process.env.HOME
|
||||
|
||||
@@ -42,7 +42,8 @@ async function readSession(args: NativeChatReadSessionArgs): Promise<ReadTranscr
|
||||
sessionId,
|
||||
args.transcriptPath
|
||||
),
|
||||
limit
|
||||
limit,
|
||||
sourceOrder: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -196,6 +197,7 @@ async function handleSubscribe(event: IpcMainEvent, args: NativeChatSubscribeArg
|
||||
transcriptPath
|
||||
),
|
||||
initialLimit: limit,
|
||||
sourceOrder: true,
|
||||
onTranscriptPending: () => {
|
||||
if (!canPublish()) {
|
||||
return
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { NativeChatMessage } from '../../shared/native-chat-types'
|
||||
import { projectNativeChatTranscriptMessages } from '../../shared/native-chat-transcript-projection'
|
||||
import { readNativeChatTranscript } from './transcript-reader'
|
||||
import { readNativeChatTranscriptTail, subscribeNativeChatTranscript } from './transcript-watch'
|
||||
|
||||
// Claude timestamps queued prompts at send time but writes them after the tool result that absorbed them.
|
||||
const PROMPT = {
|
||||
type: 'user',
|
||||
uuid: 'u-1',
|
||||
timestamp: '2026-06-01T10:00:00.000Z',
|
||||
message: { role: 'user', content: 'start the long task' }
|
||||
}
|
||||
const TOOL_CALL = {
|
||||
type: 'assistant',
|
||||
uuid: 'a-1',
|
||||
timestamp: '2026-06-01T10:00:01.000Z',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'tool_use', id: 'toolu_1', name: 'Bash', input: { command: 'sleep 30' } }]
|
||||
}
|
||||
}
|
||||
const TOOL_RESULT = {
|
||||
type: 'user',
|
||||
uuid: 'r-1',
|
||||
timestamp: '2026-06-01T10:00:30.000Z',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'done' }]
|
||||
}
|
||||
}
|
||||
const QUEUED = {
|
||||
type: 'attachment',
|
||||
uuid: 'q-1',
|
||||
parentUuid: 'r-1',
|
||||
timestamp: '2026-06-01T10:00:10.000Z',
|
||||
attachment: {
|
||||
type: 'queued_command',
|
||||
prompt: 'also check the config',
|
||||
commandMode: 'prompt',
|
||||
origin: { kind: 'human' },
|
||||
timestamp: '2026-06-01T10:00:10.000Z'
|
||||
}
|
||||
}
|
||||
const REPLY = {
|
||||
type: 'assistant',
|
||||
uuid: 'a-2',
|
||||
timestamp: '2026-06-01T10:00:35.000Z',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
stop_reason: 'end_turn',
|
||||
content: [{ type: 'text', text: 'both done' }]
|
||||
}
|
||||
}
|
||||
|
||||
const lines = (...records: object[]): string =>
|
||||
records.map((record) => `${JSON.stringify(record)}\n`).join('')
|
||||
|
||||
let roots: string[] = []
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })))
|
||||
roots = []
|
||||
})
|
||||
|
||||
async function transcriptFile(content: string): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-transcript-file-order-'))
|
||||
roots.push(root)
|
||||
const filePath = join(root, 'session.jsonl')
|
||||
await writeFile(filePath, content)
|
||||
return filePath
|
||||
}
|
||||
|
||||
function expectPromptAfterToolRun(messages: NativeChatMessage[]): void {
|
||||
const rows = projectNativeChatTranscriptMessages(messages)
|
||||
expect(rows.map((row) => row.id)).toEqual(['u-1', 'a-1', 'q-1', 'a-2'])
|
||||
expect(rows[1]?.blocks.map((block) => block.type)).toEqual(['tool-call', 'tool-result'])
|
||||
}
|
||||
|
||||
describe('transcript rows keep file order', () => {
|
||||
it.each([
|
||||
['full read', (filePath: string) => readNativeChatTranscript('claude', 's', { filePath })],
|
||||
[
|
||||
'tail read',
|
||||
(filePath: string) =>
|
||||
readNativeChatTranscriptTail({ agent: 'claude', sessionId: 's', filePath, limit: 40 })
|
||||
]
|
||||
])(
|
||||
'places a mid-turn prompt after the tool result it was read with (%s)',
|
||||
async (_name, read) => {
|
||||
const filePath = await transcriptFile(lines(PROMPT, TOOL_CALL, TOOL_RESULT, QUEUED, REPLY))
|
||||
const result = await read(filePath)
|
||||
if (!('messages' in result)) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
expectPromptAfterToolRun(result.messages)
|
||||
}
|
||||
)
|
||||
|
||||
it('places a mid-turn prompt appended after the initial snapshot', async () => {
|
||||
const filePath = await transcriptFile(lines(PROMPT, TOOL_CALL, TOOL_RESULT))
|
||||
const received: NativeChatMessage[] = []
|
||||
const subscription = await subscribeNativeChatTranscript({
|
||||
agent: 'claude',
|
||||
sessionId: 's',
|
||||
filePath,
|
||||
initialLimit: 40,
|
||||
onInitialSnapshot: (messages) => received.push(...messages),
|
||||
onAppend: (messages) => received.push(...messages),
|
||||
debounceMs: 5,
|
||||
reconciliationIntervalMs: 20
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => received.length).toBe(3)
|
||||
await appendFile(filePath, lines(QUEUED, REPLY))
|
||||
await expect.poll(() => received.length).toBe(5)
|
||||
expectPromptAfterToolRun(received)
|
||||
} finally {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { NativeChatMessage } from '../../shared/native-chat-types'
|
||||
|
||||
/** Raise timestamps to preserve file order: Claude may write queued prompts after later-dated tool results. */
|
||||
/** Modern readers use record position; legacy readers retain their historical backward-clock clamp. */
|
||||
export function inTranscriptFileOrder(
|
||||
message: NativeChatMessage,
|
||||
latest: number
|
||||
latest: number,
|
||||
sourceOffset?: number
|
||||
): { message: NativeChatMessage; latest: number } {
|
||||
if (sourceOffset !== undefined) {
|
||||
return { message: { ...message, transcriptOrder: sourceOffset }, latest }
|
||||
}
|
||||
if (message.timestamp === null || message.timestamp >= latest) {
|
||||
return { message, latest: Math.max(latest, message.timestamp ?? latest) }
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ export async function readIncrementalTranscriptMessages(
|
||||
onBatch?: (messages: NativeChatMessage[]) => void,
|
||||
decodeLifecycle?: (line: string, fallbackId: string) => NativeChatTurnLifecycle | null,
|
||||
onLifecycle?: (lifecycle: NativeChatTurnLifecycle) => void,
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
sourceOrder = false
|
||||
): Promise<NativeChatMessage[]> {
|
||||
const end = (await transcriptFileStat(filePath, 'exact', signal)).size
|
||||
if (end <= state.offset) {
|
||||
@@ -130,7 +131,11 @@ export async function readIncrementalTranscriptMessages(
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
const ordered = inTranscriptFileOrder(message, state.latestTimestamp)
|
||||
const ordered = inTranscriptFileOrder(
|
||||
message,
|
||||
state.latestTimestamp,
|
||||
sourceOrder ? state.pendingStart : undefined
|
||||
)
|
||||
state.latestTimestamp = ordered.latest
|
||||
messages.push(ordered.message)
|
||||
if (onBatch && messages.length >= APPEND_BATCH_MESSAGE_LIMIT) {
|
||||
|
||||
@@ -29,6 +29,7 @@ export type ReadTranscriptResult =
|
||||
|
||||
export type ReadTranscriptOptions = ResolveSessionFileOptions & {
|
||||
filePath?: string
|
||||
sourceOrder?: boolean
|
||||
}
|
||||
|
||||
/** Read the full transcript with no message cap; skip malformed/unknown records using the tailer’s decoders. */
|
||||
@@ -58,16 +59,24 @@ export async function readNativeChatTranscript(
|
||||
try {
|
||||
const transcriptAgent = resolveNativeChatTranscriptAgent(agent)
|
||||
if (transcriptAgent === 'claude') {
|
||||
return { messages: await readTranscript(filePath, decodeClaudeTranscriptLine) }
|
||||
return {
|
||||
messages: await readTranscript(filePath, decodeClaudeTranscriptLine, options.sourceOrder)
|
||||
}
|
||||
}
|
||||
if (transcriptAgent === 'codex') {
|
||||
return { messages: await readTranscript(filePath, decodeCodexTranscriptLine) }
|
||||
return {
|
||||
messages: await readTranscript(filePath, decodeCodexTranscriptLine, options.sourceOrder)
|
||||
}
|
||||
}
|
||||
if (transcriptAgent === 'grok') {
|
||||
return { messages: await readTranscript(filePath, decodeGrokTranscriptLine) }
|
||||
return {
|
||||
messages: await readTranscript(filePath, decodeGrokTranscriptLine, options.sourceOrder)
|
||||
}
|
||||
}
|
||||
if (transcriptAgent === 'omp') {
|
||||
return { messages: await readTranscript(filePath, decodeOmpTranscriptLine) }
|
||||
return {
|
||||
messages: await readTranscript(filePath, decodeOmpTranscriptLine, options.sourceOrder)
|
||||
}
|
||||
}
|
||||
return { error: `Unsupported agent for Chat UI transcript: ${agent}` }
|
||||
} catch (err) {
|
||||
@@ -81,9 +90,18 @@ export async function readNativeChatTranscript(
|
||||
|
||||
async function readTranscript(
|
||||
filePath: string,
|
||||
decode: (line: string, fallbackId: string) => NativeChatMessage | null
|
||||
decode: (line: string, fallbackId: string) => NativeChatMessage | null,
|
||||
sourceOrder = false
|
||||
): Promise<NativeChatMessage[]> {
|
||||
const stream = openTranscriptReadStream(filePath, { encoding: 'utf-8' }, 'exact')
|
||||
const { messages } = await decodeTranscriptStream(stream, filePath, 0, decode, true)
|
||||
const { messages } = await decodeTranscriptStream(
|
||||
stream,
|
||||
filePath,
|
||||
0,
|
||||
decode,
|
||||
true,
|
||||
Infinity,
|
||||
sourceOrder
|
||||
)
|
||||
return messages
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ export async function decodeTranscriptStream(
|
||||
start: number,
|
||||
decode: TranscriptDecoder,
|
||||
includeTrailingLine: boolean,
|
||||
maxRecordBytes = Infinity
|
||||
maxRecordBytes = Infinity,
|
||||
sourceOrder = false
|
||||
): Promise<{ messages: NativeChatMessage[]; consumedBytes: number }> {
|
||||
const messages: NativeChatMessage[] = []
|
||||
let consumedBytes = 0
|
||||
@@ -37,7 +38,11 @@ export async function decodeTranscriptStream(
|
||||
}
|
||||
const message = decode(line, transcriptFallbackId(filePath, start + relativeOffset))
|
||||
if (message) {
|
||||
const ordered = inTranscriptFileOrder(message, latest)
|
||||
const ordered = inTranscriptFileOrder(
|
||||
message,
|
||||
latest,
|
||||
sourceOrder ? start + relativeOffset : undefined
|
||||
)
|
||||
latest = ordered.latest
|
||||
messages.push(ordered.message)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ export async function readNativeChatTranscriptTailFile(
|
||||
endOffset?: number,
|
||||
decodeLifecycle?: NativeChatTurnLifecycleDecoder | null,
|
||||
signal?: AbortSignal,
|
||||
onConsumedBoundary?: (boundary: Buffer) => void
|
||||
onConsumedBoundary?: (boundary: Buffer) => void,
|
||||
sourceOrder = false
|
||||
): Promise<{
|
||||
messages: NativeChatMessage[]
|
||||
lifecycle?: NativeChatTurnLifecycle
|
||||
@@ -155,7 +156,11 @@ export async function readNativeChatTranscriptTailFile(
|
||||
}
|
||||
let latest = Number.NEGATIVE_INFINITY
|
||||
const chronological = newestFirst.toReversed().map((entry) => {
|
||||
const ordered = inTranscriptFileOrder(entry.message, latest)
|
||||
const ordered = inTranscriptFileOrder(
|
||||
entry.message,
|
||||
latest,
|
||||
sourceOrder ? entry.offset : undefined
|
||||
)
|
||||
latest = ordered.latest
|
||||
return { message: ordered.message, offset: entry.offset }
|
||||
})
|
||||
@@ -237,6 +242,7 @@ export async function readNativeChatTranscriptTail(
|
||||
filePath?: string
|
||||
limit: number
|
||||
beforeOffset?: number
|
||||
sourceOrder?: boolean
|
||||
},
|
||||
signal?: AbortSignal
|
||||
): Promise<
|
||||
@@ -282,7 +288,9 @@ export async function readNativeChatTranscriptTail(
|
||||
true,
|
||||
args.beforeOffset,
|
||||
decodeLifecycle,
|
||||
signal
|
||||
signal,
|
||||
undefined,
|
||||
args.sourceOrder
|
||||
)
|
||||
signal?.throwIfAborted()
|
||||
return {
|
||||
|
||||
@@ -28,6 +28,7 @@ export type SubscribeNativeChatTranscriptArgs = ResolveSessionFileOptions & {
|
||||
lifecycle?: NativeChatTurnLifecycle
|
||||
) => void
|
||||
initialLimit?: number
|
||||
sourceOrder?: boolean
|
||||
filePath?: string
|
||||
debounceMs?: number
|
||||
resolvePollIntervalMs?: number
|
||||
|
||||
@@ -77,7 +77,8 @@ export async function installTranscriptWatcher(
|
||||
(nextLifecycle) => {
|
||||
lifecycle = nextLifecycle
|
||||
},
|
||||
gateAbort.signal
|
||||
gateAbort.signal,
|
||||
args.sourceOrder
|
||||
)
|
||||
if (!closed && (remaining.length > 0 || lifecycle)) {
|
||||
onAppend(remaining, lifecycle)
|
||||
@@ -95,7 +96,8 @@ export async function installTranscriptWatcher(
|
||||
gateAbort.signal,
|
||||
(boundary) => {
|
||||
state.boundary = boundary
|
||||
}
|
||||
},
|
||||
args.sourceOrder
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,7 +201,8 @@ export async function installTranscriptWatcher(
|
||||
(nextLifecycle) => {
|
||||
lifecycle = nextLifecycle
|
||||
},
|
||||
gateAbort.signal
|
||||
gateAbort.signal,
|
||||
args.sourceOrder
|
||||
)
|
||||
if (closed) {
|
||||
return
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { AgentStatusIpcPayload } from '../../../../shared/agent-status-types'
|
||||
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import { eraseRpcMethods, type RpcContext } from '../core'
|
||||
import { NATIVE_CHAT_METHODS } from './native-chat'
|
||||
|
||||
const CONNECTION_ID = 'ssh-target-26057'
|
||||
const SESSION_ID = 'claude-session-26057'
|
||||
|
||||
let hookRows: Partial<AgentStatusIpcPayload>[] = []
|
||||
|
||||
function claudeLines(...texts: string[]): string {
|
||||
return texts
|
||||
.map((text, index) =>
|
||||
JSON.stringify({
|
||||
sessionId: SESSION_ID,
|
||||
uuid: `${SESSION_ID}-${index}`,
|
||||
timestamp: '2026-10-07T03:00:00.000Z',
|
||||
type: 'assistant',
|
||||
message: { role: 'assistant', content: [{ type: 'text', text }] }
|
||||
})
|
||||
)
|
||||
.map((line) => `${line}\n`)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function sshHookRow(transcriptPath: string): Partial<AgentStatusIpcPayload> {
|
||||
return {
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
connectionId: CONNECTION_ID,
|
||||
agentType: 'claude',
|
||||
providerSession: { key: 'session_id', id: SESSION_ID, transcriptPath }
|
||||
}
|
||||
}
|
||||
|
||||
const METHODS = eraseRpcMethods(NATIVE_CHAT_METHODS)
|
||||
|
||||
type SubscriptionRuntime = Pick<
|
||||
RpcContext['runtime'],
|
||||
'registerSubscriptionCleanup' | 'cleanupSubscription'
|
||||
>
|
||||
|
||||
function phoneContext(runtime: Partial<SubscriptionRuntime>): RpcContext {
|
||||
const members = { ...runtime, getAgentProviderSessionRows: () => hookRows }
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: native chat handlers use only the hook rows and the subscription-cleanup members.
|
||||
const handlerRuntime = members as unknown as RpcContext['runtime']
|
||||
return { runtime: handlerRuntime, connectionId: 'phone-1', clientKind: 'mobile' }
|
||||
}
|
||||
|
||||
function method(name: string): (typeof METHODS)[number] {
|
||||
const found = METHODS.find((candidate) => candidate.name === name)
|
||||
if (!found) {
|
||||
throw new Error(`${name} is not registered`)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function texts(value: unknown): string[] {
|
||||
if (!value || typeof value !== 'object' || !('messages' in value)) {
|
||||
return []
|
||||
}
|
||||
const messages: readonly NativeChatMessage[] = Array.isArray(value.messages) ? value.messages : []
|
||||
return messages.flatMap((message) =>
|
||||
message.blocks.flatMap((block) => (block.type === 'text' ? [block.text] : []))
|
||||
)
|
||||
}
|
||||
|
||||
async function readSession(
|
||||
transcriptPath: string
|
||||
): Promise<{ texts: string[]; error: unknown; notFound: unknown }> {
|
||||
const read = method('nativeChat.readSession')
|
||||
if ('stream' in read) {
|
||||
throw new Error('nativeChat.readSession is a stream')
|
||||
}
|
||||
const page = await read.handler(
|
||||
{ agent: 'claude', sessionId: SESSION_ID, transcriptPath },
|
||||
phoneContext({})
|
||||
)
|
||||
return {
|
||||
texts: texts(page),
|
||||
error: page && typeof page === 'object' && 'error' in page ? page.error : undefined,
|
||||
notFound: page && typeof page === 'object' && 'notFound' in page ? page.notFound : undefined
|
||||
}
|
||||
}
|
||||
|
||||
describe('native chat for an agent whose transcript lives on an SSH host (#26057)', () => {
|
||||
let localDir: string
|
||||
// A same-named local transcript must not override the remote hook's conversation.
|
||||
let transcriptPath: string
|
||||
|
||||
beforeEach(() => {
|
||||
localDir = mkdtempSync(join(tmpdir(), 'orca-ssh-transcript-'))
|
||||
transcriptPath = join(localDir, 'projects', 'p', `${SESSION_ID}.jsonl`)
|
||||
mkdirSync(dirname(transcriptPath), { recursive: true })
|
||||
writeFileSync(transcriptPath, claudeLines('LOCAL MACHINE FILE'))
|
||||
hookRows = [sshHookRow(transcriptPath)]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(localDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('never answers from this machine for an SSH-hosted transcript', async () => {
|
||||
const page = await readSession(transcriptPath)
|
||||
|
||||
expect(page.texts).toEqual([])
|
||||
expect(page.error).toBeDefined()
|
||||
})
|
||||
|
||||
it('keeps reading a local session from this machine', async () => {
|
||||
hookRows = [{ ...sshHookRow(transcriptPath), connectionId: null }]
|
||||
|
||||
const page = await readSession(transcriptPath)
|
||||
|
||||
expect(page.texts).toEqual(['LOCAL MACHINE FILE'])
|
||||
})
|
||||
})
|
||||
@@ -82,7 +82,8 @@ export const NATIVE_CHAT_METHODS = [
|
||||
params.transcriptPath
|
||||
),
|
||||
limit,
|
||||
beforeOffset: params.beforeOffset
|
||||
beforeOffset: params.beforeOffset,
|
||||
sourceOrder: params.capabilities?.transcriptSourceOrder === 1
|
||||
},
|
||||
signal
|
||||
)
|
||||
@@ -147,6 +148,7 @@ export const NATIVE_CHAT_METHODS = [
|
||||
params.transcriptPath
|
||||
),
|
||||
initialLimit: limit,
|
||||
sourceOrder: params.capabilities?.transcriptSourceOrder === 1,
|
||||
onInitialSnapshot: (messages, hasMore, beforeOffset, error, lifecycle) => {
|
||||
if (closed) {
|
||||
return
|
||||
|
||||
@@ -188,3 +188,53 @@ describe('incremental assembler — oracle differential', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds new canonical append decision work by the incoming batch at N and 10N, including null clocks', () => {
|
||||
const counts = [100, 1000].map((size) => {
|
||||
let reads = 0
|
||||
const base = Array.from({ length: size }, (_, i) => ({
|
||||
...msg({ id: `file-${i}`, timestamp: null }),
|
||||
get transcriptOrder() {
|
||||
reads += 1
|
||||
return i
|
||||
}
|
||||
}))
|
||||
const assembler = createIncrementalAssembler()
|
||||
reset(assembler, base)
|
||||
reads = 0
|
||||
applyAppends(assembler, [{ ...msg({ id: 'next', timestamp: null }), transcriptOrder: size }])
|
||||
expect(assembler.messages.at(-1)?.id).toBe('next')
|
||||
expect(reads).toBeLessThan(20)
|
||||
return reads
|
||||
})
|
||||
expect(counts[1]).toBeLessThanOrEqual(counts[0] + 1)
|
||||
})
|
||||
|
||||
it('matches a rebuild when supplementals or image units cross a canonical batch boundary', () => {
|
||||
const first = { ...msg({ id: 'first', timestamp: 100 }), transcriptOrder: 0 }
|
||||
const last = { ...msg({ id: 'last', timestamp: 0 }), transcriptOrder: 10 }
|
||||
const supplemental = msg({ id: 'middle', source: 'hook', timestamp: 50 })
|
||||
const source = {
|
||||
...msg({
|
||||
id: 'source',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: '[Image: source: /tmp/one.png]' }]
|
||||
}),
|
||||
transcriptOrder: 0
|
||||
}
|
||||
const prompt = {
|
||||
...msg({ id: 'prompt', role: 'user', blocks: [{ type: 'text', text: 'look [Image #1]' }] }),
|
||||
transcriptOrder: 10
|
||||
}
|
||||
const companion = { ...source, id: 'next-source', transcriptOrder: 20 }
|
||||
for (const [base, incoming] of [
|
||||
[[first, last], [supplemental]],
|
||||
[[source], [prompt, companion]]
|
||||
]) {
|
||||
const assembler = createIncrementalAssembler()
|
||||
reset(assembler, base)
|
||||
expect(normalizeImageTranscriptMessages(applyAppends(assembler, incoming))).toEqual(
|
||||
fullRebuild([...base, ...incoming])
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ export function applyAppends(
|
||||
return assembler.messages
|
||||
}
|
||||
|
||||
/** Require every message at/after the tail; null timestamps force a full sort because they belong first. */
|
||||
/** Compare canonical appends by source position, including null and backward provider clocks. */
|
||||
function isTailAppend(
|
||||
current: readonly NativeChatMessage[],
|
||||
incoming: readonly NativeChatMessage[]
|
||||
@@ -73,18 +73,26 @@ function isTailAppend(
|
||||
return true
|
||||
}
|
||||
if (
|
||||
hasImagePromptMarker(last) &&
|
||||
incoming[0]?.source === last.source &&
|
||||
incoming[0]?.timestamp === last.timestamp &&
|
||||
isImageSourceUserTurn(incoming[0]!)
|
||||
((hasImagePromptMarker(last) &&
|
||||
incoming[0]?.timestamp === last.timestamp &&
|
||||
isImageSourceUserTurn(incoming[0]!)) ||
|
||||
(isImageSourceUserTurn(last) && hasImagePromptMarker(incoming[0]!)))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// Prompt-first image folding displays companions before the prompt; retain their source high-water mark.
|
||||
const previous = current.at(-2)
|
||||
const boundary = previous && compareMessages(previous, last) > 0 ? previous : last
|
||||
for (const message of incoming) {
|
||||
if (message.timestamp === null) {
|
||||
// Supplemental insertion depends on the canonical sequence, not its last clock.
|
||||
if (boundary.transcriptOrder !== undefined && message.source !== 'transcript') {
|
||||
return false
|
||||
}
|
||||
if (compareMessages(message, last) < 0) {
|
||||
if (message.timestamp === null && message.transcriptOrder === undefined) {
|
||||
return false
|
||||
}
|
||||
if (compareMessages(message, boundary) < 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import { compareMessages } from './native-chat-session-assembler'
|
||||
import { orderMessages } from './native-chat-session-assembler'
|
||||
|
||||
/** Reuse assembler ordering: null timestamps first, then timestamp and id. */
|
||||
export function orderNativeChatMessages(messages: NativeChatMessage[]): NativeChatMessage[] {
|
||||
return [...messages].sort(compareMessages)
|
||||
return orderMessages(messages)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type NativeChatTranscriptProjection
|
||||
} from '../../../../shared/native-chat-transcript-projection'
|
||||
import type { NativeChatTurnJournal } from '../../../../shared/native-chat-turn-membership'
|
||||
import { compareMessages } from './native-chat-session-assembler'
|
||||
import { compareMessages, orderMessages } from './native-chat-session-assembler'
|
||||
|
||||
function sameMessage(left: NativeChatMessage, right: NativeChatMessage): boolean {
|
||||
// Most rows retain input identity; only folded assistant runs need field comparison.
|
||||
@@ -45,7 +45,7 @@ export function createNativeChatMessageListProjection(): (
|
||||
let previous: NativeChatTranscriptProjection = { conversation: [], subagentRows: new Map() }
|
||||
let byId = new Map<string, NativeChatMessage>()
|
||||
return (messages, journal) => {
|
||||
const projected = projectNativeChatTranscript(messages, compareMessages, journal)
|
||||
const projected = projectNativeChatTranscript(messages, compareMessages, journal, orderMessages)
|
||||
// Folding clones historical tool runs even when every contributing block is unchanged.
|
||||
const settle = (message: NativeChatMessage): NativeChatMessage => {
|
||||
const prior = byId.get(message.id)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { stripSentImagePromptMarkers } from '../../../../shared/native-chat-imag
|
||||
import { projectNativeChatTranscriptMessages } from '../../../../shared/native-chat-transcript-projection'
|
||||
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import type { NativeChatCommandMarker } from './native-chat-command-marker'
|
||||
import { compareMessages } from './native-chat-session-assembler'
|
||||
import { compareMessages, orderMessages } from './native-chat-session-assembler'
|
||||
|
||||
export type NativeChatRecallSource = {
|
||||
messages: readonly NativeChatMessage[]
|
||||
@@ -55,7 +55,11 @@ export function nativeChatSentPrompts(source: NativeChatRecallSource): NativeCha
|
||||
const commands = source.commands ?? []
|
||||
let nextCommand = 0
|
||||
// Use projected rows so harness turns and subagent prompts never enter user recall.
|
||||
for (const message of projectNativeChatTranscriptMessages(source.messages, compareMessages)) {
|
||||
for (const message of projectNativeChatTranscriptMessages(
|
||||
source.messages,
|
||||
compareMessages,
|
||||
orderMessages
|
||||
)) {
|
||||
// `from` marks a prompt another agent sent; it was never typed here.
|
||||
if (message.role !== 'user' || message.from) {
|
||||
continue
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
type NativeChatSessionStatus
|
||||
} from '../../../../shared/native-chat-types'
|
||||
import { NATIVE_CHAT_STREAMING_ID } from '../../../../shared/native-chat/native-chat-streaming'
|
||||
import { compareNativeChatTranscriptMessages } from '../../../../shared/native-chat-transcript-projection'
|
||||
import {
|
||||
compareNativeChatTranscriptMessages,
|
||||
orderNativeChatTranscriptEntries
|
||||
} from '../../../../shared/native-chat-transcript-projection'
|
||||
import {
|
||||
hasImagePromptMarker,
|
||||
isImageSourceUserTurn,
|
||||
@@ -95,6 +98,19 @@ export function compareMessages(a: NativeChatMessage, b: NativeChatMessage): num
|
||||
return compareNativeChatTranscriptMessages(a, b)
|
||||
}
|
||||
|
||||
function orderEntries<T>(entries: readonly T[], messageOf: (entry: T) => NativeChatMessage): T[] {
|
||||
return [0, 1, 2].flatMap((rank) => {
|
||||
const tier = entries.filter((entry) => messageSortRank(messageOf(entry)) === rank)
|
||||
return rank === 0
|
||||
? orderNativeChatTranscriptEntries(tier, messageOf)
|
||||
: tier.sort((a, b) => compareMessages(messageOf(a), messageOf(b)))
|
||||
})
|
||||
}
|
||||
|
||||
export function orderMessages(messages: readonly NativeChatMessage[]): NativeChatMessage[] {
|
||||
return orderEntries(messages, (message) => message)
|
||||
}
|
||||
|
||||
export function assembleNativeChatSession(
|
||||
input: AssembleNativeChatSessionInput
|
||||
): NativeChatSession {
|
||||
@@ -114,7 +130,7 @@ export function assembleNativeChatSession(
|
||||
mergeOne(byId, byTurn, message)
|
||||
}
|
||||
|
||||
const messages = Array.from(byId.values()).sort(compareMessages)
|
||||
const messages = orderMessages(Array.from(byId.values()))
|
||||
|
||||
const derivedStatus: NativeChatSessionStatus = messages.length === 0 ? 'empty' : 'ready'
|
||||
|
||||
@@ -136,23 +152,27 @@ type ImageNormalizationUnit = {
|
||||
export function sortForImageNormalization(
|
||||
messages: readonly NativeChatMessage[]
|
||||
): readonly NativeChatMessage[] {
|
||||
// Restore file adjacency before supplemental rows can be inserted between image companions.
|
||||
const sourceOrdered = messages.some((message) => message.transcriptOrder !== undefined)
|
||||
? Array.from(messages).sort(compareMessages)
|
||||
: messages
|
||||
const units: ImageNormalizationUnit[] = []
|
||||
for (let index = 0; index < messages.length; index += 1) {
|
||||
const message = messages[index]!
|
||||
for (let index = 0; index < sourceOrdered.length; index += 1) {
|
||||
const message = sourceOrdered[index]!
|
||||
const companions: NativeChatMessage[] = []
|
||||
let prompt: NativeChatMessage | undefined
|
||||
|
||||
if (isImageSourceUserTurn(message)) {
|
||||
let nextIndex = index
|
||||
while (
|
||||
nextIndex < messages.length &&
|
||||
messages[nextIndex]?.source === message.source &&
|
||||
isImageSourceUserTurn(messages[nextIndex]!)
|
||||
nextIndex < sourceOrdered.length &&
|
||||
sourceOrdered[nextIndex]?.source === message.source &&
|
||||
isImageSourceUserTurn(sourceOrdered[nextIndex]!)
|
||||
) {
|
||||
companions.push(messages[nextIndex]!)
|
||||
companions.push(sourceOrdered[nextIndex]!)
|
||||
nextIndex += 1
|
||||
}
|
||||
const candidate = messages[nextIndex]
|
||||
const candidate = sourceOrdered[nextIndex]
|
||||
if (
|
||||
candidate?.role === 'user' &&
|
||||
candidate.source === message.source &&
|
||||
@@ -164,7 +184,7 @@ export function sortForImageNormalization(
|
||||
companions.length = 0
|
||||
}
|
||||
} else {
|
||||
const candidate = messages[index + 1]
|
||||
const candidate = sourceOrdered[index + 1]
|
||||
if (
|
||||
message.role === 'user' &&
|
||||
hasImagePromptMarker(message) &&
|
||||
@@ -174,12 +194,12 @@ export function sortForImageNormalization(
|
||||
) {
|
||||
let nextIndex = index + 1
|
||||
while (
|
||||
nextIndex < messages.length &&
|
||||
messages[nextIndex]?.source === message.source &&
|
||||
messages[nextIndex]?.timestamp === message.timestamp &&
|
||||
isImageSourceUserTurn(messages[nextIndex]!)
|
||||
nextIndex < sourceOrdered.length &&
|
||||
sourceOrdered[nextIndex]?.source === message.source &&
|
||||
sourceOrdered[nextIndex]?.timestamp === message.timestamp &&
|
||||
isImageSourceUserTurn(sourceOrdered[nextIndex]!)
|
||||
) {
|
||||
companions.push(messages[nextIndex]!)
|
||||
companions.push(sourceOrdered[nextIndex]!)
|
||||
nextIndex += 1
|
||||
}
|
||||
prompt = message
|
||||
@@ -191,8 +211,7 @@ export function sortForImageNormalization(
|
||||
units.push({ messages: unitMessages, sortKey: prompt ?? message })
|
||||
}
|
||||
|
||||
units.sort((a, b) => compareMessages(a.sortKey, b.sortKey))
|
||||
const sorted = units.flatMap((unit) => unit.messages)
|
||||
const sorted = orderEntries(units, (unit) => unit.sortKey).flatMap((unit) => unit.messages)
|
||||
return sorted.every((message, index) => message === messages[index]) ? messages : sorted
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,13 @@ describe('getNativeChatSessionTransport — selection', () => {
|
||||
expect(runtimeEnvironmentsCall).toHaveBeenCalledWith({
|
||||
selector: ENV,
|
||||
method: 'nativeChat.readSession',
|
||||
params: { agent: 'claude', sessionId: 'sess-1', limit: 40, transcriptPath: '/t/path' },
|
||||
params: {
|
||||
agent: 'claude',
|
||||
sessionId: 'sess-1',
|
||||
limit: 40,
|
||||
transcriptPath: '/t/path',
|
||||
capabilities: { transcriptSourceOrder: 1 }
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(nativeChatReadSession).not.toHaveBeenCalled()
|
||||
@@ -181,7 +187,9 @@ describe('runtime subscribe', () => {
|
||||
|
||||
expect(runtimeEnvironmentsSubscribe).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({ capabilities: { transcriptPending: 1 } })
|
||||
params: expect.objectContaining({
|
||||
capabilities: { transcriptPending: 1, transcriptSourceOrder: 1 }
|
||||
})
|
||||
}),
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ function createRuntimeNativeChatTransport(environmentId: string): NativeChatSess
|
||||
const result = await callRuntimeRpc<unknown>(
|
||||
target,
|
||||
'nativeChat.readSession',
|
||||
{ agent, sessionId, limit, transcriptPath },
|
||||
{ agent, sessionId, limit, transcriptPath, capabilities: { transcriptSourceOrder: 1 } },
|
||||
{ timeoutMs: 15_000 }
|
||||
)
|
||||
return parseRuntimeNativeChatReadSessionResult(result)
|
||||
@@ -94,7 +94,7 @@ function createRuntimeNativeChatTransport(environmentId: string): NativeChatSess
|
||||
sessionId,
|
||||
transcriptPath,
|
||||
limit,
|
||||
capabilities: { transcriptPending: 1 }
|
||||
capabilities: { transcriptPending: 1, transcriptSourceOrder: 1 }
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
},
|
||||
|
||||
@@ -18,7 +18,8 @@ export function createWebNativeChatApi(): NativeChatApi {
|
||||
agent,
|
||||
sessionId,
|
||||
limit,
|
||||
transcriptPath
|
||||
transcriptPath,
|
||||
capabilities: { transcriptSourceOrder: 1 }
|
||||
})
|
||||
),
|
||||
subscribe: (args, onFrame) => {
|
||||
@@ -48,7 +49,7 @@ export function createWebNativeChatApi(): NativeChatApi {
|
||||
subscriptionId: args.subscriptionId,
|
||||
transcriptPath: args.transcriptPath,
|
||||
limit: args.limit,
|
||||
capabilities: { transcriptPending: 1 }
|
||||
capabilities: { transcriptPending: 1, transcriptSourceOrder: 1 }
|
||||
},
|
||||
{
|
||||
onResponse: (response) => {
|
||||
|
||||
@@ -25,9 +25,11 @@ describe('web native chat preload API', () => {
|
||||
timestamp: 42,
|
||||
source: 'transcript' as const
|
||||
}
|
||||
let readCall: { method: string; params: unknown } | undefined
|
||||
vi.doMock('./web-runtime-client', () => ({
|
||||
WebRuntimeClient: class {
|
||||
call(): Promise<RuntimeRpcResponse<unknown>> {
|
||||
call(method: string, params: unknown): Promise<RuntimeRpcResponse<unknown>> {
|
||||
readCall = { method, params }
|
||||
return Promise.resolve({
|
||||
id: 'read-1',
|
||||
ok: true,
|
||||
@@ -70,6 +72,10 @@ describe('web native chat preload API', () => {
|
||||
lifecycle
|
||||
}
|
||||
)
|
||||
expect(readCall).toMatchObject({
|
||||
method: 'nativeChat.readSession',
|
||||
params: { capabilities: { transcriptSourceOrder: 1 } }
|
||||
})
|
||||
const frames: unknown[] = []
|
||||
globals.window.api.nativeChat.subscribe(
|
||||
{ subscriptionId: 'sub-1', agent: 'claude', sessionId: 'session-1' },
|
||||
@@ -130,7 +136,9 @@ describe('web native chat preload API', () => {
|
||||
)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(subscribeParams).toMatchObject({ capabilities: { transcriptPending: 1 } })
|
||||
expect(subscribeParams).toMatchObject({
|
||||
capabilities: { transcriptPending: 1, transcriptSourceOrder: 1 }
|
||||
})
|
||||
|
||||
// The host's unflushed-transcript frame, then the flush that follows it.
|
||||
deliver({ type: 'snapshot', messages: [], hasMore: false, pending: true })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import './native-chat-reasoning-order.spec'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type {
|
||||
AgentJournalItemBody,
|
||||
|
||||
@@ -80,30 +80,60 @@ function projectRows(messages: readonly NativeChatMessage[]): NativeChatMessage[
|
||||
return stripNoiseMessages(foldToolMessages(foldSubagentItems(messages)))
|
||||
}
|
||||
|
||||
function sortedCopy(
|
||||
messages: readonly NativeChatMessage[],
|
||||
compare: NativeChatMessageCompare
|
||||
): NativeChatMessage[] {
|
||||
// Not `toSorted`: mobile's Hermes lacks it, and src/shared must stay loadable there.
|
||||
return Array.from(messages).sort(compare)
|
||||
/** Preserve canonical sequence, inserting ordinary supplemental rows by their actual clocks. */
|
||||
export function orderNativeChatTranscriptEntries<T>(
|
||||
entries: readonly T[],
|
||||
messageOf: (entry: T) => NativeChatMessage,
|
||||
compare: NativeChatMessageCompare = compareNativeChatTranscriptMessages
|
||||
): T[] {
|
||||
if (entries.some((entry) => messageOf(entry).journalPosition)) {
|
||||
return Array.from(entries).sort((a, b) => compare(messageOf(a), messageOf(b)))
|
||||
}
|
||||
const canonical: T[] = []
|
||||
const supplemental: T[] = []
|
||||
for (const entry of entries) {
|
||||
const message = messageOf(entry)
|
||||
const target =
|
||||
message.source !== 'transcript' && message.transcriptOrder === undefined
|
||||
? supplemental
|
||||
: canonical
|
||||
target.push(entry)
|
||||
}
|
||||
canonical.sort((a, b) => compare(messageOf(a), messageOf(b)))
|
||||
supplemental.sort((a, b) => compareNativeChatMessagesByTime(messageOf(a), messageOf(b)))
|
||||
const ordered: T[] = []
|
||||
let next = 0
|
||||
for (const entry of canonical) {
|
||||
while (
|
||||
next < supplemental.length &&
|
||||
compareNativeChatMessagesByTime(messageOf(supplemental[next]!), messageOf(entry)) < 0
|
||||
) {
|
||||
ordered.push(supplemental[next++]!)
|
||||
}
|
||||
ordered.push(entry)
|
||||
}
|
||||
return ordered.concat(supplemental.slice(next))
|
||||
}
|
||||
|
||||
/** The conversation alone. `compare` lets the renderer order its own tail rows
|
||||
* (streaming, optimistic sends), which never exist on the host. */
|
||||
export function projectNativeChatTranscriptMessages(
|
||||
messages: readonly NativeChatMessage[],
|
||||
compare: NativeChatMessageCompare = compareNativeChatTranscriptMessages
|
||||
compare: NativeChatMessageCompare = compareNativeChatTranscriptMessages,
|
||||
order?: (messages: readonly NativeChatMessage[]) => NativeChatMessage[]
|
||||
): NativeChatMessage[] {
|
||||
return projectNativeChatTranscript(messages, compare).conversation
|
||||
return projectNativeChatTranscript(messages, compare, undefined, order).conversation
|
||||
}
|
||||
|
||||
/** Use conversation turn membership for each subagent row, as though it were a parent row at that position. */
|
||||
export function projectNativeChatTranscript(
|
||||
messages: readonly NativeChatMessage[],
|
||||
compare: NativeChatMessageCompare = compareNativeChatTranscriptMessages,
|
||||
journal?: NativeChatTurnJournal | null
|
||||
journal?: NativeChatTurnJournal | null,
|
||||
order = (rows: readonly NativeChatMessage[]) =>
|
||||
orderNativeChatTranscriptEntries(rows, (row) => row, compare)
|
||||
): NativeChatTranscriptProjection {
|
||||
const sorted = sortedCopy(messages, compare)
|
||||
const sorted = order(messages)
|
||||
const own: NativeChatMessage[] = []
|
||||
const byAgent = new Map<string, NativeChatMessage[]>()
|
||||
for (const message of sorted) {
|
||||
|
||||
@@ -204,12 +204,12 @@ export type NativeChatMessage = AgentJournalProducerLinkage & {
|
||||
id: string
|
||||
role: NativeChatRole
|
||||
blocks: NativeChatBlock[]
|
||||
/** Epoch ms; null when unavailable (e.g. scrape), sorting before timestamps. */
|
||||
/** Epoch ms; null when unavailable (e.g. scrape). */
|
||||
timestamp: number | null
|
||||
source: NativeChatSource
|
||||
/** Optional provider row cursor; split projections share it for whole-row paging. */
|
||||
transcriptOffset?: number
|
||||
/** Display order when the provider's sequence differs from its stable paging cursor. */
|
||||
/** Canonical display position: JSONL record byte start or database provider sequence, independent of clocks/cursors. */
|
||||
transcriptOrder?: number
|
||||
/** Model id that produced an assistant response, as the provider API names it. */
|
||||
model?: string
|
||||
|
||||
@@ -25,7 +25,12 @@ export const NativeChatSession = z.object({
|
||||
// Hook transcript paths locate files when provider session IDs no longer name them; optional for old clients.
|
||||
transcriptPath: z.string().min(1).optional(),
|
||||
// Gate pending snapshots: legacy clients mistake them for settled empty history and overwrite retention/unblock drafts.
|
||||
capabilities: z.object({ transcriptPending: z.literal(1).optional() }).optional(),
|
||||
capabilities: z
|
||||
.object({
|
||||
transcriptPending: z.literal(1).optional(),
|
||||
transcriptSourceOrder: z.literal(1).optional()
|
||||
})
|
||||
.optional(),
|
||||
beforeOffset: z.number().int().nonnegative().optional()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const { transport, resetTransport } = vi.hoisted(() => {
|
||||
let listener: (frame: unknown) => void = () => {}
|
||||
const unsubscribe = vi.fn()
|
||||
const transport = {
|
||||
unsubscribe,
|
||||
readSession: vi.fn(),
|
||||
subscribe: vi.fn((_args, onFrame) => {
|
||||
listener = onFrame
|
||||
return unsubscribe
|
||||
}),
|
||||
emit: (frame: unknown) => listener(frame)
|
||||
}
|
||||
return {
|
||||
transport,
|
||||
resetTransport: () => {
|
||||
listener = () => {}
|
||||
transport.readSession.mockReset()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../src/renderer/src/components/native-chat/native-chat-session-transport', () => ({
|
||||
getNativeChatSessionTransport: () => transport
|
||||
}))
|
||||
|
||||
import {
|
||||
useNativeChatLiveSession,
|
||||
type NativeChatLiveSession,
|
||||
type UseNativeChatLiveSessionArgs
|
||||
} from '../../src/renderer/src/components/native-chat/use-native-chat-live-session'
|
||||
|
||||
describe('useNativeChatLiveSession — file watcher pagination composition', () => {
|
||||
const AGENT = 'claude' as const
|
||||
const SESSION = 'sess-1'
|
||||
const PANE = 'pane-1'
|
||||
const roots: Root[] = []
|
||||
let latest: NativeChatLiveSession | null = null
|
||||
|
||||
function Probe(props: UseNativeChatLiveSessionArgs): null {
|
||||
latest = useNativeChatLiveSession(props)
|
||||
return null
|
||||
}
|
||||
|
||||
async function flush(): Promise<void> {
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
async function render(props: UseNativeChatLiveSessionArgs): Promise<Root> {
|
||||
const container = document.createElement('div')
|
||||
const root = createRoot(container)
|
||||
roots.push(root)
|
||||
await act(async () => {
|
||||
root.render(createElement(Probe, props))
|
||||
})
|
||||
await flush()
|
||||
return root
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useAppStore.setState({ agentStatusByPaneKey: {} })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
act(() => root.unmount())
|
||||
}
|
||||
latest = null
|
||||
vi.clearAllMocks()
|
||||
resetTransport()
|
||||
})
|
||||
|
||||
it('keeps the original watcher append after a real 300-to-500 loadEarlier', async () => {
|
||||
const { mkdtemp, writeFile, appendFile, rm } = await import('node:fs/promises')
|
||||
const { tmpdir } = await import('node:os')
|
||||
const { join } = await import('node:path')
|
||||
const { readNativeChatTranscriptTail, subscribeNativeChatTranscript } =
|
||||
await import('../../src/main/native-chat/transcript-watch')
|
||||
const { createNativeChatMessageListProjection } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-message-list-projection')
|
||||
const directory = await mkdtemp(join(tmpdir(), 'pr31794-live-'))
|
||||
const filePath = join(directory, 'session.jsonl')
|
||||
const line = (n: number) =>
|
||||
`${JSON.stringify({
|
||||
type: 'assistant',
|
||||
uuid: String(n).padStart(4, '0'),
|
||||
timestamp: '2026-06-01T10:00:00Z',
|
||||
message: { role: 'assistant', content: `reply ${n}` }
|
||||
})}\n`
|
||||
await writeFile(filePath, Array.from({ length: 500 }, (_, i) => line(i + 1)).join(''))
|
||||
transport.readSession.mockImplementation((_agent, _session, limit) =>
|
||||
readNativeChatTranscriptTail({
|
||||
agent: AGENT,
|
||||
sessionId: SESSION,
|
||||
filePath,
|
||||
limit,
|
||||
sourceOrder: true
|
||||
})
|
||||
)
|
||||
const session = () => {
|
||||
if (!latest) {
|
||||
throw new Error('No live session')
|
||||
}
|
||||
return latest
|
||||
}
|
||||
const ids = () =>
|
||||
createNativeChatMessageListProjection()(session().messages).conversation.map((row) => row.id)
|
||||
await render({
|
||||
paneKey: PANE,
|
||||
agent: AGENT,
|
||||
sessionId: SESSION,
|
||||
runtimeEnvironmentId: 'env-history'
|
||||
})
|
||||
const initialSnapshotCommitted = Promise.withResolvers<void>()
|
||||
const appendCommitted = Promise.withResolvers<void>()
|
||||
const subscription = await subscribeNativeChatTranscript({
|
||||
agent: AGENT,
|
||||
sessionId: SESSION,
|
||||
filePath,
|
||||
sourceOrder: true,
|
||||
initialLimit: 300,
|
||||
debounceMs: 5,
|
||||
reconciliationIntervalMs: 20,
|
||||
onInitialSnapshot: (messages, hasMore) => {
|
||||
void act(async () => {
|
||||
transport.emit({ type: 'snapshot', messages, hasMore })
|
||||
}).then(initialSnapshotCommitted.resolve, initialSnapshotCommitted.reject)
|
||||
},
|
||||
onAppend: (messages) => {
|
||||
void act(async () => {
|
||||
transport.emit({ type: 'appended', messages })
|
||||
}).then(appendCommitted.resolve, appendCommitted.reject)
|
||||
},
|
||||
onReplace: (messages, hasMore) =>
|
||||
act(() => transport.emit({ type: 'replacement', messages, hasMore }))
|
||||
})
|
||||
try {
|
||||
await initialSnapshotCommitted.promise
|
||||
expect(ids()).toEqual(Array.from({ length: 300 }, (_, i) => String(i + 201).padStart(4, '0')))
|
||||
await act(async () => {
|
||||
await expect(session().loadEarlier()).resolves.toBe('applied')
|
||||
})
|
||||
expect(transport.readSession).toHaveBeenLastCalledWith(AGENT, SESSION, 500, undefined)
|
||||
expect(ids()).toEqual(Array.from({ length: 500 }, (_, i) => String(i + 1).padStart(4, '0')))
|
||||
await appendFile(filePath, line(501))
|
||||
await appendCommitted.promise
|
||||
expect(ids()).toEqual(Array.from({ length: 501 }, (_, i) => String(i + 1).padStart(4, '0')))
|
||||
expect(ids().at(-1)).toBe('0501')
|
||||
expect(transport.subscribe).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
subscription.unsubscribe()
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,227 @@
|
||||
import { appendFileSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { AgentStatusIpcPayload } from '../../src/shared/agent-status-types'
|
||||
import type { NativeChatMessage } from '../../src/shared/native-chat-types'
|
||||
import { eraseRpcMethods, type RpcContext } from '../../src/main/runtime/rpc/core'
|
||||
import { NATIVE_CHAT_METHODS } from '../../src/main/runtime/rpc/methods/native-chat'
|
||||
|
||||
const CONNECTION_ID = 'ssh-target-26057'
|
||||
const SESSION_ID = 'claude-session-26057'
|
||||
|
||||
let hookRows: Partial<AgentStatusIpcPayload>[] = []
|
||||
|
||||
function claudeLines(...texts: string[]): string {
|
||||
return texts
|
||||
.map((text, index) =>
|
||||
JSON.stringify({
|
||||
sessionId: SESSION_ID,
|
||||
uuid: `${SESSION_ID}-${index}`,
|
||||
timestamp: '2026-10-07T03:00:00.000Z',
|
||||
type: 'assistant',
|
||||
message: { role: 'assistant', content: [{ type: 'text', text }] }
|
||||
})
|
||||
)
|
||||
.map((line) => `${line}\n`)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function sshHookRow(transcriptPath: string): Partial<AgentStatusIpcPayload> {
|
||||
return {
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
connectionId: CONNECTION_ID,
|
||||
agentType: 'claude',
|
||||
providerSession: { key: 'session_id', id: SESSION_ID, transcriptPath }
|
||||
}
|
||||
}
|
||||
|
||||
const METHODS = eraseRpcMethods(NATIVE_CHAT_METHODS)
|
||||
|
||||
type SubscriptionRuntime = Pick<
|
||||
RpcContext['runtime'],
|
||||
'registerSubscriptionCleanup' | 'cleanupSubscription'
|
||||
>
|
||||
|
||||
function phoneContext(runtime: Partial<SubscriptionRuntime>): RpcContext {
|
||||
const members = { ...runtime, getAgentProviderSessionRows: () => hookRows }
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: native chat handlers use only the hook rows and the subscription-cleanup members.
|
||||
const handlerRuntime = members as unknown as RpcContext['runtime']
|
||||
return { runtime: handlerRuntime, connectionId: 'phone-1', clientKind: 'mobile' }
|
||||
}
|
||||
|
||||
function method(name: string): (typeof METHODS)[number] {
|
||||
const found = METHODS.find((candidate) => candidate.name === name)
|
||||
if (!found) {
|
||||
throw new Error(`${name} is not registered`)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function texts(value: unknown): string[] {
|
||||
if (!value || typeof value !== 'object' || !('messages' in value)) {
|
||||
return []
|
||||
}
|
||||
const messages: readonly NativeChatMessage[] = Array.isArray(value.messages) ? value.messages : []
|
||||
return messages.flatMap((message) =>
|
||||
message.blocks.flatMap((block) => (block.type === 'text' ? [block.text] : []))
|
||||
)
|
||||
}
|
||||
|
||||
async function readSession(
|
||||
transcriptPath: string
|
||||
): Promise<{ texts: string[]; error: unknown; notFound: unknown }> {
|
||||
const read = method('nativeChat.readSession')
|
||||
if ('stream' in read) {
|
||||
throw new Error('nativeChat.readSession is a stream')
|
||||
}
|
||||
const page = await read.handler(
|
||||
{ agent: 'claude', sessionId: SESSION_ID, transcriptPath },
|
||||
phoneContext({})
|
||||
)
|
||||
return {
|
||||
texts: texts(page),
|
||||
error: page && typeof page === 'object' && 'error' in page ? page.error : undefined,
|
||||
notFound: page && typeof page === 'object' && 'notFound' in page ? page.notFound : undefined
|
||||
}
|
||||
}
|
||||
|
||||
describe('native chat for an agent whose transcript lives on an SSH host (#26057)', () => {
|
||||
let localDir: string
|
||||
// A same-named local transcript must not override the remote hook's conversation.
|
||||
let transcriptPath: string
|
||||
|
||||
beforeEach(() => {
|
||||
localDir = mkdtempSync(join(tmpdir(), 'orca-ssh-transcript-'))
|
||||
transcriptPath = join(localDir, 'projects', 'p', `${SESSION_ID}.jsonl`)
|
||||
mkdirSync(dirname(transcriptPath), { recursive: true })
|
||||
writeFileSync(transcriptPath, claudeLines('LOCAL MACHINE FILE'))
|
||||
hookRows = [sshHookRow(transcriptPath)]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(localDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'negotiates reader and every stream representation before slicing (modern=%s)',
|
||||
async (modern) => {
|
||||
const { NativeChatSession } = await import('../../src/shared/rpc-contract/native-chat-params')
|
||||
const { compareNativeChatTranscriptMessages } =
|
||||
await import('../../src/shared/native-chat-transcript-projection')
|
||||
const { parseRuntimeNativeChatReadSessionResult } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-runtime-contract')
|
||||
const { createNativeChatMessageListProjection } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-message-list-projection')
|
||||
const { commandMarkersAsMessages } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-command-marker')
|
||||
hookRows = [{ ...sshHookRow(transcriptPath), connectionId: null }]
|
||||
const line = (id: string, seconds: number) =>
|
||||
`${JSON.stringify({
|
||||
type: 'assistant',
|
||||
uuid: id,
|
||||
timestamp: `2026-06-01T10:00:${String(seconds).padStart(2, '0')}.000Z`,
|
||||
message: { role: 'assistant', content: id }
|
||||
})}\n`
|
||||
writeFileSync(transcriptPath, line('z-first', 30) + line('a-second', 10))
|
||||
const params = NativeChatSession.parse({
|
||||
agent: 'claude',
|
||||
sessionId: SESSION_ID,
|
||||
transcriptPath,
|
||||
limit: 2,
|
||||
...(modern ? { capabilities: { transcriptSourceOrder: 1 } } : {})
|
||||
})
|
||||
const read = method('nativeChat.readSession')
|
||||
const subscribe = method('nativeChat.subscribe')
|
||||
if ('stream' in read || !('stream' in subscribe)) {
|
||||
throw new Error('Unexpected RPC method kind')
|
||||
}
|
||||
const parse = (payload: unknown) => {
|
||||
const result = parseRuntimeNativeChatReadSessionResult(payload)
|
||||
if (!('messages' in result)) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
return result.messages
|
||||
}
|
||||
const rows = parse(await read.handler(params, phoneContext({})))
|
||||
const time = Date.parse('2026-06-01T10:00:00Z')
|
||||
expect(rows.map((row) => row.timestamp)).toEqual(
|
||||
modern ? [time + 30000, time + 10000] : [time + 30000, time + 30001]
|
||||
)
|
||||
expect(rows.map((row) => row.transcriptOrder)).toEqual(
|
||||
modern ? [0, Buffer.byteLength(line('z-first', 30))] : [undefined, undefined]
|
||||
)
|
||||
const marker = commandMarkersAsMessages([
|
||||
{ id: 'compact', command: '/compact', sentAt: time + 20000 }
|
||||
])[0]
|
||||
const ordered = modern
|
||||
? createNativeChatMessageListProjection()([...rows, marker]).conversation
|
||||
: [...rows, marker].sort(compareNativeChatTranscriptMessages)
|
||||
expect(ordered.map((row) => row.id)).toEqual([marker.id, 'z-first', 'a-second'])
|
||||
// The previous nested schema strips the unknown semantic on a new-client/old-host request.
|
||||
const oldSchema = NativeChatSession.extend({
|
||||
capabilities: NativeChatSession.shape.capabilities
|
||||
.unwrap()
|
||||
.omit({ transcriptSourceOrder: true })
|
||||
.optional()
|
||||
})
|
||||
const oldParams = oldSchema.parse({ ...params, capabilities: { transcriptSourceOrder: 1 } })
|
||||
expect(oldParams.capabilities).toEqual({})
|
||||
expect(
|
||||
parse(await read.handler(oldParams, phoneContext({}))).map((row) => row.transcriptOrder)
|
||||
).toEqual([undefined, undefined])
|
||||
let cleanup = () => {}
|
||||
const context = phoneContext({
|
||||
registerSubscriptionCleanup: (_key, stop) => {
|
||||
cleanup = stop
|
||||
}
|
||||
})
|
||||
const frames: { type: unknown; messages: NativeChatMessage[] }[] = []
|
||||
await subscribe.handler(params, context, (frame) => {
|
||||
if (frame && typeof frame === 'object' && 'type' in frame && 'messages' in frame) {
|
||||
frames.push({ type: frame.type, messages: parse(frame) })
|
||||
}
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => frames.length, { timeout: 5000 }).toBe(1)
|
||||
expect(frames[0]).toMatchObject({ type: 'snapshot', messages: rows })
|
||||
appendFileSync(transcriptPath, line('next', 5))
|
||||
await expect.poll(() => frames.length, { timeout: 5000 }).toBe(2)
|
||||
expect(frames[1]).toMatchObject({
|
||||
type: 'appended',
|
||||
messages: [{ id: 'next', timestamp: modern ? time + 5000 : time + 30002 }]
|
||||
})
|
||||
expect(frames[1].messages[0].transcriptOrder).toBe(
|
||||
modern ? Buffer.byteLength(line('z-first', 30) + line('a-second', 10)) : undefined
|
||||
)
|
||||
writeFileSync(transcriptPath, line('replacement', 1))
|
||||
await expect.poll(() => frames.length, { timeout: 5000 }).toBe(3)
|
||||
expect(frames[2]).toMatchObject({
|
||||
type: 'replacement',
|
||||
messages: [{ id: 'replacement', timestamp: time + 1000 }]
|
||||
})
|
||||
expect(frames[2].messages[0].transcriptOrder).toBe(modern ? 0 : undefined)
|
||||
appendFileSync(transcriptPath, line('after-replacement', 0))
|
||||
await expect.poll(() => frames.length, { timeout: 5000 }).toBe(4)
|
||||
expect(frames[3].messages[0].timestamp).toBe(modern ? time : time + 1001)
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('never answers from this machine for an SSH-hosted transcript', async () => {
|
||||
const page = await readSession(transcriptPath)
|
||||
|
||||
expect(page.texts).toEqual([])
|
||||
expect(page.error).toBeDefined()
|
||||
})
|
||||
|
||||
it('keeps reading a local session from this machine', async () => {
|
||||
hookRows = [{ ...sshHookRow(transcriptPath), connectionId: null }]
|
||||
|
||||
const page = await readSession(transcriptPath)
|
||||
|
||||
expect(page.texts).toEqual(['LOCAL MACHINE FILE'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,423 @@
|
||||
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { NativeChatMessage } from '../../src/shared/native-chat-types'
|
||||
import { projectNativeChatTranscriptMessages } from '../../src/shared/native-chat-transcript-projection'
|
||||
import { createNativeChatMerger, replaceList } from '../../src/shared/native-chat-merge'
|
||||
import { readNativeChatTranscript } from '../../src/main/native-chat/transcript-reader'
|
||||
import {
|
||||
readNativeChatTranscriptTail,
|
||||
subscribeNativeChatTranscript
|
||||
} from '../../src/main/native-chat/transcript-watch'
|
||||
|
||||
// Claude timestamps queued prompts at send time but writes them after the tool result that absorbed them.
|
||||
const PROMPT = {
|
||||
type: 'user',
|
||||
uuid: 'u-1',
|
||||
timestamp: '2026-06-01T10:00:00.000Z',
|
||||
message: { role: 'user', content: 'start the long task' }
|
||||
}
|
||||
const TOOL_CALL = {
|
||||
type: 'assistant',
|
||||
uuid: 'a-1',
|
||||
timestamp: '2026-06-01T10:00:01.000Z',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'tool_use', id: 'toolu_1', name: 'Bash', input: { command: 'sleep 30' } }]
|
||||
}
|
||||
}
|
||||
const TOOL_RESULT = {
|
||||
type: 'user',
|
||||
uuid: 'r-1',
|
||||
timestamp: '2026-06-01T10:00:30.000Z',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'done' }]
|
||||
}
|
||||
}
|
||||
const QUEUED = {
|
||||
type: 'attachment',
|
||||
uuid: 'q-1',
|
||||
parentUuid: 'r-1',
|
||||
timestamp: '2026-06-01T10:00:10.000Z',
|
||||
attachment: {
|
||||
type: 'queued_command',
|
||||
prompt: 'also check the config',
|
||||
commandMode: 'prompt',
|
||||
origin: { kind: 'human' },
|
||||
timestamp: '2026-06-01T10:00:10.000Z'
|
||||
}
|
||||
}
|
||||
const REPLY = {
|
||||
type: 'assistant',
|
||||
uuid: 'a-2',
|
||||
timestamp: '2026-06-01T10:00:35.000Z',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
stop_reason: 'end_turn',
|
||||
content: [{ type: 'text', text: 'both done' }]
|
||||
}
|
||||
}
|
||||
|
||||
const lines = (...records: object[]): string =>
|
||||
records.map((record) => `${JSON.stringify(record)}\n`).join('')
|
||||
|
||||
let roots: string[] = []
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })))
|
||||
roots = []
|
||||
})
|
||||
|
||||
async function transcriptFile(content: string): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-transcript-file-order-'))
|
||||
roots.push(root)
|
||||
const filePath = join(root, 'session.jsonl')
|
||||
await writeFile(filePath, content)
|
||||
return filePath
|
||||
}
|
||||
|
||||
function expectPromptAfterToolRun(messages: NativeChatMessage[]): void {
|
||||
const rows = projectNativeChatTranscriptMessages(messages)
|
||||
expect(rows.map((row) => row.id)).toEqual(['u-1', 'a-1', 'q-1', 'a-2'])
|
||||
expect(rows[1]?.blocks.map((block) => block.type)).toEqual(['tool-call', 'tool-result'])
|
||||
}
|
||||
|
||||
describe('transcript rows keep file order', () => {
|
||||
it.each([
|
||||
['full read', (filePath: string) => readNativeChatTranscript('claude', 's', { filePath })],
|
||||
[
|
||||
'modern full read',
|
||||
(filePath: string) => readNativeChatTranscript('claude', 's', { filePath, sourceOrder: true })
|
||||
],
|
||||
[
|
||||
'modern tail read',
|
||||
(filePath: string) =>
|
||||
readNativeChatTranscriptTail({
|
||||
agent: 'claude',
|
||||
sessionId: 's',
|
||||
filePath,
|
||||
limit: 40,
|
||||
sourceOrder: true
|
||||
})
|
||||
],
|
||||
[
|
||||
'tail read',
|
||||
(filePath: string) =>
|
||||
readNativeChatTranscriptTail({ agent: 'claude', sessionId: 's', filePath, limit: 40 })
|
||||
]
|
||||
])(
|
||||
'places a mid-turn prompt after the tool result it was read with (%s)',
|
||||
async (_name, read) => {
|
||||
const filePath = await transcriptFile(lines(PROMPT, TOOL_CALL, TOOL_RESULT, QUEUED, REPLY))
|
||||
const result = await read(filePath)
|
||||
if (!('messages' in result)) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
expectPromptAfterToolRun(result.messages)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([false, true])(
|
||||
'places a mid-turn prompt appended after the initial snapshot (modern=%s)',
|
||||
async (sourceOrder) => {
|
||||
const filePath = await transcriptFile(lines(PROMPT, TOOL_CALL, TOOL_RESULT))
|
||||
const received: NativeChatMessage[] = []
|
||||
const subscription = await subscribeNativeChatTranscript({
|
||||
agent: 'claude',
|
||||
sessionId: 's',
|
||||
filePath,
|
||||
initialLimit: 40,
|
||||
sourceOrder,
|
||||
onInitialSnapshot: (messages) => received.push(...messages),
|
||||
onAppend: (messages) => received.push(...messages),
|
||||
debounceMs: 5,
|
||||
reconciliationIntervalMs: 20
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => received.length).toBe(3)
|
||||
await appendFile(filePath, lines(QUEUED, REPLY))
|
||||
await expect.poll(() => received.length).toBe(5)
|
||||
expectPromptAfterToolRun(received)
|
||||
} finally {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('canonical source positions', () => {
|
||||
const record = (id: string, timestamp: string | null, text = id, role = 'assistant') => ({
|
||||
type: role,
|
||||
uuid: id,
|
||||
timestamp,
|
||||
message: { role, content: text }
|
||||
})
|
||||
const read = async (filePath: string, limit: number, beforeOffset?: number) => {
|
||||
const result = await readNativeChatTranscriptTail({
|
||||
agent: 'claude',
|
||||
sessionId: 's',
|
||||
filePath,
|
||||
limit,
|
||||
beforeOffset,
|
||||
sourceOrder: true
|
||||
})
|
||||
if (!('messages' in result)) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
it('retains reader-produced tied image pages through prepend, append and reconnect into bubbles', async () => {
|
||||
const { applyMobileNativeChatStreamFrame } =
|
||||
await import('../../mobile/src/session/mobile-native-chat-stream-frame')
|
||||
const { foldMobileNativeChatMessages } =
|
||||
await import('../../mobile/src/session/mobile-native-chat-render-data')
|
||||
const filePath = await transcriptFile(
|
||||
lines(
|
||||
...[
|
||||
['z-source', '[Image: source: /tmp/first.png]'],
|
||||
['z-prompt', 'first [Image #1]'],
|
||||
['a-source', '[Image: source: /tmp/second.png]'],
|
||||
['a-prompt', 'second [Image #1]']
|
||||
].map(([id, text]) => record(id, '2026-06-01T10:00:00Z', text, 'user'))
|
||||
)
|
||||
)
|
||||
const tail = await read(filePath, 1)
|
||||
const older = await read(filePath, 3, tail.beforeOffset)
|
||||
const merger = createNativeChatMerger()
|
||||
const frame = (
|
||||
type: 'snapshot' | 'appended',
|
||||
messages: NativeChatMessage[],
|
||||
replaceSnapshot = false
|
||||
) =>
|
||||
applyMobileNativeChatStreamFrame({
|
||||
merger,
|
||||
frame: { type, messages, hasMore: true },
|
||||
limit: 100,
|
||||
replaceSnapshot
|
||||
})
|
||||
frame('snapshot', tail.messages, true)
|
||||
replaceList(merger, [...older.messages, ...merger.list])
|
||||
frame('appended', tail.messages)
|
||||
frame('snapshot', tail.messages)
|
||||
const bubbles = foldMobileNativeChatMessages(merger.list, 'claude')
|
||||
expect(bubbles.map((row) => row.id)).toEqual(['z-prompt', 'a-prompt'])
|
||||
expect(
|
||||
bubbles.map((row) =>
|
||||
row.blocks.filter((block) => block.type === 'image-ref').map((block) => block.path)
|
||||
)
|
||||
).toEqual([['/tmp/first.png'], ['/tmp/second.png']])
|
||||
})
|
||||
|
||||
it.each(['tied', 'backward', 'null'] as const)(
|
||||
'keeps absolute UTF8/CRLF positions across full, older and partial append reads (%s)',
|
||||
async (clock) => {
|
||||
const timestamps =
|
||||
clock === 'null'
|
||||
? [null, null, null]
|
||||
: clock === 'tied'
|
||||
? [PROMPT.timestamp, PROMPT.timestamp, PROMPT.timestamp]
|
||||
: [TOOL_RESULT.timestamp, QUEUED.timestamp, PROMPT.timestamp]
|
||||
const records = ['z-思考', 'm-emoji', 'a-answer'].map((id, i) =>
|
||||
record(id, timestamps[i], `${id} 🐳`)
|
||||
)
|
||||
const encoded = records.map((item) => `${JSON.stringify(item)}\r\n`)
|
||||
const filePath = await transcriptFile(encoded.join(''))
|
||||
const full = await readNativeChatTranscript('claude', 's', { filePath, sourceOrder: true })
|
||||
if (!('messages' in full)) {
|
||||
throw new Error(full.error)
|
||||
}
|
||||
expect(full.messages.map((row) => row.transcriptOrder)).toEqual([
|
||||
0,
|
||||
Buffer.byteLength(encoded[0]),
|
||||
Buffer.byteLength(encoded[0] + encoded[1])
|
||||
])
|
||||
expect(full.messages.map((row) => row.timestamp)).toEqual(
|
||||
timestamps.map((time) => (time === null ? null : Date.parse(time)))
|
||||
)
|
||||
const tail = await read(filePath, 1)
|
||||
const older = await read(filePath, 2, tail.beforeOffset)
|
||||
expect([...older.messages, ...tail.messages]).toEqual(full.messages)
|
||||
const { createIncrementalTranscriptState, readIncrementalTranscriptMessages } =
|
||||
await import('../../src/main/native-chat/transcript-incremental-reader')
|
||||
const { decodeClaudeTranscriptLine } =
|
||||
await import('../../src/main/native-chat/transcript-line-decoders')
|
||||
const state = createIncrementalTranscriptState()
|
||||
state.offset = state.pendingStart = Buffer.byteLength(encoded[0])
|
||||
await writeFile(filePath, encoded[0] + encoded[1].slice(0, -2))
|
||||
expect(
|
||||
await readIncrementalTranscriptMessages(
|
||||
filePath,
|
||||
state,
|
||||
decodeClaudeTranscriptLine,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
).toEqual([])
|
||||
await appendFile(filePath, `\r\n${encoded[2]}`)
|
||||
const appended = await readIncrementalTranscriptMessages(
|
||||
filePath,
|
||||
state,
|
||||
decodeClaudeTranscriptLine,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
expect(appended).toEqual(full.messages.slice(1))
|
||||
expect(projectNativeChatTranscriptMessages(full.messages).map((row) => row.id)).toEqual([
|
||||
'z-思考',
|
||||
'm-emoji',
|
||||
'a-answer'
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
it.each([false, true])(
|
||||
'rejoins image units across page and append boundaries, source-first=%s',
|
||||
async (sourceFirst) => {
|
||||
const { createIncrementalAssembler, reset, applyAppends } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-incremental-assembler')
|
||||
const { prepareNativeChatLiveMessages } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-live-message-preparation')
|
||||
const { commandMarkersAsMessages } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-command-marker')
|
||||
const { createNativeChatMessageListProjection } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-message-list-projection')
|
||||
const pair = (id: string, path: string) => {
|
||||
const prompt = record(id, PROMPT.timestamp, `${id} [Image #1]`, 'user')
|
||||
const images = [1, 2].map((n) =>
|
||||
record(
|
||||
`${id}-image-${n}`,
|
||||
PROMPT.timestamp,
|
||||
`[Image: source: /tmp/${path}-${n}.png]`,
|
||||
'user'
|
||||
)
|
||||
)
|
||||
return sourceFirst ? [...images, prompt] : [prompt, ...images]
|
||||
}
|
||||
const records = [...pair('z-first', 'first'), ...pair('a-second', 'second')]
|
||||
const filePath = await transcriptFile(lines(...records))
|
||||
const page = await read(filePath, 2)
|
||||
const older = await read(filePath, 4, page.beforeOffset)
|
||||
const assembler = createIncrementalAssembler()
|
||||
reset(assembler, older.messages)
|
||||
for (const message of page.messages) {
|
||||
applyAppends(assembler, [message])
|
||||
}
|
||||
const marker = commandMarkersAsMessages([
|
||||
{ id: 'between', command: '/compact', sentAt: Date.parse(PROMPT.timestamp) }
|
||||
])
|
||||
const normalized = prepareNativeChatLiveMessages([...assembler.messages, ...marker], 'claude')
|
||||
const bubbles = createNativeChatMessageListProjection()(normalized).conversation.filter(
|
||||
(row) => row.role === 'user'
|
||||
)
|
||||
expect(bubbles.map((row) => row.id)).toEqual(['z-first', 'a-second'])
|
||||
expect(
|
||||
bubbles.map((row) =>
|
||||
row.blocks.filter((block) => block.type === 'image-ref').map((block) => block.path)
|
||||
)
|
||||
).toEqual([
|
||||
['/tmp/first-1.png', '/tmp/first-2.png'],
|
||||
['/tmp/second-1.png', '/tmp/second-2.png']
|
||||
])
|
||||
expect(assembler.byId.size).toBe(6)
|
||||
expect(bubbles.map((row) => row.transcriptOrder)).toEqual([
|
||||
older.messages.find((row) => row.id === 'z-first')?.transcriptOrder,
|
||||
[...older.messages, ...page.messages].find((row) => row.id === 'a-second')?.transcriptOrder
|
||||
])
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps factual clocks for status, clear boundaries, mixed sources and optimistic tiers', async () => {
|
||||
const { mergeNativeChatLiveSession } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-live-status')
|
||||
const { applyCommandMarkerBoundaries, commandMarkersAsMessages } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-command-marker')
|
||||
const { assembleNativeChatSession } =
|
||||
await import('../../src/renderer/src/components/native-chat/native-chat-session-assembler')
|
||||
const { NATIVE_CHAT_STREAMING_ID } =
|
||||
await import('../../src/shared/native-chat/native-chat-streaming')
|
||||
const filePath = await transcriptFile(
|
||||
lines(...Array.from({ length: 20 }, (_, i) => record(`old-${i}`, PROMPT.timestamp)))
|
||||
)
|
||||
const rows = (await read(filePath, 30)).messages
|
||||
const time = Date.parse(PROMPT.timestamp)
|
||||
expect(
|
||||
mergeNativeChatLiveSession({
|
||||
messages: rows,
|
||||
sessionId: 's',
|
||||
agent: 'grok',
|
||||
hookState: 'working',
|
||||
stateStartedAt: time + 10
|
||||
}).status
|
||||
).toBe('working')
|
||||
expect(
|
||||
applyCommandMarkerBoundaries(rows, [{ id: 'clear', command: '/clear', sentAt: time + 10 }])
|
||||
).toEqual([])
|
||||
const later = { ...rows[1], id: 'later', timestamp: time + 30 }
|
||||
const marker = commandMarkersAsMessages([
|
||||
{ id: 'compact', command: '/compact', sentAt: time + 20 }
|
||||
])[0]
|
||||
const echo = (
|
||||
id: string,
|
||||
timestamp: number | null,
|
||||
source: NativeChatMessage['source'] = 'scrape'
|
||||
): NativeChatMessage => ({
|
||||
id,
|
||||
timestamp,
|
||||
source,
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: id }]
|
||||
})
|
||||
const transcript = [rows[0], later]
|
||||
const result = assembleNativeChatSession({
|
||||
agent: 'claude',
|
||||
sessionId: 's',
|
||||
sources: {
|
||||
transcript,
|
||||
hook: [echo('hook', time + 10, 'hook')],
|
||||
scrape: [
|
||||
marker,
|
||||
echo('null', null),
|
||||
echo('after', time + 40),
|
||||
echo(NATIVE_CHAT_STREAMING_ID, null),
|
||||
echo('pending:1', null),
|
||||
{ ...echo('queued', null), queued: true },
|
||||
echo('launch-pending:1', null)
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(result.messages.map((row) => row.id)).toEqual([
|
||||
'null',
|
||||
'old-0',
|
||||
'hook',
|
||||
marker.id,
|
||||
'later',
|
||||
'after',
|
||||
NATIVE_CHAT_STREAMING_ID,
|
||||
'launch-pending:1',
|
||||
'pending:1',
|
||||
'queued'
|
||||
])
|
||||
// Contradictory clocks preserve the canonical subsequence, with one explicit supplemental placement.
|
||||
expect(
|
||||
assembleNativeChatSession({
|
||||
agent: 'claude',
|
||||
sessionId: 's',
|
||||
sources: {
|
||||
transcript: [
|
||||
{ ...rows[0], timestamp: 100 },
|
||||
{ ...later, timestamp: 0 }
|
||||
],
|
||||
hook: [echo('middle', 50, 'hook')]
|
||||
}
|
||||
}).messages.map((row) => row.id)
|
||||
).toEqual(['middle', 'old-0', 'later'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterAll, afterEach, beforeAll, expect, it, vi } from 'vitest'
|
||||
|
||||
// Why: these fixtures have neither inline visual owners nor sender navigation.
|
||||
const unusedChatLeaves = vi.hoisted(() => ({
|
||||
inlineVisual: vi.fn(() => {
|
||||
throw new Error('Unexpected inline visual in transcript fixture')
|
||||
}),
|
||||
openSender: vi.fn(() => {
|
||||
throw new Error('Unexpected sender navigation in transcript fixture')
|
||||
})
|
||||
}))
|
||||
vi.mock('../../src/renderer/src/components/native-chat/NativeChatInlineVisual', () => ({
|
||||
NativeChatInlineVisual: unusedChatLeaves.inlineVisual
|
||||
}))
|
||||
vi.mock('@/lib/open-agent-message-sender', () => ({
|
||||
openAgentMessageSender: unusedChatLeaves.openSender
|
||||
}))
|
||||
afterEach(() => {
|
||||
expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled()
|
||||
expect(unusedChatLeaves.openSender).not.toHaveBeenCalled()
|
||||
})
|
||||
import { createElement } from 'react'
|
||||
import { NativeChatMessageList } from '../../src/renderer/src/components/native-chat/NativeChatMessageList'
|
||||
import { installNativeChatMessageListTestViewport } from '../../src/renderer/src/components/native-chat/native-chat-message-list-test-viewport'
|
||||
|
||||
let restoreViewport = (): void => {}
|
||||
beforeAll(() => {
|
||||
restoreViewport = installNativeChatMessageListTestViewport()
|
||||
})
|
||||
afterAll(() => restoreViewport())
|
||||
afterEach(cleanup)
|
||||
|
||||
it('renders terminal file reasoning before its equal-clock answer without journal authority', async () => {
|
||||
const { Readable } = await import('node:stream')
|
||||
const { decodeTranscriptStream } =
|
||||
await import('../../src/main/native-chat/transcript-stream-lines')
|
||||
const { decodeClaudeTranscriptLine } =
|
||||
await import('../../src/main/native-chat/transcript-line-decoders')
|
||||
const records = [
|
||||
{ type: 'user', uuid: 'prompt', message: { role: 'user', content: 'Explain the result' } },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'z-thinking',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'thinking', thinking: 'File reasoning before final answer' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'a-answer',
|
||||
message: { role: 'assistant', content: 'The final file answer' }
|
||||
}
|
||||
]
|
||||
.map((row) => `${JSON.stringify({ ...row, timestamp: '2026-06-01T10:00:00Z' })}\n`)
|
||||
.join('')
|
||||
const { messages } = await decodeTranscriptStream(
|
||||
Readable.from([records]),
|
||||
'terminal.jsonl',
|
||||
0,
|
||||
decodeClaudeTranscriptLine,
|
||||
true,
|
||||
Infinity,
|
||||
true
|
||||
)
|
||||
render(
|
||||
createElement(NativeChatMessageList, {
|
||||
session: {
|
||||
messages,
|
||||
status: 'ready',
|
||||
sessionId: 'session-1',
|
||||
agent: 'claude',
|
||||
hasMore: false,
|
||||
loadingEarlier: false,
|
||||
olderHistoryGeneration: 0,
|
||||
loadEarlier: vi.fn(),
|
||||
readPhase: 'ready'
|
||||
},
|
||||
isWorking: false,
|
||||
expandSignal: true
|
||||
})
|
||||
)
|
||||
const disclosure = screen.getByRole('button', { name: /^Reasoning/ })
|
||||
if (disclosure.getAttribute('aria-expanded') === 'false') {
|
||||
fireEvent.click(disclosure)
|
||||
}
|
||||
const thought = screen.getByText('File reasoning before final answer')
|
||||
const answer = screen.getByText('The final file answer')
|
||||
expect(thought.compareDocumentPosition(answer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
Reference in New Issue
Block a user