@myagentroam/node 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/native-jsonl-reader.d.ts +21 -0
- package/dist/native-jsonl-reader.js +89 -0
- package/dist/native-session-history.d.ts +17 -1
- package/dist/native-session-history.js +61 -23
- package/dist/runner/codex-runner.js +4 -3
- package/dist/service/conversation-history-service.d.ts +1 -1
- package/dist/service/conversation-history-service.js +44 -6
- package/dist/util/runner-native-session-parsers.js +68 -11
- package/package.json +2 -2
|
@@ -3,6 +3,27 @@ export interface JsonlWindow {
|
|
|
3
3
|
readonly truncated: boolean;
|
|
4
4
|
readonly size: number;
|
|
5
5
|
}
|
|
6
|
+
export interface JsonlBackwardWindow extends JsonlWindow {
|
|
7
|
+
readonly pageContent: string;
|
|
8
|
+
readonly start: number;
|
|
9
|
+
readonly end: number;
|
|
10
|
+
readonly snapshotEnd: number;
|
|
11
|
+
readonly fileAnchor: string;
|
|
12
|
+
readonly anchorBytes: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function readJsonlFileAnchor(path: string, anchorBytes: number): Promise<string | undefined>;
|
|
15
|
+
/** Reads one newline-aligned window backwards from an opaque byte boundary. */
|
|
16
|
+
export declare function readJsonlBackwardWindow(path: string, options: {
|
|
17
|
+
readonly headBytes: number;
|
|
18
|
+
readonly targetLines: number;
|
|
19
|
+
readonly maxBytes: number;
|
|
20
|
+
readonly page?: {
|
|
21
|
+
readonly snapshotEnd: number;
|
|
22
|
+
readonly nextEnd: number;
|
|
23
|
+
readonly fileAnchor: string;
|
|
24
|
+
readonly anchorBytes: number;
|
|
25
|
+
};
|
|
26
|
+
}): Promise<JsonlBackwardWindow | undefined>;
|
|
6
27
|
/** Reads bounded head/tail windows without ever loading an unbounded transcript. */
|
|
7
28
|
export declare function readJsonlWindow(path: string, options: {
|
|
8
29
|
readonly headBytes?: number;
|
|
@@ -1,5 +1,94 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { open, readdir } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
4
|
+
export async function readJsonlFileAnchor(path, anchorBytes) {
|
|
5
|
+
try {
|
|
6
|
+
const handle = await open(path, 'r');
|
|
7
|
+
try {
|
|
8
|
+
const metadata = await handle.stat();
|
|
9
|
+
if (anchorBytes < 0 || anchorBytes > metadata.size)
|
|
10
|
+
return undefined;
|
|
11
|
+
return createHash('sha256')
|
|
12
|
+
.update(await readAt(handle, anchorBytes, 0))
|
|
13
|
+
.digest('base64url');
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Reads one newline-aligned window backwards from an opaque byte boundary. */
|
|
24
|
+
export async function readJsonlBackwardWindow(path, options) {
|
|
25
|
+
try {
|
|
26
|
+
const handle = await open(path, 'r');
|
|
27
|
+
try {
|
|
28
|
+
const metadata = await handle.stat();
|
|
29
|
+
const snapshotEnd = options.page?.snapshotEnd ?? metadata.size;
|
|
30
|
+
if (snapshotEnd > metadata.size)
|
|
31
|
+
return undefined;
|
|
32
|
+
const anchorBytes = options.page?.anchorBytes ?? Math.min(4096, snapshotEnd);
|
|
33
|
+
if (anchorBytes < 0 || anchorBytes > snapshotEnd)
|
|
34
|
+
return undefined;
|
|
35
|
+
const anchor = await readAt(handle, anchorBytes, 0);
|
|
36
|
+
const fileAnchor = createHash('sha256').update(anchor).digest('base64url');
|
|
37
|
+
if (options.page !== undefined && options.page.fileAnchor !== fileAnchor)
|
|
38
|
+
return undefined;
|
|
39
|
+
const headSize = Math.min(options.headBytes, snapshotEnd);
|
|
40
|
+
const end = Math.min(Math.max(options.page?.nextEnd ?? snapshotEnd, headSize), snapshotEnd);
|
|
41
|
+
const rawStart = Math.max(headSize, end - options.maxBytes);
|
|
42
|
+
const head = await readAt(handle, headSize, 0);
|
|
43
|
+
let tail = await readAt(handle, end - rawStart, rawStart);
|
|
44
|
+
let start = rawStart;
|
|
45
|
+
if (rawStart > headSize) {
|
|
46
|
+
const newline = tail.indexOf(0x0a);
|
|
47
|
+
if (newline < 0) {
|
|
48
|
+
tail = Buffer.alloc(0);
|
|
49
|
+
start = end;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
start += newline + 1;
|
|
53
|
+
tail = tail.subarray(newline + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (tail.length > 0) {
|
|
57
|
+
let lines = tail.at(-1) === 0x0a ? 0 : 1;
|
|
58
|
+
for (let index = tail.length - 1; index >= 0; index -= 1) {
|
|
59
|
+
if (tail[index] !== 0x0a)
|
|
60
|
+
continue;
|
|
61
|
+
lines += 1;
|
|
62
|
+
if (lines <= options.targetLines)
|
|
63
|
+
continue;
|
|
64
|
+
start += index + 1;
|
|
65
|
+
tail = tail.subarray(index + 1);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const contiguous = start === headSize;
|
|
70
|
+
return {
|
|
71
|
+
content: contiguous
|
|
72
|
+
? Buffer.concat([head, tail]).toString('utf8')
|
|
73
|
+
: `${head.toString('utf8')}${headSize > 0 && tail.length > 0 ? '\n' : ''}${tail.toString('utf8')}`,
|
|
74
|
+
truncated: start > headSize,
|
|
75
|
+
pageContent: tail.toString('utf8'),
|
|
76
|
+
size: metadata.size,
|
|
77
|
+
start,
|
|
78
|
+
end,
|
|
79
|
+
snapshotEnd,
|
|
80
|
+
fileAnchor,
|
|
81
|
+
anchorBytes
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await handle.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
3
92
|
/** Reads bounded head/tail windows without ever loading an unbounded transcript. */
|
|
4
93
|
export async function readJsonlWindow(path, options) {
|
|
5
94
|
try {
|
|
@@ -59,6 +59,16 @@ export interface NativeSessionHistory {
|
|
|
59
59
|
readonly lastActivityAt: number;
|
|
60
60
|
/** The bounded reader omitted bytes between its header and tail windows. */
|
|
61
61
|
readonly truncated?: boolean;
|
|
62
|
+
/** Opaque paging facts for reading the preceding newline-aligned JSONL window. */
|
|
63
|
+
readonly windowStart?: number;
|
|
64
|
+
readonly windowEnd?: number;
|
|
65
|
+
readonly snapshotEnd?: number;
|
|
66
|
+
readonly fileAnchor?: string;
|
|
67
|
+
readonly anchorBytes?: number;
|
|
68
|
+
/** The supplied page cursor no longer matched this transcript. */
|
|
69
|
+
readonly cursorReset?: boolean;
|
|
70
|
+
/** Page-size input used to derive the stable JSONL line window. */
|
|
71
|
+
readonly windowReadLimit?: number;
|
|
62
72
|
}
|
|
63
73
|
/** Actual context-window facts emitted by the Codex native transcript. */
|
|
64
74
|
export interface NativeCodexContextUsage {
|
|
@@ -84,7 +94,13 @@ export declare function discoverNativeSessions(runner: RunnerName, workspacePath
|
|
|
84
94
|
*/
|
|
85
95
|
export declare function discoverAllNativeSessions(runner: RunnerName): Promise<readonly NativeSessionHistory[]>;
|
|
86
96
|
/** Reads one already-discovered external session again to follow appended JSONL records. */
|
|
87
|
-
export declare function readNativeSession(runner: RunnerName, workspacePath: string, externalSessionId: string
|
|
97
|
+
export declare function readNativeSession(runner: RunnerName, workspacePath: string, externalSessionId: string, page?: {
|
|
98
|
+
readonly snapshotEnd: number;
|
|
99
|
+
readonly nextEnd: number;
|
|
100
|
+
readonly fileAnchor: string;
|
|
101
|
+
readonly anchorBytes: number;
|
|
102
|
+
readonly readLimit?: number;
|
|
103
|
+
}, limit?: number): Promise<NativeSessionHistory | undefined>;
|
|
88
104
|
/**
|
|
89
105
|
* Codex records the most recent prompt token count alongside the model context
|
|
90
106
|
* window in `event_msg/token_count`. Read only the tail of an already
|
|
@@ -2,11 +2,15 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { readFile, readdir, stat, unlink } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, join, resolve } from 'node:path';
|
|
5
|
-
import { forEachJsonlRecord, listJsonlFiles, mapConcurrent, readJsonlWindow } from './native-jsonl-reader.js';
|
|
5
|
+
import { forEachJsonlRecord, listJsonlFiles, mapConcurrent, readJsonlBackwardWindow, readJsonlFileAnchor, readJsonlWindow } from './native-jsonl-reader.js';
|
|
6
6
|
import { nodeLog } from './operational.js';
|
|
7
7
|
import { stripClaudePlanTag } from './runner-command-engine.js';
|
|
8
8
|
const DISCOVERY_READ_BYTES = 32 * 1024;
|
|
9
|
-
const
|
|
9
|
+
const TRANSCRIPT_HEADER_READ_BYTES = 32 * 1024;
|
|
10
|
+
const TRANSCRIPT_READ_MAX_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const TRANSCRIPT_MIN_LINES = 256;
|
|
12
|
+
const TRANSCRIPT_MAX_LINES = 4096;
|
|
13
|
+
const TRANSCRIPT_LINES_PER_TURN = 32;
|
|
10
14
|
const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
|
|
11
15
|
const CLAUDE_CONTEXT_USAGE_READ_BYTES = [CONTEXT_USAGE_READ_BYTES, 1024 * 1024, 4 * 1024 * 1024];
|
|
12
16
|
const TRANSCRIPT_INDEX_TTL_MS = 60 * 60_000;
|
|
@@ -157,51 +161,68 @@ async function performNativeSessionDiscovery(runner, previous) {
|
|
|
157
161
|
};
|
|
158
162
|
}
|
|
159
163
|
/** Reads one already-discovered external session again to follow appended JSONL records. */
|
|
160
|
-
export async function readNativeSession(runner, workspacePath, externalSessionId) {
|
|
164
|
+
export async function readNativeSession(runner, workspacePath, externalSessionId, page, limit = 30) {
|
|
161
165
|
const wanted = canonical(workspacePath);
|
|
162
166
|
const path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
|
|
163
167
|
if (path === undefined)
|
|
164
168
|
return undefined;
|
|
165
|
-
const
|
|
169
|
+
const detail = await readTranscriptDetail(path, runner, page, page?.readLimit ?? limit);
|
|
170
|
+
const parsed = detail?.history;
|
|
166
171
|
if (parsed === undefined ||
|
|
167
172
|
parsed.externalSessionId !== externalSessionId ||
|
|
168
173
|
canonical(parsed.cwd) !== wanted) {
|
|
169
174
|
deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
|
|
170
175
|
return undefined;
|
|
171
176
|
}
|
|
172
|
-
return parsed;
|
|
177
|
+
return detail?.cursorAccepted === false ? { ...parsed, cursorReset: true } : parsed;
|
|
173
178
|
}
|
|
174
|
-
async function readTranscriptDetail(path, runner) {
|
|
175
|
-
const key = `${runner}\u0000${path}`;
|
|
179
|
+
async function readTranscriptDetail(path, runner, page, limit) {
|
|
176
180
|
let fingerprint;
|
|
177
181
|
try {
|
|
178
182
|
const metadata = await stat(path);
|
|
179
183
|
fingerprint = `${metadata.size}:${metadata.mtimeMs}`;
|
|
180
184
|
}
|
|
181
185
|
catch {
|
|
182
|
-
transcriptDetailCache.delete(key);
|
|
183
|
-
transcriptDetailReads.delete(key);
|
|
184
186
|
return undefined;
|
|
185
187
|
}
|
|
188
|
+
const metadataSize = Number(fingerprint.slice(0, fingerprint.indexOf(':')));
|
|
189
|
+
const validPage = page !== undefined &&
|
|
190
|
+
page.snapshotEnd <= metadataSize &&
|
|
191
|
+
(await readJsonlFileAnchor(path, page.anchorBytes)) === page.fileAnchor
|
|
192
|
+
? page
|
|
193
|
+
: undefined;
|
|
194
|
+
const targetLines = Math.min(TRANSCRIPT_MAX_LINES, Math.max(TRANSCRIPT_MIN_LINES, Math.max(1, limit) * TRANSCRIPT_LINES_PER_TURN));
|
|
195
|
+
const pageKey = `${validPage === undefined
|
|
196
|
+
? 'latest'
|
|
197
|
+
: `${validPage.fileAnchor}:${validPage.snapshotEnd}:${validPage.nextEnd}`}:${targetLines}`;
|
|
198
|
+
const key = `${runner}\u0000${path}\u0000${pageKey}`;
|
|
199
|
+
const cacheFingerprint = validPage === undefined ? fingerprint : `${validPage.fileAnchor}:${validPage.snapshotEnd}`;
|
|
186
200
|
const cached = transcriptDetailCache.get(key);
|
|
187
|
-
if (cached?.fingerprint ===
|
|
201
|
+
if (cached?.fingerprint === cacheFingerprint && cached.expiresAt > Date.now()) {
|
|
188
202
|
cached.expiresAt = Date.now() + TRANSCRIPT_DETAIL_CACHE_TTL_MS;
|
|
189
203
|
transcriptDetailCache.delete(key);
|
|
190
204
|
transcriptDetailCache.set(key, cached);
|
|
191
|
-
return
|
|
205
|
+
return {
|
|
206
|
+
history: cached.parsed,
|
|
207
|
+
cursorAccepted: page === undefined || validPage !== undefined
|
|
208
|
+
};
|
|
192
209
|
}
|
|
193
210
|
transcriptDetailCache.delete(key);
|
|
194
211
|
const current = transcriptDetailReads.get(key);
|
|
195
|
-
if (current?.fingerprint ===
|
|
212
|
+
if (current?.fingerprint === cacheFingerprint) {
|
|
196
213
|
nodeLog('native.session.history.single-flight-joined', { runner });
|
|
197
|
-
return
|
|
214
|
+
return {
|
|
215
|
+
history: await current.read,
|
|
216
|
+
cursorAccepted: page === undefined || validPage !== undefined
|
|
217
|
+
};
|
|
198
218
|
}
|
|
199
219
|
const startedAt = performance.now();
|
|
200
|
-
const read = readTranscript(path, runner,
|
|
220
|
+
const read = readTranscript(path, runner, TRANSCRIPT_READ_MAX_BYTES, validPage, targetLines)
|
|
221
|
+
.then((parsed) => parsed === undefined ? undefined : { ...parsed, windowReadLimit: page?.readLimit ?? limit })
|
|
201
222
|
.then((parsed) => {
|
|
202
223
|
if (transcriptDetailReads.get(key)?.read === read) {
|
|
203
224
|
transcriptDetailCache.set(key, {
|
|
204
|
-
fingerprint,
|
|
225
|
+
fingerprint: cacheFingerprint,
|
|
205
226
|
parsed,
|
|
206
227
|
expiresAt: Date.now() + TRANSCRIPT_DETAIL_CACHE_TTL_MS
|
|
207
228
|
});
|
|
@@ -220,8 +241,8 @@ async function readTranscriptDetail(path, runner) {
|
|
|
220
241
|
if (transcriptDetailReads.get(key)?.read === read)
|
|
221
242
|
transcriptDetailReads.delete(key);
|
|
222
243
|
});
|
|
223
|
-
transcriptDetailReads.set(key, { fingerprint, read });
|
|
224
|
-
return read;
|
|
244
|
+
transcriptDetailReads.set(key, { fingerprint: cacheFingerprint, read });
|
|
245
|
+
return { history: await read, cursorAccepted: page === undefined || validPage !== undefined };
|
|
225
246
|
}
|
|
226
247
|
function pruneTranscriptDetailCache() {
|
|
227
248
|
const now = Date.now();
|
|
@@ -354,7 +375,7 @@ export async function removeNativeSession(runner, workspacePath, externalSession
|
|
|
354
375
|
}
|
|
355
376
|
if (path === undefined)
|
|
356
377
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
357
|
-
const parsed = await readTranscript(path, runner,
|
|
378
|
+
const parsed = await readTranscript(path, runner, DISCOVERY_READ_BYTES);
|
|
358
379
|
if (parsed === undefined ||
|
|
359
380
|
parsed.externalSessionId !== externalSessionId ||
|
|
360
381
|
canonical(parsed.cwd) !== wanted) {
|
|
@@ -370,8 +391,13 @@ export async function removeNativeSession(runner, workspacePath, externalSession
|
|
|
370
391
|
throw error;
|
|
371
392
|
}
|
|
372
393
|
deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
|
|
373
|
-
|
|
374
|
-
|
|
394
|
+
const detailKeyPrefix = `${runner}\u0000${path}\u0000`;
|
|
395
|
+
for (const key of transcriptDetailCache.keys())
|
|
396
|
+
if (key.startsWith(detailKeyPrefix))
|
|
397
|
+
transcriptDetailCache.delete(key);
|
|
398
|
+
for (const key of transcriptDetailReads.keys())
|
|
399
|
+
if (key.startsWith(detailKeyPrefix))
|
|
400
|
+
transcriptDetailReads.delete(key);
|
|
375
401
|
invalidateNativeDiscovery(runner);
|
|
376
402
|
}
|
|
377
403
|
function transcriptRoot(runner) {
|
|
@@ -388,8 +414,13 @@ function invalidateNativeDiscovery(runner) {
|
|
|
388
414
|
discoverySnapshots.delete(key);
|
|
389
415
|
discoveryReads.delete(key);
|
|
390
416
|
}
|
|
391
|
-
async function readTranscript(path, runner, byteLimit) {
|
|
392
|
-
const window = await
|
|
417
|
+
async function readTranscript(path, runner, byteLimit, page, targetLines = Number.MAX_SAFE_INTEGER) {
|
|
418
|
+
const window = await readJsonlBackwardWindow(path, {
|
|
419
|
+
headBytes: Math.min(byteLimit, TRANSCRIPT_HEADER_READ_BYTES),
|
|
420
|
+
targetLines,
|
|
421
|
+
maxBytes: byteLimit,
|
|
422
|
+
...(page === undefined ? {} : { page })
|
|
423
|
+
});
|
|
393
424
|
if (window === undefined)
|
|
394
425
|
return undefined;
|
|
395
426
|
const { content, truncated } = window;
|
|
@@ -405,6 +436,8 @@ async function readTranscript(path, runner, byteLimit) {
|
|
|
405
436
|
derivedSession = true;
|
|
406
437
|
cwd ??= workspacePath(value);
|
|
407
438
|
id ??= sessionId(value);
|
|
439
|
+
});
|
|
440
|
+
forEachJsonlRecord(truncated ? window.pageContent : content, (value) => {
|
|
408
441
|
const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
|
|
409
442
|
const parentId = typeof value['parentUuid'] === 'string' ? value['parentUuid'] : undefined;
|
|
410
443
|
if (value['isMeta'] === true && recordId !== undefined)
|
|
@@ -470,7 +503,12 @@ async function readTranscript(path, runner, byteLimit) {
|
|
|
470
503
|
digest: createHash('sha256').update(content).digest('hex'),
|
|
471
504
|
items,
|
|
472
505
|
lastActivityAt: Math.max(0, ...items.map((item) => item.createdAt ?? 0)),
|
|
473
|
-
truncated
|
|
506
|
+
truncated,
|
|
507
|
+
windowStart: window.start,
|
|
508
|
+
windowEnd: window.end,
|
|
509
|
+
snapshotEnd: window.snapshotEnd,
|
|
510
|
+
fileAnchor: window.fileAnchor,
|
|
511
|
+
anchorBytes: window.anchorBytes
|
|
474
512
|
};
|
|
475
513
|
}
|
|
476
514
|
function derivedTranscriptRecord(runner, value) {
|
|
@@ -252,8 +252,9 @@ export class CodexRunner extends AbstractRunner {
|
|
|
252
252
|
}
|
|
253
253
|
async readConversationHistory(readers) {
|
|
254
254
|
// Codex JSONL is the low-latency durable record for normal history reads,
|
|
255
|
-
// while thread/read can take seconds on large threads.
|
|
256
|
-
//
|
|
255
|
+
// while thread/read can take seconds on large threads. A bounded tail is a
|
|
256
|
+
// valid recent-history page; use the official API only when JSONL is absent
|
|
257
|
+
// or cannot produce any visible history.
|
|
257
258
|
const native = await readers.native();
|
|
258
259
|
if (native !== undefined)
|
|
259
260
|
return { ...native, source: 'native' };
|
|
@@ -272,7 +273,7 @@ export class CodexRunner extends AbstractRunner {
|
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
acceptsTruncatedNativeHistory() {
|
|
275
|
-
return
|
|
276
|
+
return true;
|
|
276
277
|
}
|
|
277
278
|
async readManagedRunnerTitle(session, nativeSessionId) {
|
|
278
279
|
void session;
|
|
@@ -31,7 +31,7 @@ export declare class ConversationHistoryService {
|
|
|
31
31
|
private prefetchActiveHistory;
|
|
32
32
|
private takeActiveHistoryRead;
|
|
33
33
|
refreshExternalActivity(session: NodeAgentSession): Promise<void>;
|
|
34
|
-
readNative(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeSessionHistory | undefined>;
|
|
34
|
+
readNative(session: NodeAgentSession, cursor?: string, limit?: number): Promise<import("../native-session-history.js").NativeSessionHistory | undefined>;
|
|
35
35
|
private attachNativeImages;
|
|
36
36
|
readOfficial(session: NodeAgentSession, input: {
|
|
37
37
|
readonly cursor?: string;
|
|
@@ -158,7 +158,7 @@ export class ConversationHistoryService {
|
|
|
158
158
|
const startedAt = performance.now();
|
|
159
159
|
let history;
|
|
160
160
|
try {
|
|
161
|
-
history = await this.readNative(session);
|
|
161
|
+
history = await this.readNative(session, input.cursor, input.limit);
|
|
162
162
|
}
|
|
163
163
|
catch {
|
|
164
164
|
return undefined;
|
|
@@ -174,7 +174,8 @@ export class ConversationHistoryService {
|
|
|
174
174
|
}
|
|
175
175
|
if (history === undefined)
|
|
176
176
|
return undefined;
|
|
177
|
-
if (history.truncated === true &&
|
|
177
|
+
if (history.truncated === true &&
|
|
178
|
+
(!runner.acceptsTruncatedNativeHistory() || history.items.length === 0)) {
|
|
178
179
|
incompleteNative = true;
|
|
179
180
|
return undefined;
|
|
180
181
|
}
|
|
@@ -182,7 +183,11 @@ export class ConversationHistoryService {
|
|
|
182
183
|
? []
|
|
183
184
|
: this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 })
|
|
184
185
|
.turns;
|
|
185
|
-
const native = nativeConversationPage(session, history,
|
|
186
|
+
const native = nativeConversationPage(session, history, history.cursorReset === true
|
|
187
|
+
? input.limit === undefined
|
|
188
|
+
? {}
|
|
189
|
+
: { limit: input.limit }
|
|
190
|
+
: input, runtimeTurns, (images) => this.options.attachments.registerNative(session, images));
|
|
186
191
|
return native;
|
|
187
192
|
},
|
|
188
193
|
official: () => this.readOfficial(session, input)
|
|
@@ -286,14 +291,15 @@ export class ConversationHistoryService {
|
|
|
286
291
|
if (activity !== undefined)
|
|
287
292
|
this.options.setExternalActivity(session.id, activity);
|
|
288
293
|
}
|
|
289
|
-
async readNative(session) {
|
|
294
|
+
async readNative(session, cursor, limit = 30) {
|
|
290
295
|
if (session.externalSessionId === null)
|
|
291
296
|
return undefined;
|
|
292
|
-
const
|
|
297
|
+
const page = nativeHistoryPageCursor(cursor, session.id);
|
|
298
|
+
const current = await readNativeSession(session.runner, session.cwd, session.externalSessionId, page, limit);
|
|
293
299
|
if (current !== undefined)
|
|
294
300
|
return current;
|
|
295
301
|
await discoverNativeSessions(session.runner, session.cwd);
|
|
296
|
-
return readNativeSession(session.runner, session.cwd, session.externalSessionId);
|
|
302
|
+
return readNativeSession(session.runner, session.cwd, session.externalSessionId, page, limit);
|
|
297
303
|
}
|
|
298
304
|
async attachNativeImages(session, turns) {
|
|
299
305
|
const startedAt = performance.now();
|
|
@@ -364,6 +370,38 @@ export class ConversationHistoryService {
|
|
|
364
370
|
};
|
|
365
371
|
}
|
|
366
372
|
}
|
|
373
|
+
function nativeHistoryPageCursor(cursor, sessionId) {
|
|
374
|
+
if (cursor === undefined)
|
|
375
|
+
return undefined;
|
|
376
|
+
try {
|
|
377
|
+
const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
378
|
+
return value.sessionId === sessionId &&
|
|
379
|
+
Number.isInteger(value.snapshotEnd) &&
|
|
380
|
+
Number.isInteger(value.nextEnd) &&
|
|
381
|
+
Number.isInteger(value.anchorBytes) &&
|
|
382
|
+
value.snapshotEnd >= 0 &&
|
|
383
|
+
value.nextEnd >= 0 &&
|
|
384
|
+
value.nextEnd <= value.snapshotEnd &&
|
|
385
|
+
value.anchorBytes >= 0 &&
|
|
386
|
+
value.anchorBytes <= value.snapshotEnd &&
|
|
387
|
+
(value.readLimit === undefined ||
|
|
388
|
+
(Number.isInteger(value.readLimit) &&
|
|
389
|
+
value.readLimit >= 1 &&
|
|
390
|
+
value.readLimit <= 500)) &&
|
|
391
|
+
typeof value.fileAnchor === 'string'
|
|
392
|
+
? {
|
|
393
|
+
snapshotEnd: value.snapshotEnd,
|
|
394
|
+
nextEnd: value.nextEnd,
|
|
395
|
+
fileAnchor: value.fileAnchor,
|
|
396
|
+
anchorBytes: value.anchorBytes,
|
|
397
|
+
...(value.readLimit === undefined ? {} : { readLimit: value.readLimit })
|
|
398
|
+
}
|
|
399
|
+
: undefined;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
367
405
|
const ACTIVE_HISTORY_CURSOR_PREFIX = 'mar-active-history:1:';
|
|
368
406
|
function encodeActiveHistoryCursor(cursor) {
|
|
369
407
|
return `${ACTIVE_HISTORY_CURSOR_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString('base64url')}`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { stripClaudePlanTag } from '../runner-command-engine.js';
|
|
3
4
|
import { compactRunnerText, isClaudeCommandTool, isPlainRecord, nativeClaudeCommand, nativePageEnd } from './node-operation-parsers.js';
|
|
@@ -152,26 +153,27 @@ export function deduplicateDirectSessions(sessions) {
|
|
|
152
153
|
}
|
|
153
154
|
export function nativeConversationPage(session, history, input, runtimeTurns = [], registerImages = () => []) {
|
|
154
155
|
const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
|
|
156
|
+
const effectiveCursor = history.cursorReset === true ? undefined : input.cursor;
|
|
155
157
|
const nativeTurns = nativeTranscriptTurns(history.items);
|
|
158
|
+
const nativeTurnIds = nativeTranscriptTurnIds(session.id, nativeTurns);
|
|
156
159
|
const representedClientMessageIds = new Set(nativeTurns.flatMap((entries) => entries.flatMap(({ entry, index }) => {
|
|
157
160
|
if (entry.kind !== 'message' || entry.role !== 'USER')
|
|
158
161
|
return [];
|
|
159
162
|
const clientMessageId = nativeUserClientMessageId(entry, entry.createdAt ?? index, runtimeTurns);
|
|
160
163
|
return clientMessageId === null ? [] : [clientMessageId];
|
|
161
164
|
})));
|
|
162
|
-
const missingRuntimeErrorTurns =
|
|
165
|
+
const missingRuntimeErrorTurns = effectiveCursor === undefined
|
|
163
166
|
? runtimeTurns.filter((turn) => {
|
|
164
167
|
const clientMessageId = runtimeTurnClientMessageId(turn);
|
|
165
168
|
return (turn.items.some((item) => item.kind === 'error') &&
|
|
166
169
|
(clientMessageId === undefined || !representedClientMessageIds.has(clientMessageId)));
|
|
167
170
|
})
|
|
168
171
|
: [];
|
|
169
|
-
const end = nativePageEnd(
|
|
172
|
+
const end = nativePageEnd(effectiveCursor, session.id, nativeTurns.length);
|
|
170
173
|
const start = Math.max(0, end - limit);
|
|
171
174
|
const nativePageTurns = nativeTurns
|
|
172
175
|
.slice(start, end)
|
|
173
176
|
.map((entries, relativeIndex) => {
|
|
174
|
-
const firstIndex = entries[0]?.index ?? 0;
|
|
175
177
|
const nativeUser = entries.find(({ entry }) => entry.kind === 'message' && entry.role === 'USER')?.entry;
|
|
176
178
|
const matchedClientMessageId = nativeUser?.kind === 'message'
|
|
177
179
|
? nativeUserClientMessageId(nativeUser, nativeUser.createdAt, runtimeTurns)
|
|
@@ -179,9 +181,10 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
179
181
|
const runtimeTurn = typeof matchedClientMessageId === 'string'
|
|
180
182
|
? runtimeTurns.find((turn) => runtimeTurnClientMessageId(turn) === matchedClientMessageId)
|
|
181
183
|
: undefined;
|
|
182
|
-
const turnId = runtimeTurn?.id ??
|
|
183
|
-
const
|
|
184
|
-
|
|
184
|
+
const turnId = runtimeTurn?.id ?? nativeTurnIds[start + relativeIndex];
|
|
185
|
+
const itemIdentities = nativeTranscriptItemIdentities(entries);
|
|
186
|
+
const nativeItems = entries.map(({ entry, index }, entryIndex) => ({
|
|
187
|
+
...nativeTranscriptConversationItem(session, entry, index, itemIdentities[entryIndex], turnId, runtimeTurns, registerImages),
|
|
185
188
|
runId: runtimeTurn?.runId ?? null
|
|
186
189
|
}));
|
|
187
190
|
const runtimeErrors = (runtimeTurn?.items ?? [])
|
|
@@ -221,11 +224,33 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
221
224
|
const turns = combined.slice(-limit);
|
|
222
225
|
const nativePageIds = new Set(nativePageTurns.map((turn) => turn.id));
|
|
223
226
|
const nextEnd = start + dropped.filter((turn) => nativePageIds.has(turn.id)).length;
|
|
227
|
+
const cursorBase = {
|
|
228
|
+
sessionId: session.id,
|
|
229
|
+
...(history.snapshotEnd === undefined ? {} : { snapshotEnd: history.snapshotEnd }),
|
|
230
|
+
...(history.windowEnd === undefined ? {} : { nextEnd: history.windowEnd }),
|
|
231
|
+
...(history.fileAnchor === undefined ? {} : { fileAnchor: history.fileAnchor }),
|
|
232
|
+
...(history.anchorBytes === undefined ? {} : { anchorBytes: history.anchorBytes }),
|
|
233
|
+
readLimit: history.windowReadLimit ?? limit
|
|
234
|
+
};
|
|
235
|
+
const precedingWindow = history.truncated === true &&
|
|
236
|
+
history.windowStart !== undefined &&
|
|
237
|
+
history.snapshotEnd !== undefined &&
|
|
238
|
+
history.fileAnchor !== undefined &&
|
|
239
|
+
history.anchorBytes !== undefined
|
|
240
|
+
? Buffer.from(JSON.stringify({
|
|
241
|
+
sessionId: session.id,
|
|
242
|
+
snapshotEnd: history.snapshotEnd,
|
|
243
|
+
nextEnd: history.windowStart,
|
|
244
|
+
fileAnchor: history.fileAnchor,
|
|
245
|
+
anchorBytes: history.anchorBytes,
|
|
246
|
+
readLimit: history.windowReadLimit ?? limit
|
|
247
|
+
})).toString('base64url')
|
|
248
|
+
: null;
|
|
224
249
|
return {
|
|
225
250
|
turns,
|
|
226
251
|
nextCursor: nextEnd > 0
|
|
227
|
-
? Buffer.from(JSON.stringify({
|
|
228
|
-
:
|
|
252
|
+
? Buffer.from(JSON.stringify({ ...cursorBase, end: nextEnd })).toString('base64url')
|
|
253
|
+
: precedingWindow
|
|
229
254
|
};
|
|
230
255
|
}
|
|
231
256
|
function runtimeTurnClientMessageId(turn) {
|
|
@@ -237,6 +262,38 @@ function runtimeTurnClientMessageId(turn) {
|
|
|
237
262
|
function lastNativeItemId(entries) {
|
|
238
263
|
return entries.findLast(({ entry }) => entry.nativeId !== undefined)?.entry.nativeId;
|
|
239
264
|
}
|
|
265
|
+
function nativeTranscriptTurnIds(sessionId, turns) {
|
|
266
|
+
const occurrences = new Map();
|
|
267
|
+
return turns.map((entries) => {
|
|
268
|
+
const user = entries.find(({ entry }) => entry.kind === 'message' && entry.role === 'USER')?.entry;
|
|
269
|
+
const first = user ?? entries[0]?.entry;
|
|
270
|
+
const base = first?.nativeId ?? nativeTranscriptItemFingerprint(first);
|
|
271
|
+
const occurrence = occurrences.get(base) ?? 0;
|
|
272
|
+
occurrences.set(base, occurrence + 1);
|
|
273
|
+
return `${sessionId}:native:${base}${occurrence === 0 ? '' : `:${occurrence}`}`;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function nativeTranscriptItemIdentities(entries) {
|
|
277
|
+
const nativeIdCounts = new Map();
|
|
278
|
+
for (const { entry } of entries)
|
|
279
|
+
if (entry.nativeId !== undefined)
|
|
280
|
+
nativeIdCounts.set(entry.nativeId, (nativeIdCounts.get(entry.nativeId) ?? 0) + 1);
|
|
281
|
+
const occurrences = new Map();
|
|
282
|
+
return entries.map(({ entry }) => {
|
|
283
|
+
const base = entry.nativeId !== undefined && nativeIdCounts.get(entry.nativeId) === 1
|
|
284
|
+
? entry.nativeId
|
|
285
|
+
: `${entry.nativeId === undefined ? '' : `${entry.nativeId}:`}${nativeTranscriptItemFingerprint(entry)}`;
|
|
286
|
+
const occurrence = occurrences.get(base) ?? 0;
|
|
287
|
+
occurrences.set(base, occurrence + 1);
|
|
288
|
+
return occurrence === 0 ? base : `${base}:${occurrence}`;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function nativeTranscriptItemFingerprint(entry) {
|
|
292
|
+
return createHash('sha256')
|
|
293
|
+
.update(JSON.stringify(entry ?? null))
|
|
294
|
+
.digest('base64url')
|
|
295
|
+
.slice(0, 24);
|
|
296
|
+
}
|
|
240
297
|
function nativeTranscriptTurns(items) {
|
|
241
298
|
const turns = [];
|
|
242
299
|
let current = [];
|
|
@@ -251,7 +308,7 @@ function nativeTranscriptTurns(items) {
|
|
|
251
308
|
turns.push(current);
|
|
252
309
|
return turns;
|
|
253
310
|
}
|
|
254
|
-
function nativeTranscriptConversationItem(session, entry, index, turnId, runtimeTurns, registerImages) {
|
|
311
|
+
function nativeTranscriptConversationItem(session, entry, index, itemIdentity, turnId, runtimeTurns, registerImages) {
|
|
255
312
|
const time = entry.createdAt;
|
|
256
313
|
const isToolCall = entry.kind === 'tool_call';
|
|
257
314
|
const isUnknown = entry.kind === 'unknown';
|
|
@@ -276,7 +333,7 @@ function nativeTranscriptConversationItem(session, entry, index, turnId, runtime
|
|
|
276
333
|
: '';
|
|
277
334
|
const planText = entry.kind === 'message' && entry.role !== 'USER' ? claudePlanText(entryText) : undefined;
|
|
278
335
|
const item = {
|
|
279
|
-
id: `${turnId}:item:${
|
|
336
|
+
id: `${turnId}:item:${itemIdentity}`,
|
|
280
337
|
sessionId: session.id,
|
|
281
338
|
turnId,
|
|
282
339
|
runId: null,
|
|
@@ -301,7 +358,7 @@ function nativeTranscriptConversationItem(session, entry, index, turnId, runtime
|
|
|
301
358
|
status: 'COMPLETED',
|
|
302
359
|
payload: isUserInputRequest
|
|
303
360
|
? {
|
|
304
|
-
requestId: `native:${entry.kind === 'tool_call' ? entry.toolUseId : `item-${
|
|
361
|
+
requestId: `native:${entry.kind === 'tool_call' ? entry.toolUseId : `item-${itemIdentity}`}`,
|
|
305
362
|
questions: userInputQuestions
|
|
306
363
|
}
|
|
307
364
|
: isReasoning
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node-pty": "1.1.0",
|
|
25
25
|
"ws": "^8.21.3",
|
|
26
26
|
"zod": "4.4.3",
|
|
27
|
-
"@myagentroam/protocol": "^0.9.
|
|
27
|
+
"@myagentroam/protocol": "^0.9.6"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/ws": "^8.18.1"
|