diff --git a/config/vitest.config.ts b/config/vitest.config.ts index 34065de49bf..5a83d1df802 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -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', diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json index ddd2bfb0c6a..a441e6284f3 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json @@ -4,157 +4,7 @@ "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" - }, - "error": { - "$rpc": "null" - }, - "hasMore": false, - "loadingEarlier": false, - "messageIds": ["m-1", "m-2", "m-3", "m-4"], - "status": "ready", - "transcriptLoading": false - }, - "197fa03857dd": { - "crash": { - "$rpc": "null" - }, - "error": { - "$rpc": "null" - }, - "hasMore": true, - "loadingEarlier": false, - "messageIds": ["m-3", "m-4", "m-5"], - "status": "ready", - "transcriptLoading": false - }, - "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\"}}" - }, - "65ff352ca953": { - "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": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "", - "isRpcDeliveryUnknown": true - } - } - }, - "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\"}}" - }, - "75842f212e52": { - "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": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true - } - } - }, - "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\"}}" - }, - "81eaceac1130": { + "04f0746da2ec": { "name": "nativeChat.readSession#1", "ordinal": 2, "args": [ @@ -167,6 +17,9 @@ "value": { "agent": "claude", "beforeOffset": 1200, + "capabilities": { + "transcriptSourceOrder": 1 + }, "limit": 60, "sessionId": "session-1", "transcriptPath": "/work/feature/.claude/session-1.jsonl" @@ -194,7 +47,20 @@ } } }, - "84f6bcad6466": { + "0943a7b4b434": { + "crash": { + "$rpc": "null" + }, + "error": { + "$rpc": "null" + }, + "hasMore": false, + "loadingEarlier": false, + "messageIds": ["m-1", "m-2", "m-3", "m-4"], + "status": "ready", + "transcriptLoading": false + }, + "17ad4374230c": { "name": "nativeChat.readSession#1", "ordinal": 2, "args": [ @@ -207,6 +73,158 @@ "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": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "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" + }, + "error": { + "$rpc": "null" + }, + "hasMore": true, + "loadingEarlier": false, + "messageIds": ["m-3", "m-4", "m-5"], + "status": "ready", + "transcriptLoading": false + }, + "1d80f6e1757c": { + "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": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "20e9fdaca4bb": { + "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": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "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\"}}" + }, + "468cdefc1b9a": { + "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" @@ -236,20 +254,7 @@ } } }, - "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": { + "4c78f366c576": { "name": "nativeChat.readSession#1", "ordinal": 2, "args": [ @@ -262,6 +267,51 @@ "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": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "512a0c61c2d8": { + "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" @@ -315,217 +365,12 @@ } } }, - "a3e69f56e5e2": { - "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": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "$rpc": "null" - } - } - } + "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\"}}" }, - "ab5d827d2b71": { - "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": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "error": "refused" - } - } - } - }, - "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, - "settledAt": 0, - "value": { - "$rpc": "undefined" - } - }, - "ee33a7e2ed08": { - "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": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "refused", - "message": "" - }, - "id": "frame-2", - "ok": false - } - } - }, - "ee46c03cf1b8": { - "crash": { - "$rpc": "null" - }, - "error": { - "$rpc": "null" - }, - "hasMore": false, - "loadingEarlier": false, - "messageIds": [], - "status": "loading", - "transcriptLoading": true - }, - "fa953f37a758": { - "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": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "error": "inner refused", - "ok": false - } - } - } - }, - "fcea12946cf7": { + "61d8a95fd63a": { "name": "nativeChat.readSession#1", "ordinal": 2, "args": [ @@ -538,6 +383,9 @@ "value": { "agent": "claude", "beforeOffset": 1200, + "capabilities": { + "transcriptSourceOrder": 1 + }, "limit": 60, "sessionId": "session-1", "transcriptPath": "/work/feature/.claude/session-1.jsonl" @@ -565,7 +413,7 @@ } } }, - "fd440192b538": { + "62edf08185e6": { "name": "nativeChat.readSession#1", "ordinal": 2, "args": [ @@ -578,6 +426,9 @@ "value": { "agent": "claude", "beforeOffset": 1200, + "capabilities": { + "transcriptSourceOrder": 1 + }, "limit": 60, "sessionId": "session-1", "transcriptPath": "/work/feature/.claude/session-1.jsonl" @@ -601,6 +452,191 @@ "isRpcDeliveryUnknown": true } } + }, + "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\"}}" + }, + "8dace0dee96b": { + "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": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "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", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ee46c03cf1b8": { + "crash": { + "$rpc": "null" + }, + "error": { + "$rpc": "null" + }, + "hasMore": false, + "loadingEarlier": false, + "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 + } + }, + "f8f00339ea6f": { + "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": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } } }, "recording": { @@ -610,7 +646,7 @@ "id": "native-chat-page-earlier.prelude:subscribed", "observation": { "sender": [], - "payloads": ["456781b97acc"], + "payloads": ["7cbe5e7d1f66"], "settlements": { "mount": "eb79a9b3682a" }, @@ -622,7 +658,7 @@ "id": "native-chat-page-earlier.prelude:snapshot", "observation": { "sender": [], - "payloads": ["456781b97acc"], + "payloads": ["7cbe5e7d1f66"], "settlements": { "mount": "eb79a9b3682a" }, @@ -633,8 +669,8 @@ { "id": "native-chat-page-earlier.prelude:paging", "observation": { - "sender": ["e88c218171fa"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["f1dfb69b0f6f"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -646,8 +682,8 @@ { "id": "native-chat-page-earlier.normal:paged", "observation": { - "sender": ["911b3f485409"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["512a0c61c2d8"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -659,8 +695,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", @@ -673,8 +709,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", @@ -687,11 +723,11 @@ { "id": "native-chat-page-earlier.normal:unmounted", "observation": { - "sender": ["911b3f485409"], + "sender": ["512a0c61c2d8"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -708,8 +744,8 @@ { "id": "native-chat-page-earlier.result-absent:paged", "observation": { - "sender": ["75842f212e52"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["8dace0dee96b"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -721,8 +757,8 @@ { "id": "native-chat-page-earlier.result-absent:re-subscribed", "observation": { - "sender": ["75842f212e52"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["8dace0dee96b"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -735,8 +771,8 @@ { "id": "native-chat-page-earlier.result-absent:replayed", "observation": { - "sender": ["75842f212e52"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["8dace0dee96b"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -749,11 +785,11 @@ { "id": "native-chat-page-earlier.result-absent:unmounted", "observation": { - "sender": ["75842f212e52"], + "sender": ["8dace0dee96b"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -770,8 +806,8 @@ { "id": "native-chat-page-earlier.result-null:paged", "observation": { - "sender": ["a3e69f56e5e2"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["f8f00339ea6f"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -783,8 +819,8 @@ { "id": "native-chat-page-earlier.result-null:re-subscribed", "observation": { - "sender": ["a3e69f56e5e2"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["f8f00339ea6f"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -797,8 +833,8 @@ { "id": "native-chat-page-earlier.result-null:replayed", "observation": { - "sender": ["a3e69f56e5e2"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["f8f00339ea6f"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -811,11 +847,11 @@ { "id": "native-chat-page-earlier.result-null:unmounted", "observation": { - "sender": ["a3e69f56e5e2"], + "sender": ["f8f00339ea6f"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -832,8 +868,8 @@ { "id": "native-chat-page-earlier.inner-ok-missing:paged", "observation": { - "sender": ["ab5d827d2b71"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["4c78f366c576"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -845,8 +881,8 @@ { "id": "native-chat-page-earlier.inner-ok-missing:re-subscribed", "observation": { - "sender": ["ab5d827d2b71"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["4c78f366c576"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -859,8 +895,8 @@ { "id": "native-chat-page-earlier.inner-ok-missing:replayed", "observation": { - "sender": ["ab5d827d2b71"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["4c78f366c576"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -873,11 +909,11 @@ { "id": "native-chat-page-earlier.inner-ok-missing:unmounted", "observation": { - "sender": ["ab5d827d2b71"], + "sender": ["4c78f366c576"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -894,8 +930,8 @@ { "id": "native-chat-page-earlier.inner-false-string-error:paged", "observation": { - "sender": ["fa953f37a758"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["20e9fdaca4bb"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -907,8 +943,8 @@ { "id": "native-chat-page-earlier.inner-false-string-error:re-subscribed", "observation": { - "sender": ["fa953f37a758"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["20e9fdaca4bb"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -921,8 +957,8 @@ { "id": "native-chat-page-earlier.inner-false-string-error:replayed", "observation": { - "sender": ["fa953f37a758"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["20e9fdaca4bb"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -935,11 +971,11 @@ { "id": "native-chat-page-earlier.inner-false-string-error:unmounted", "observation": { - "sender": ["fa953f37a758"], + "sender": ["20e9fdaca4bb"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -956,8 +992,8 @@ { "id": "native-chat-page-earlier.inner-false-object-error:paged", "observation": { - "sender": ["84f6bcad6466"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["468cdefc1b9a"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -969,8 +1005,8 @@ { "id": "native-chat-page-earlier.inner-false-object-error:re-subscribed", "observation": { - "sender": ["84f6bcad6466"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["468cdefc1b9a"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -983,8 +1019,8 @@ { "id": "native-chat-page-earlier.inner-false-object-error:replayed", "observation": { - "sender": ["84f6bcad6466"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["468cdefc1b9a"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -997,11 +1033,11 @@ { "id": "native-chat-page-earlier.inner-false-object-error:unmounted", "observation": { - "sender": ["84f6bcad6466"], + "sender": ["468cdefc1b9a"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -1018,8 +1054,8 @@ { "id": "native-chat-page-earlier.outer-refused:paged", "observation": { - "sender": ["81eaceac1130"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["04f0746da2ec"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -1031,8 +1067,8 @@ { "id": "native-chat-page-earlier.outer-refused:re-subscribed", "observation": { - "sender": ["81eaceac1130"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["04f0746da2ec"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1045,8 +1081,8 @@ { "id": "native-chat-page-earlier.outer-refused:replayed", "observation": { - "sender": ["81eaceac1130"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["04f0746da2ec"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1059,11 +1095,11 @@ { "id": "native-chat-page-earlier.outer-refused:unmounted", "observation": { - "sender": ["81eaceac1130"], + "sender": ["04f0746da2ec"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -1080,8 +1116,8 @@ { "id": "native-chat-page-earlier.outer-refused-no-message:paged", "observation": { - "sender": ["ee33a7e2ed08"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["1d80f6e1757c"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -1093,8 +1129,8 @@ { "id": "native-chat-page-earlier.outer-refused-no-message:re-subscribed", "observation": { - "sender": ["ee33a7e2ed08"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["1d80f6e1757c"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1107,8 +1143,8 @@ { "id": "native-chat-page-earlier.outer-refused-no-message:replayed", "observation": { - "sender": ["ee33a7e2ed08"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["1d80f6e1757c"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1121,11 +1157,11 @@ { "id": "native-chat-page-earlier.outer-refused-no-message:unmounted", "observation": { - "sender": ["ee33a7e2ed08"], + "sender": ["1d80f6e1757c"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -1142,8 +1178,8 @@ { "id": "native-chat-page-earlier.method-not-found:paged", "observation": { - "sender": ["fcea12946cf7"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["61d8a95fd63a"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -1155,8 +1191,8 @@ { "id": "native-chat-page-earlier.method-not-found:re-subscribed", "observation": { - "sender": ["fcea12946cf7"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["61d8a95fd63a"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1169,8 +1205,8 @@ { "id": "native-chat-page-earlier.method-not-found:replayed", "observation": { - "sender": ["fcea12946cf7"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["61d8a95fd63a"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1183,11 +1219,11 @@ { "id": "native-chat-page-earlier.method-not-found:unmounted", "observation": { - "sender": ["fcea12946cf7"], + "sender": ["61d8a95fd63a"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -1204,8 +1240,8 @@ { "id": "native-chat-page-earlier.transport-rejection:paged", "observation": { - "sender": ["fd440192b538"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["62edf08185e6"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -1217,8 +1253,8 @@ { "id": "native-chat-page-earlier.transport-rejection:re-subscribed", "observation": { - "sender": ["fd440192b538"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["62edf08185e6"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1231,8 +1267,8 @@ { "id": "native-chat-page-earlier.transport-rejection:replayed", "observation": { - "sender": ["fd440192b538"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["62edf08185e6"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1245,11 +1281,11 @@ { "id": "native-chat-page-earlier.transport-rejection:unmounted", "observation": { - "sender": ["fd440192b538"], + "sender": ["62edf08185e6"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], @@ -1266,8 +1302,8 @@ { "id": "native-chat-page-earlier.transport-rejection-no-message:paged", "observation": { - "sender": ["65ff352ca953"], - "payloads": ["456781b97acc", "08ef53d83e8c"], + "sender": ["17ad4374230c"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a" @@ -1279,8 +1315,8 @@ { "id": "native-chat-page-earlier.transport-rejection-no-message:re-subscribed", "observation": { - "sender": ["65ff352ca953"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["17ad4374230c"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1293,8 +1329,8 @@ { "id": "native-chat-page-earlier.transport-rejection-no-message:replayed", "observation": { - "sender": ["65ff352ca953"], - "payloads": ["456781b97acc", "08ef53d83e8c", "663babae1f1d", "7eedfcf76fd3"], + "sender": ["17ad4374230c"], + "payloads": ["7cbe5e7d1f66", "18a95418eb6d", "6138b52f75d7", "7eedfcf76fd3"], "settlements": { "mount": "eb79a9b3682a", "page": "eb79a9b3682a", @@ -1307,11 +1343,11 @@ { "id": "native-chat-page-earlier.transport-rejection-no-message:unmounted", "observation": { - "sender": ["65ff352ca953"], + "sender": ["17ad4374230c"], "payloads": [ - "456781b97acc", - "08ef53d83e8c", - "663babae1f1d", + "7cbe5e7d1f66", + "18a95418eb6d", + "6138b52f75d7", "7eedfcf76fd3", "3e14b1ec5bde" ], diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json index 859409c0e68..31779334d49 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json @@ -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", diff --git a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json index 06f5faea844..a2d04c3325b 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json @@ -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", diff --git a/mobile/rpc-foundation/goldens/native-chat-page-earlier.json b/mobile/rpc-foundation/goldens/native-chat-page-earlier.json index a2e8ea89078..738a45b6274 100644 --- a/mobile/rpc-foundation/goldens/native-chat-page-earlier.json +++ b/mobile/rpc-foundation/goldens/native-chat-page-earlier.json @@ -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" ], diff --git a/mobile/rpc-foundation/pilot-scenarios.json b/mobile/rpc-foundation/pilot-scenarios.json index 4c6048b4ab6..68f22bef8db 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -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" }, diff --git a/mobile/src/session/mobile-native-chat-page-rejection.test.ts b/mobile/src/session/mobile-native-chat-page-rejection.test.ts index 1dfd10e936f..7a548f9d812 100644 --- a/mobile/src/session/mobile-native-chat-page-rejection.test.ts +++ b/mobile/src/session/mobile-native-chat-page-rejection.test.ts @@ -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') diff --git a/mobile/src/session/use-mobile-native-chat-session-tool-result-paging.test.tsx b/mobile/src/session/use-mobile-native-chat-session-tool-result-paging.test.tsx index c561592a031..01ef8c56fb6 100644 --- a/mobile/src/session/use-mobile-native-chat-session-tool-result-paging.test.tsx +++ b/mobile/src/session/use-mobile-native-chat-session-tool-result-paging.test.tsx @@ -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 }) diff --git a/mobile/src/session/use-mobile-native-chat-session.test.ts b/mobile/src/session/use-mobile-native-chat-session.test.ts index b5877c26aa5..9fb26f9bce4 100644 --- a/mobile/src/session/use-mobile-native-chat-session.test.ts +++ b/mobile/src/session/use-mobile-native-chat-session.test.ts @@ -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({ diff --git a/mobile/src/session/use-mobile-native-chat-session.ts b/mobile/src/session/use-mobile-native-chat-session.ts index 7a779703084..4765a24d110 100644 --- a/mobile/src/session/use-mobile-native-chat-session.ts +++ b/mobile/src/session/use-mobile-native-chat-session.ts @@ -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 } : {}) }, diff --git a/src/main/ipc/native-chat-subscribe-lifecycle.test.ts b/src/main/ipc/native-chat-subscribe-lifecycle.test.ts index 030067dd68d..b1c33f736df 100644 --- a/src/main/ipc/native-chat-subscribe-lifecycle.test.ts +++ b/src/main/ipc/native-chat-subscribe-lifecycle.test.ts @@ -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) diff --git a/src/main/ipc/native-chat.test.ts b/src/main/ipc/native-chat.test.ts index 9fea67f63b9..fb636ed8829 100644 --- a/src/main/ipc/native-chat.test.ts +++ b/src/main/ipc/native-chat.test.ts @@ -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 diff --git a/src/main/ipc/native-chat.ts b/src/main/ipc/native-chat.ts index 9f54fdab980..1429f634127 100644 --- a/src/main/ipc/native-chat.ts +++ b/src/main/ipc/native-chat.ts @@ -42,7 +42,8 @@ async function readSession(args: NativeChatReadSessionArgs): Promise { if (!canPublish()) { return diff --git a/src/main/native-chat/transcript-file-order.test.ts b/src/main/native-chat/transcript-file-order.test.ts deleted file mode 100644 index bbb6c57a4fa..00000000000 --- a/src/main/native-chat/transcript-file-order.test.ts +++ /dev/null @@ -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 { - 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() - } - }) -}) diff --git a/src/main/native-chat/transcript-file-order.ts b/src/main/native-chat/transcript-file-order.ts index 48ddefea3d1..adf55b41ee3 100644 --- a/src/main/native-chat/transcript-file-order.ts +++ b/src/main/native-chat/transcript-file-order.ts @@ -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) } } diff --git a/src/main/native-chat/transcript-incremental-reader.ts b/src/main/native-chat/transcript-incremental-reader.ts index 41e256f772c..1634b0b847f 100644 --- a/src/main/native-chat/transcript-incremental-reader.ts +++ b/src/main/native-chat/transcript-incremental-reader.ts @@ -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 { 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) { diff --git a/src/main/native-chat/transcript-reader.ts b/src/main/native-chat/transcript-reader.ts index f5c61709e97..b27c1f543ef 100644 --- a/src/main/native-chat/transcript-reader.ts +++ b/src/main/native-chat/transcript-reader.ts @@ -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 { 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 } diff --git a/src/main/native-chat/transcript-stream-lines.ts b/src/main/native-chat/transcript-stream-lines.ts index 4a6bb550fb2..83c4b485b6a 100644 --- a/src/main/native-chat/transcript-stream-lines.ts +++ b/src/main/native-chat/transcript-stream-lines.ts @@ -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) } diff --git a/src/main/native-chat/transcript-tail-reader.ts b/src/main/native-chat/transcript-tail-reader.ts index 75c91657696..0535049ae8d 100644 --- a/src/main/native-chat/transcript-tail-reader.ts +++ b/src/main/native-chat/transcript-tail-reader.ts @@ -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 { diff --git a/src/main/native-chat/transcript-watch-contract.ts b/src/main/native-chat/transcript-watch-contract.ts index 275c6a3f18e..4cd3a571a85 100644 --- a/src/main/native-chat/transcript-watch-contract.ts +++ b/src/main/native-chat/transcript-watch-contract.ts @@ -28,6 +28,7 @@ export type SubscribeNativeChatTranscriptArgs = ResolveSessionFileOptions & { lifecycle?: NativeChatTurnLifecycle ) => void initialLimit?: number + sourceOrder?: boolean filePath?: string debounceMs?: number resolvePollIntervalMs?: number diff --git a/src/main/native-chat/transcript-watch-engine.ts b/src/main/native-chat/transcript-watch-engine.ts index b9fb1b1fc30..67f830912ab 100644 --- a/src/main/native-chat/transcript-watch-engine.ts +++ b/src/main/native-chat/transcript-watch-engine.ts @@ -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 diff --git a/src/main/runtime/rpc/methods/native-chat-ssh-transcript.test.ts b/src/main/runtime/rpc/methods/native-chat-ssh-transcript.test.ts deleted file mode 100644 index 73f85d43c7f..00000000000 --- a/src/main/runtime/rpc/methods/native-chat-ssh-transcript.test.ts +++ /dev/null @@ -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[] = [] - -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 { - 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): 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']) - }) -}) diff --git a/src/main/runtime/rpc/methods/native-chat.ts b/src/main/runtime/rpc/methods/native-chat.ts index 168e63c556e..374518f0945 100644 --- a/src/main/runtime/rpc/methods/native-chat.ts +++ b/src/main/runtime/rpc/methods/native-chat.ts @@ -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 diff --git a/src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts b/src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts index 10555ce69f2..f80feb3864b 100644 --- a/src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts @@ -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]) + ) + } +}) diff --git a/src/renderer/src/components/native-chat/native-chat-incremental-assembler.ts b/src/renderer/src/components/native-chat/native-chat-incremental-assembler.ts index ce5702fe4c9..3965237305a 100644 --- a/src/renderer/src/components/native-chat/native-chat-incremental-assembler.ts +++ b/src/renderer/src/components/native-chat/native-chat-incremental-assembler.ts @@ -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 } } diff --git a/src/renderer/src/components/native-chat/native-chat-message-grouping.ts b/src/renderer/src/components/native-chat/native-chat-message-grouping.ts index 77d2cce9815..15ef64d9ba4 100644 --- a/src/renderer/src/components/native-chat/native-chat-message-grouping.ts +++ b/src/renderer/src/components/native-chat/native-chat-message-grouping.ts @@ -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) } diff --git a/src/renderer/src/components/native-chat/native-chat-message-list-projection.ts b/src/renderer/src/components/native-chat/native-chat-message-list-projection.ts index 7aafb8df312..ddf350dd3cc 100644 --- a/src/renderer/src/components/native-chat/native-chat-message-list-projection.ts +++ b/src/renderer/src/components/native-chat/native-chat-message-list-projection.ts @@ -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() 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) diff --git a/src/renderer/src/components/native-chat/native-chat-sent-prompt-history.ts b/src/renderer/src/components/native-chat/native-chat-sent-prompt-history.ts index f461cccf463..7063adbf43d 100644 --- a/src/renderer/src/components/native-chat/native-chat-sent-prompt-history.ts +++ b/src/renderer/src/components/native-chat/native-chat-sent-prompt-history.ts @@ -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 diff --git a/src/renderer/src/components/native-chat/native-chat-session-assembler.ts b/src/renderer/src/components/native-chat/native-chat-session-assembler.ts index ab0b8071f2c..3a0a8e6fcaf 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-assembler.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-assembler.ts @@ -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(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 } diff --git a/src/renderer/src/components/native-chat/native-chat-session-transport.test.ts b/src/renderer/src/components/native-chat/native-chat-session-transport.test.ts index 5612f291f7b..fa37984706f 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-transport.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-transport.test.ts @@ -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) ) diff --git a/src/renderer/src/components/native-chat/native-chat-session-transport.ts b/src/renderer/src/components/native-chat/native-chat-session-transport.ts index a75daf78427..ca99a292eb0 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-transport.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-transport.ts @@ -45,7 +45,7 @@ function createRuntimeNativeChatTransport(environmentId: string): NativeChatSess const result = await callRuntimeRpc( 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 }, diff --git a/src/renderer/src/web/preload-api/web-native-chat-api.ts b/src/renderer/src/web/preload-api/web-native-chat-api.ts index d9f15a430d2..9770e9f125c 100644 --- a/src/renderer/src/web/preload-api/web-native-chat-api.ts +++ b/src/renderer/src/web/preload-api/web-native-chat-api.ts @@ -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) => { diff --git a/src/renderer/src/web/web-preload-api-agent-providers.test.ts b/src/renderer/src/web/web-preload-api-agent-providers.test.ts index 4280cbb85b0..22c7db0c26e 100644 --- a/src/renderer/src/web/web-preload-api-agent-providers.test.ts +++ b/src/renderer/src/web/web-preload-api-agent-providers.test.ts @@ -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> { + call(method: string, params: unknown): Promise> { + 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 }) diff --git a/src/shared/native-chat-transcript-projection.test.ts b/src/shared/native-chat-transcript-projection.test.ts index df6ff80d7a4..674359ef8cb 100644 --- a/src/shared/native-chat-transcript-projection.test.ts +++ b/src/shared/native-chat-transcript-projection.test.ts @@ -1,3 +1,4 @@ +import './native-chat-reasoning-order.spec' import { describe, expect, it } from 'vitest' import type { AgentJournalItemBody, diff --git a/src/shared/native-chat-transcript-projection.ts b/src/shared/native-chat-transcript-projection.ts index cf4eb0f3c6a..ab2e4b369be 100644 --- a/src/shared/native-chat-transcript-projection.ts +++ b/src/shared/native-chat-transcript-projection.ts @@ -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( + 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() for (const message of sorted) { diff --git a/src/shared/native-chat-types.ts b/src/shared/native-chat-types.ts index f6b29555def..2d3fc247bef 100644 --- a/src/shared/native-chat-types.ts +++ b/src/shared/native-chat-types.ts @@ -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 diff --git a/src/shared/rpc-contract/native-chat-params.ts b/src/shared/rpc-contract/native-chat-params.ts index 452426807e8..1331ee6c315 100644 --- a/src/shared/rpc-contract/native-chat-params.ts +++ b/src/shared/rpc-contract/native-chat-params.ts @@ -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() }) diff --git a/tests/e2e/native-chat-live-history.unit.test.ts b/tests/e2e/native-chat-live-history.unit.test.ts new file mode 100644 index 00000000000..013eacc759c --- /dev/null +++ b/tests/e2e/native-chat-live-history.unit.test.ts @@ -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 { + await act(async () => { + await Promise.resolve() + await Promise.resolve() + }) + } + + async function render(props: UseNativeChatLiveSessionArgs): Promise { + 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() + const appendCommitted = Promise.withResolvers() + 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 }) + } + }) +}) diff --git a/tests/e2e/native-chat-ssh-transcript.unit.test.ts b/tests/e2e/native-chat-ssh-transcript.unit.test.ts new file mode 100644 index 00000000000..2d645233e6d --- /dev/null +++ b/tests/e2e/native-chat-ssh-transcript.unit.test.ts @@ -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[] = [] + +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 { + 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): 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']) + }) +}) diff --git a/tests/e2e/native-chat-transcript-file-order.unit.test.ts b/tests/e2e/native-chat-transcript-file-order.unit.test.ts new file mode 100644 index 00000000000..5bb4786f57d --- /dev/null +++ b/tests/e2e/native-chat-transcript-file-order.unit.test.ts @@ -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 { + 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']) + }) +}) diff --git a/tests/e2e/native-chat-transcript-reasoning.unit.test.ts b/tests/e2e/native-chat-transcript-reasoning.unit.test.ts new file mode 100644 index 00000000000..bb542d24705 --- /dev/null +++ b/tests/e2e/native-chat-transcript-reasoning.unit.test.ts @@ -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() +})