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:
Brennan Benson
2026-10-26 18:45:21 -07:00
committed by GitHub
parent 5ad2d09ee4
commit 028f609b5a
41 changed files with 2217 additions and 1251 deletions
+25
View File
@@ -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',
@@ -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",
@@ -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"
],
+7 -2
View File
@@ -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)
+4
View File
@@ -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
+3 -1
View File
@@ -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) {
+24 -6
View File
@@ -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)
}
+11 -3
View File
@@ -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'])
})
})
+3 -1
View 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,
+40 -10
View File
@@ -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) {
+2 -2
View File
@@ -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()
})