@myagentroam/node 0.11.14 → 0.11.16
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/connector.js +13 -20
- package/dist/native-session-history.js +35 -832
- package/dist/native-transcript-adapters.js +9 -0
- package/dist/native-transcript-model.js +1 -0
- package/dist/runner/abstract-runner.js +12 -0
- package/dist/runner/claude/conversation-parser.js +80 -1
- package/dist/runner/claude/managed-run-controller.js +4 -2
- package/dist/runner/claude/message-parsers.js +37 -0
- package/dist/runner/claude/native-context-usage.js +90 -0
- package/dist/runner/claude/native-transcript.js +347 -0
- package/dist/runner/claude/tool-normalizer.js +12 -2
- package/dist/runner/claude-code-runner.js +13 -2
- package/dist/runner/codex/conversation-parser.js +67 -3
- package/dist/runner/codex/managed-run-controller.js +3 -2
- package/dist/runner/codex/native-context-usage.js +42 -0
- package/dist/runner/codex/native-transcript.js +575 -0
- package/dist/runner/codex-runner.js +18 -3
- package/dist/runner/mar-agent/managed-run-controller.js +1 -1
- package/dist/runner/mar-agent-runner.js +6 -0
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.js +1 -1
- package/dist/runner/opencode-runner.js +7 -1
- package/dist/runner/runner-registry.js +4 -0
- package/dist/runtime-state.js +1 -0
- package/dist/service/continuous-run-coordinator.js +1 -0
- package/dist/service/conversation-template-manifest.js +179 -0
- package/dist/service/conversation-template-node-service.js +10 -132
- package/dist/service/external-session-resume-service.js +15 -0
- package/dist/service/fake-managed-run-service.js +1 -1
- package/dist/service/goal-continuation-coordinator.js +4 -3
- package/dist/service/goal-runtime.js +0 -9
- package/dist/service/hosted-conversation-node-service.js +16 -179
- package/dist/service/native-session-projection-service.js +7 -24
- package/dist/service/node-control-channel.js +1 -1
- package/dist/service/node-request-service.js +1 -1
- package/dist/service/node-run-event-bridge.js +0 -24
- package/dist/service/node-terminal-channel.js +1 -1
- package/dist/service/recent-run-projection-service.js +2 -1
- package/dist/service/run-workbench-service.js +1 -1
- package/dist/service/runner-interaction-service.js +2 -1
- package/dist/service/runner-service.js +6 -4
- package/dist/service/session-identity-service.js +1 -0
- package/dist/service/session-message-service.js +1 -0
- package/dist/service/session-query-service.js +1 -1
- package/dist/service/workbench-event-projection.js +1 -1
- package/dist/service/workbench-event-service.js +1 -1
- package/dist/service/workspace-file-service.js +0 -2
- package/dist/service/workspace-queue-workbench-service.js +2 -2
- package/dist/util/node-operation-parsers.js +0 -45
- package/dist/util/runner-native-session-parsers.js +17 -194
- package/package.json +4 -4
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { stat, unlink } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { listJsonlFiles, mapConcurrent, readJsonlBackwardWindow, readJsonlFileAnchor } from './native-jsonl-reader.js';
|
|
6
|
+
import { nativeTranscriptAdapter } from './native-transcript-adapters.js';
|
|
6
7
|
import { nodeLog } from './operational.js';
|
|
7
|
-
import { stripClaudePlanTag } from './runner-command-engine.js';
|
|
8
|
-
import { stripGoalStartPrompt } from './service/goal-runtime.js';
|
|
9
|
-
import { parseCodexComposerImageMarkers } from './runner/codex/composer-image-markers.js';
|
|
10
|
-
import { codexTranscriptSourceItemId } from './runner/codex/conversation-parser.js';
|
|
11
|
-
import { claudeAssistantBlocks, extractClaudeThinkingText } from './runner/claude/conversation-parser.js';
|
|
12
|
-
import { extractClaudeToolResults } from './runner/claude/tool-normalizer.js';
|
|
13
8
|
const DISCOVERY_READ_BYTES = 32 * 1024;
|
|
14
9
|
const TRANSCRIPT_HEADER_READ_BYTES = 32 * 1024;
|
|
15
10
|
const TRANSCRIPT_READ_MAX_BYTES = 16 * 1024 * 1024;
|
|
16
11
|
const TRANSCRIPT_MIN_LINES = 256;
|
|
17
12
|
const TRANSCRIPT_MAX_LINES = 4096;
|
|
18
13
|
const TRANSCRIPT_LINES_PER_TURN = 32;
|
|
19
|
-
const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
|
|
20
|
-
const CLAUDE_CONTEXT_USAGE_READ_BYTES = [CONTEXT_USAGE_READ_BYTES, 1024 * 1024, 4 * 1024 * 1024];
|
|
21
14
|
const TRANSCRIPT_INDEX_TTL_MS = 60 * 60_000;
|
|
22
15
|
const DISCOVERY_SNAPSHOT_TTL_MS = 15_000;
|
|
23
16
|
const TRANSCRIPT_DETAIL_CACHE_TTL_MS = 60_000;
|
|
@@ -29,8 +22,6 @@ const transcriptDetailReads = new Map();
|
|
|
29
22
|
const discoveryReads = new Map();
|
|
30
23
|
const discoverySnapshots = new Map();
|
|
31
24
|
const discoveryGenerations = new Map();
|
|
32
|
-
const DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS = 200_000;
|
|
33
|
-
const CLAUDE_CONTEXT_WINDOW_ENV = 'MAR_CLAUDE_CONTEXT_WINDOW_TOKENS';
|
|
34
25
|
/**
|
|
35
26
|
* Read-only adapter for the supported vendor JSONL stores. It requires an
|
|
36
27
|
* explicit cwd on the record and returns normalized visible messages and tool
|
|
@@ -76,7 +67,7 @@ async function performNativeSessionDiscovery(runner, previous) {
|
|
|
76
67
|
const startedAt = performance.now();
|
|
77
68
|
const found = new Map();
|
|
78
69
|
nodeLog('native.session.discovery.started', { runner });
|
|
79
|
-
const files = (await listJsonlFiles(transcriptRoot(runner))).filter((path) => runner
|
|
70
|
+
const files = (await listJsonlFiles(transcriptRoot(runner))).filter((path) => nativeTranscriptAdapter(runner).includesPath(path));
|
|
80
71
|
nodeLog('native.session.discovery.files-listed', {
|
|
81
72
|
runner,
|
|
82
73
|
files: files.length,
|
|
@@ -261,110 +252,6 @@ function pruneTranscriptDetailCache() {
|
|
|
261
252
|
transcriptDetailCache.delete(oldest);
|
|
262
253
|
}
|
|
263
254
|
}
|
|
264
|
-
/**
|
|
265
|
-
* Codex records the most recent prompt token count alongside the model context
|
|
266
|
-
* window in `event_msg/token_count`. Read only the tail of an already
|
|
267
|
-
* discovered transcript: the count is append-only and never needs a costly
|
|
268
|
-
* full-history scan.
|
|
269
|
-
*/
|
|
270
|
-
export async function readCodexNativeContextUsage(workspacePath, externalSessionId) {
|
|
271
|
-
const wanted = canonical(workspacePath);
|
|
272
|
-
let path = getTranscriptIndex(indexKey('codex', wanted, externalSessionId));
|
|
273
|
-
if (path === undefined) {
|
|
274
|
-
await discoverNativeSessions('codex', workspacePath);
|
|
275
|
-
path = getTranscriptIndex(indexKey('codex', wanted, externalSessionId));
|
|
276
|
-
}
|
|
277
|
-
if (path === undefined)
|
|
278
|
-
return undefined;
|
|
279
|
-
const window = await readJsonlWindow(path, { tailBytes: CONTEXT_USAGE_READ_BYTES });
|
|
280
|
-
if (window === undefined)
|
|
281
|
-
return undefined;
|
|
282
|
-
let usage;
|
|
283
|
-
forEachJsonlRecord(window.content, (record) => {
|
|
284
|
-
const next = codexContextUsage(record);
|
|
285
|
-
if (next !== undefined)
|
|
286
|
-
usage = next;
|
|
287
|
-
});
|
|
288
|
-
return usage;
|
|
289
|
-
}
|
|
290
|
-
/** Reads the latest actual Claude prompt occupancy without starting a CLI/SDK control stream. */
|
|
291
|
-
export async function readClaudeNativeContextUsage(workspacePath, externalSessionId, options = {}) {
|
|
292
|
-
const wanted = canonical(workspacePath);
|
|
293
|
-
let path = getTranscriptIndex(indexKey('claude-code', wanted, externalSessionId));
|
|
294
|
-
path ??= await locateClaudeTranscript(wanted, externalSessionId);
|
|
295
|
-
if (path === undefined) {
|
|
296
|
-
await discoverNativeSessions('claude-code', workspacePath);
|
|
297
|
-
path = getTranscriptIndex(indexKey('claude-code', wanted, externalSessionId));
|
|
298
|
-
}
|
|
299
|
-
if (path === undefined)
|
|
300
|
-
return undefined;
|
|
301
|
-
const latest = await readLatestClaudeContextUsage(path);
|
|
302
|
-
if (latest === undefined)
|
|
303
|
-
return undefined;
|
|
304
|
-
return {
|
|
305
|
-
...latest,
|
|
306
|
-
maxTokens: await claudeContextWindowTokens(options)
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
async function readLatestClaudeContextUsage(path) {
|
|
310
|
-
for (const tailBytes of CLAUDE_CONTEXT_USAGE_READ_BYTES) {
|
|
311
|
-
const window = await readJsonlWindow(path, { tailBytes });
|
|
312
|
-
if (window === undefined)
|
|
313
|
-
return undefined;
|
|
314
|
-
let latest;
|
|
315
|
-
forEachJsonlRecord(window.content, (record) => {
|
|
316
|
-
const usage = claudeContextUsage(record);
|
|
317
|
-
if (usage !== undefined)
|
|
318
|
-
latest = usage;
|
|
319
|
-
});
|
|
320
|
-
if (latest !== undefined)
|
|
321
|
-
return latest;
|
|
322
|
-
if (!window.truncated)
|
|
323
|
-
return undefined;
|
|
324
|
-
}
|
|
325
|
-
return undefined;
|
|
326
|
-
}
|
|
327
|
-
/** Claude main transcripts use the native session ID as their filename. */
|
|
328
|
-
async function locateClaudeTranscript(wantedCwd, externalSessionId) {
|
|
329
|
-
const root = transcriptRoot('claude-code');
|
|
330
|
-
let entries;
|
|
331
|
-
try {
|
|
332
|
-
entries = await readdir(root, { withFileTypes: true });
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
return undefined;
|
|
336
|
-
}
|
|
337
|
-
const candidates = entries
|
|
338
|
-
.filter((entry) => entry.isDirectory())
|
|
339
|
-
.map((entry) => join(root, entry.name, `${externalSessionId}.jsonl`));
|
|
340
|
-
const parsed = await mapConcurrent(candidates, 16, async (path) => ({
|
|
341
|
-
path,
|
|
342
|
-
history: await readTranscript(path, 'claude-code', DISCOVERY_READ_BYTES)
|
|
343
|
-
}));
|
|
344
|
-
const match = parsed.find(({ history }) => history?.externalSessionId === externalSessionId && canonical(history.cwd) === wantedCwd);
|
|
345
|
-
if (match === undefined)
|
|
346
|
-
return undefined;
|
|
347
|
-
setTranscriptIndex(indexKey('claude-code', wantedCwd, externalSessionId), match.path);
|
|
348
|
-
return match.path;
|
|
349
|
-
}
|
|
350
|
-
export async function claudeContextWindowTokens(options = {}) {
|
|
351
|
-
const environment = options.environment ?? process.env;
|
|
352
|
-
const direct = positiveInteger(environment[CLAUDE_CONTEXT_WINDOW_ENV]);
|
|
353
|
-
if (direct !== undefined)
|
|
354
|
-
return direct;
|
|
355
|
-
try {
|
|
356
|
-
const settings = JSON.parse(await readFile(options.settingsPath ?? join(homedir(), '.claude', 'settings.json'), 'utf8'));
|
|
357
|
-
if (isRecord(settings) && isRecord(settings.env)) {
|
|
358
|
-
const configured = positiveInteger(settings.env[CLAUDE_CONTEXT_WINDOW_ENV]);
|
|
359
|
-
if (configured !== undefined)
|
|
360
|
-
return configured;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
// Missing or malformed optional Claude settings use the stable default.
|
|
365
|
-
}
|
|
366
|
-
return DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS;
|
|
367
|
-
}
|
|
368
255
|
/**
|
|
369
256
|
* Permanently removes exactly one previously discovered transcript. The
|
|
370
257
|
* caller supplies no path: the in-process discovery index resolves it, then
|
|
@@ -406,9 +293,7 @@ export async function removeNativeSession(runner, workspacePath, externalSession
|
|
|
406
293
|
invalidateNativeDiscovery(runner);
|
|
407
294
|
}
|
|
408
295
|
function transcriptRoot(runner) {
|
|
409
|
-
return runner
|
|
410
|
-
? join(homedir(), '.codex', 'sessions')
|
|
411
|
-
: join(homedir(), '.claude', 'projects');
|
|
296
|
+
return nativeTranscriptAdapter(runner).transcriptRoot();
|
|
412
297
|
}
|
|
413
298
|
function discoveryKey(runner) {
|
|
414
299
|
return `${runner}\u0000${transcriptRoot(runner)}`;
|
|
@@ -428,102 +313,16 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
|
|
|
428
313
|
});
|
|
429
314
|
if (window === undefined)
|
|
430
315
|
return undefined;
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
let id;
|
|
434
|
-
let derivedSession = false;
|
|
435
|
-
const items = [];
|
|
436
|
-
const toolIndexes = new Map();
|
|
437
|
-
const metaRecordIds = new Set();
|
|
438
|
-
const claudeCompactCommandIds = new Set();
|
|
439
|
-
let codexTurnId;
|
|
440
|
-
forEachJsonlRecord(content, (value) => {
|
|
441
|
-
if (derivedTranscriptRecord(runner, value))
|
|
442
|
-
derivedSession = true;
|
|
443
|
-
cwd ??= workspacePath(value);
|
|
444
|
-
id ??= sessionId(value);
|
|
445
|
-
if (runner === 'claude-code' &&
|
|
446
|
-
typeof value['uuid'] === 'string' &&
|
|
447
|
-
isClaudeCompactCommand(value))
|
|
448
|
-
claudeCompactCommandIds.add(value['uuid']);
|
|
449
|
-
});
|
|
450
|
-
forEachJsonlRecord(truncated ? window.pageContent : content, (value) => {
|
|
451
|
-
if (runner === 'codex' && value['type'] === 'event_msg' && isRecord(value['payload'])) {
|
|
452
|
-
const payload = value['payload'];
|
|
453
|
-
if (payload['type'] === 'task_started')
|
|
454
|
-
codexTurnId = typeof payload['turn_id'] === 'string' ? payload['turn_id'] : undefined;
|
|
455
|
-
else if (payload['type'] === 'task_complete')
|
|
456
|
-
codexTurnId = undefined;
|
|
457
|
-
}
|
|
458
|
-
const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
|
|
459
|
-
const parentId = typeof value['parentUuid'] === 'string' ? value['parentUuid'] : undefined;
|
|
460
|
-
if (value['isMeta'] === true && recordId !== undefined)
|
|
461
|
-
metaRecordIds.add(recordId);
|
|
462
|
-
if (runner === 'claude-code' &&
|
|
463
|
-
parentId !== undefined &&
|
|
464
|
-
claudeCompactCommandIds.has(parentId) &&
|
|
465
|
-
isClaudeCompactSuccess(value)) {
|
|
466
|
-
items.push({
|
|
467
|
-
kind: 'context_compaction',
|
|
468
|
-
phase: 'COMPLETED',
|
|
469
|
-
summary: null,
|
|
470
|
-
createdAt: timestampOf(value),
|
|
471
|
-
nativeId: parentId
|
|
472
|
-
});
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (isHiddenTranscriptRecord(value) ||
|
|
476
|
-
(parentId !== undefined && metaRecordIds.has(parentId))) {
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
const generatedImage = codexGeneratedImageEvent(value);
|
|
480
|
-
if (generatedImage !== undefined)
|
|
481
|
-
items.push(generatedImage);
|
|
482
|
-
const sourceItemId = runner === 'codex' ? codexTranscriptSourceItemId(value, codexTurnId) : undefined;
|
|
483
|
-
for (const parsedItem of transcriptItems(runner, value)) {
|
|
484
|
-
const item = {
|
|
485
|
-
...parsedItem,
|
|
486
|
-
...(sourceItemId === undefined ? {} : { sourceItemId }),
|
|
487
|
-
...(recordId === undefined ? {} : { nativeId: recordId }),
|
|
488
|
-
...(runner === 'codex' &&
|
|
489
|
-
parsedItem.kind === 'message' &&
|
|
490
|
-
parsedItem.role === 'USER' &&
|
|
491
|
-
codexTurnId !== undefined
|
|
492
|
-
? { nativeTurnId: codexTurnId }
|
|
493
|
-
: {})
|
|
494
|
-
};
|
|
495
|
-
if (item.kind === 'tool_result') {
|
|
496
|
-
const toolIndex = toolIndexes.get(item.toolUseId);
|
|
497
|
-
if (toolIndex !== undefined)
|
|
498
|
-
items[toolIndex] = withToolResult(items[toolIndex], item);
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (item.kind === 'tool_call') {
|
|
502
|
-
toolIndexes.set(item.toolUseId, items.length);
|
|
503
|
-
items.push(item);
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
items.push(item);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
if (derivedSession)
|
|
510
|
-
return undefined;
|
|
511
|
-
if (cwd === undefined)
|
|
316
|
+
const parsed = nativeTranscriptAdapter(runner)?.parse(path, window);
|
|
317
|
+
if (parsed === undefined)
|
|
512
318
|
return undefined;
|
|
513
|
-
const title = items.find((item) => item.kind === 'message' && item.role === 'USER')?.text;
|
|
514
|
-
// Tagged Plan Mode instruction labels do not form a session title.
|
|
515
|
-
const strippedTitle = title === undefined ? undefined : stripClaudePlanTag(stripGoalStartPrompt(title));
|
|
516
319
|
return {
|
|
320
|
+
...parsed,
|
|
517
321
|
runner,
|
|
518
|
-
externalSessionId:
|
|
519
|
-
|
|
520
|
-
...(
|
|
521
|
-
|
|
522
|
-
: { title: compact(strippedTitle) }),
|
|
523
|
-
digest: createHash('sha256').update(content).digest('hex'),
|
|
524
|
-
items,
|
|
525
|
-
lastActivityAt: Math.max(0, ...items.map((item) => item.createdAt ?? 0)),
|
|
526
|
-
truncated,
|
|
322
|
+
externalSessionId: parsed.externalSessionId ?? basename(path, '.jsonl'),
|
|
323
|
+
digest: createHash('sha256').update(window.content).digest('hex'),
|
|
324
|
+
lastActivityAt: Math.max(0, ...parsed.items.map((item) => item.createdAt ?? 0)),
|
|
325
|
+
truncated: window.truncated,
|
|
527
326
|
windowStart: window.start,
|
|
528
327
|
windowEnd: window.end,
|
|
529
328
|
snapshotEnd: window.snapshotEnd,
|
|
@@ -531,621 +330,6 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
|
|
|
531
330
|
anchorBytes: window.anchorBytes
|
|
532
331
|
};
|
|
533
332
|
}
|
|
534
|
-
function derivedTranscriptRecord(runner, value) {
|
|
535
|
-
if (runner === 'claude-code')
|
|
536
|
-
return value['isSidechain'] === true;
|
|
537
|
-
if (runner !== 'codex' || value['type'] !== 'session_meta')
|
|
538
|
-
return false;
|
|
539
|
-
const payload = isRecord(value['payload']) ? value['payload'] : undefined;
|
|
540
|
-
if (payload === undefined)
|
|
541
|
-
return false;
|
|
542
|
-
if (payload['thread_source'] === 'subagent')
|
|
543
|
-
return true;
|
|
544
|
-
const source = payload['source'];
|
|
545
|
-
return (source === 'subagent' ||
|
|
546
|
-
(isRecord(source) && (source['kind'] === 'subagent' || isRecord(source['subagent']))));
|
|
547
|
-
}
|
|
548
|
-
function workspacePath(record) {
|
|
549
|
-
return (firstString(record, ['cwd', 'working_directory', 'workspacePath']) ??
|
|
550
|
-
nestedString(record, ['payload', 'cwd']) ??
|
|
551
|
-
nestedString(record, ['message', 'cwd']));
|
|
552
|
-
}
|
|
553
|
-
function sessionId(record) {
|
|
554
|
-
// A Codex sub-agent transcript records its own thread in `payload.id`, but
|
|
555
|
-
// retains the parent thread in `payload.session_id`. Treating the latter
|
|
556
|
-
// as this file's identity makes the child file overwrite the parent thread
|
|
557
|
-
// in the discovery index, which then shows unrelated messages for the
|
|
558
|
-
// parent Chat.
|
|
559
|
-
if (record['type'] === 'session_meta') {
|
|
560
|
-
const transcriptId = nestedString(record, ['payload', 'id']);
|
|
561
|
-
if (transcriptId !== undefined)
|
|
562
|
-
return transcriptId;
|
|
563
|
-
}
|
|
564
|
-
return (firstString(record, ['sessionId', 'session_id', 'thread_id', 'conversationId']) ??
|
|
565
|
-
nestedString(record, ['payload', 'id']) ??
|
|
566
|
-
nestedString(record, ['payload', 'session_id']) ??
|
|
567
|
-
nestedString(record, ['payload', 'thread_id']));
|
|
568
|
-
}
|
|
569
|
-
function codexContextUsage(value) {
|
|
570
|
-
if (!isRecord(value) || value.type !== 'event_msg' || !isRecord(value.payload))
|
|
571
|
-
return undefined;
|
|
572
|
-
const payload = value.payload;
|
|
573
|
-
if (payload.type !== 'token_count' || !isRecord(payload.info))
|
|
574
|
-
return undefined;
|
|
575
|
-
const promptUsage = payload.info.last_token_usage;
|
|
576
|
-
const maxTokens = payload.info.model_context_window;
|
|
577
|
-
if (!isRecord(promptUsage) ||
|
|
578
|
-
typeof promptUsage.input_tokens !== 'number' ||
|
|
579
|
-
typeof maxTokens !== 'number')
|
|
580
|
-
return undefined;
|
|
581
|
-
if (!Number.isFinite(promptUsage.input_tokens) ||
|
|
582
|
-
!Number.isInteger(promptUsage.input_tokens) ||
|
|
583
|
-
!Number.isFinite(maxTokens) ||
|
|
584
|
-
!Number.isInteger(maxTokens) ||
|
|
585
|
-
maxTokens <= 0 ||
|
|
586
|
-
promptUsage.input_tokens < 0)
|
|
587
|
-
return undefined;
|
|
588
|
-
return { usedTokens: promptUsage.input_tokens, maxTokens };
|
|
589
|
-
}
|
|
590
|
-
function claudeContextUsage(value) {
|
|
591
|
-
if (value.type !== 'assistant' || !isRecord(value.message))
|
|
592
|
-
return undefined;
|
|
593
|
-
const model = value.message.model;
|
|
594
|
-
const usage = value.message.usage;
|
|
595
|
-
if (typeof model !== 'string' ||
|
|
596
|
-
model.length === 0 ||
|
|
597
|
-
model === '<synthetic>' ||
|
|
598
|
-
!isRecord(usage))
|
|
599
|
-
return undefined;
|
|
600
|
-
const inputTokens = nonNegativeInteger(usage.input_tokens);
|
|
601
|
-
const cacheCreationTokens = optionalNonNegativeInteger(usage.cache_creation_input_tokens);
|
|
602
|
-
const cacheReadTokens = optionalNonNegativeInteger(usage.cache_read_input_tokens);
|
|
603
|
-
if (inputTokens === undefined ||
|
|
604
|
-
cacheCreationTokens === undefined ||
|
|
605
|
-
cacheReadTokens === undefined)
|
|
606
|
-
return undefined;
|
|
607
|
-
return { model, usedTokens: inputTokens + cacheCreationTokens + cacheReadTokens };
|
|
608
|
-
}
|
|
609
|
-
function optionalNonNegativeInteger(value) {
|
|
610
|
-
return value === undefined ? 0 : nonNegativeInteger(value);
|
|
611
|
-
}
|
|
612
|
-
function nonNegativeInteger(value) {
|
|
613
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
614
|
-
}
|
|
615
|
-
function positiveInteger(value) {
|
|
616
|
-
if (typeof value !== 'string' || !/^\d+$/.test(value.trim()))
|
|
617
|
-
return undefined;
|
|
618
|
-
const parsed = Number(value);
|
|
619
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
620
|
-
}
|
|
621
|
-
function transcriptItems(runner, record) {
|
|
622
|
-
if (runner === 'claude-code' && record.type === 'assistant') {
|
|
623
|
-
return claudeAssistantBlocks(record).flatMap(({ record: blockRecord, blockIndex, sourceItemId }) => {
|
|
624
|
-
const block = contentBlocks(blockRecord)[0];
|
|
625
|
-
if (block?.type === 'thinking') {
|
|
626
|
-
const text = extractClaudeThinkingText(blockRecord)[0] ?? '';
|
|
627
|
-
return text.length === 0
|
|
628
|
-
? []
|
|
629
|
-
: [
|
|
630
|
-
{
|
|
631
|
-
kind: 'reasoning_summary',
|
|
632
|
-
representation: 'visible_thinking',
|
|
633
|
-
sections: [{ index: blockIndex, text }],
|
|
634
|
-
createdAt: timestampOf(record),
|
|
635
|
-
...(sourceItemId === undefined ? {} : { sourceItemId })
|
|
636
|
-
}
|
|
637
|
-
];
|
|
638
|
-
}
|
|
639
|
-
return transcriptRecordItems(runner, blockRecord).map((item) => ({
|
|
640
|
-
...item,
|
|
641
|
-
...(sourceItemId === undefined ? {} : { sourceItemId })
|
|
642
|
-
}));
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
return transcriptRecordItems(runner, record);
|
|
646
|
-
}
|
|
647
|
-
function transcriptRecordItems(runner, record) {
|
|
648
|
-
const createdAt = timestampOf(record);
|
|
649
|
-
const results = [...toolResultsOf(record, createdAt), ...codexToolResultsOf(record, createdAt)];
|
|
650
|
-
if (results.length > 0)
|
|
651
|
-
return results;
|
|
652
|
-
const values = [
|
|
653
|
-
...toolUsesOf(record, createdAt),
|
|
654
|
-
...codexToolUsesOf(record, createdAt),
|
|
655
|
-
...codexReasoningSummariesOf(record, createdAt),
|
|
656
|
-
...unsupportedCodexResponseItems(record, createdAt),
|
|
657
|
-
...unsupportedTranscriptItems(record, createdAt)
|
|
658
|
-
];
|
|
659
|
-
const role = roleOf(record);
|
|
660
|
-
if (role === undefined)
|
|
661
|
-
return values;
|
|
662
|
-
const rawText = messageText(textOf(record) ?? '');
|
|
663
|
-
if (rawText.length === 0)
|
|
664
|
-
return values;
|
|
665
|
-
// Codex transcripts can contain the host's injected AGENTS.md and runtime
|
|
666
|
-
// context as a `user` item before the first actual prompt. It is neither a
|
|
667
|
-
// user-visible chat turn nor a meaningful session title.
|
|
668
|
-
if (role === 'USER' && isInjectedContextPrompt(rawText))
|
|
669
|
-
return values;
|
|
670
|
-
const message = role === 'USER' && runner === 'codex'
|
|
671
|
-
? parseCodexComposerImageMarkers(rawText)
|
|
672
|
-
: { text: rawText, images: [] };
|
|
673
|
-
values.unshift({
|
|
674
|
-
kind: 'message',
|
|
675
|
-
role,
|
|
676
|
-
text: message.text,
|
|
677
|
-
...(message.images.length === 0
|
|
678
|
-
? {}
|
|
679
|
-
: {
|
|
680
|
-
imageAttachments: message.images.map((image) => ({
|
|
681
|
-
name: `Image #${image.index}`,
|
|
682
|
-
mime: imageMime(image.localPath),
|
|
683
|
-
localPath: image.localPath
|
|
684
|
-
}))
|
|
685
|
-
}),
|
|
686
|
-
createdAt
|
|
687
|
-
});
|
|
688
|
-
return values;
|
|
689
|
-
}
|
|
690
|
-
function unsupportedCodexResponseItems(record, createdAt) {
|
|
691
|
-
const payload = codexToolPayload(record);
|
|
692
|
-
const type = typeof payload?.type === 'string' ? payload.type : undefined;
|
|
693
|
-
if (type === undefined ||
|
|
694
|
-
typeof payload?.role === 'string' ||
|
|
695
|
-
type === 'reasoning' ||
|
|
696
|
-
type === 'custom_tool_call' ||
|
|
697
|
-
type === 'custom_tool_call_output' ||
|
|
698
|
-
type === 'function_call' ||
|
|
699
|
-
type === 'function_call_output')
|
|
700
|
-
return [];
|
|
701
|
-
return [
|
|
702
|
-
{
|
|
703
|
-
kind: 'unknown',
|
|
704
|
-
sourceEventType: `codex:${type}`,
|
|
705
|
-
label: `Codex ${compact(type).slice(0, 128)}`,
|
|
706
|
-
createdAt
|
|
707
|
-
}
|
|
708
|
-
];
|
|
709
|
-
}
|
|
710
|
-
function codexReasoningSummariesOf(record, createdAt) {
|
|
711
|
-
const payload = codexToolPayload(record);
|
|
712
|
-
if (payload?.type !== 'reasoning')
|
|
713
|
-
return [];
|
|
714
|
-
const sections = Array.isArray(payload.summary)
|
|
715
|
-
? payload.summary.flatMap((value, index) => {
|
|
716
|
-
if (typeof value !== 'string')
|
|
717
|
-
return [];
|
|
718
|
-
const text = compact(value).slice(0, 10_000);
|
|
719
|
-
return text.length === 0 ? [] : [{ index, text }];
|
|
720
|
-
})
|
|
721
|
-
: [];
|
|
722
|
-
return [{ kind: 'reasoning_summary', representation: 'summary', sections, createdAt }];
|
|
723
|
-
}
|
|
724
|
-
function unsupportedTranscriptItems(record, createdAt) {
|
|
725
|
-
return contentBlocks(record).flatMap((block) => {
|
|
726
|
-
const type = typeof block.type === 'string' ? block.type : undefined;
|
|
727
|
-
if (type === undefined ||
|
|
728
|
-
type === 'text' ||
|
|
729
|
-
type === 'input_text' ||
|
|
730
|
-
type === 'output_text' ||
|
|
731
|
-
type === 'image' ||
|
|
732
|
-
type === 'input_image' ||
|
|
733
|
-
type === 'thinking' ||
|
|
734
|
-
type === 'redacted_thinking' ||
|
|
735
|
-
type === 'encrypted_thinking' ||
|
|
736
|
-
type === 'tool_use' ||
|
|
737
|
-
type === 'tool_result')
|
|
738
|
-
return [];
|
|
739
|
-
return [
|
|
740
|
-
{
|
|
741
|
-
kind: 'unknown',
|
|
742
|
-
sourceEventType: `transcript:${type}`,
|
|
743
|
-
label: `Agent ${compact(type).slice(0, 128)}`,
|
|
744
|
-
createdAt
|
|
745
|
-
}
|
|
746
|
-
];
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
function toolUsesOf(record, createdAt) {
|
|
750
|
-
return contentBlocks(record).flatMap((block) => {
|
|
751
|
-
if (block['type'] !== 'tool_use' ||
|
|
752
|
-
typeof block['id'] !== 'string' ||
|
|
753
|
-
typeof block['name'] !== 'string')
|
|
754
|
-
return [];
|
|
755
|
-
const input = block['input'];
|
|
756
|
-
const summary = compactValue(input, 10_000);
|
|
757
|
-
return [
|
|
758
|
-
{
|
|
759
|
-
kind: 'tool_call',
|
|
760
|
-
toolUseId: block['id'],
|
|
761
|
-
toolName: compact(block['name']).slice(0, 128),
|
|
762
|
-
input,
|
|
763
|
-
inputSummary: summary.text || null,
|
|
764
|
-
inputTruncated: summary.truncated,
|
|
765
|
-
outputSummary: null,
|
|
766
|
-
outputTruncated: false,
|
|
767
|
-
failed: false,
|
|
768
|
-
createdAt
|
|
769
|
-
}
|
|
770
|
-
];
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
function toolResultsOf(record, createdAt) {
|
|
774
|
-
return extractClaudeToolResults(record).map((result) => ({
|
|
775
|
-
...result,
|
|
776
|
-
kind: 'tool_result',
|
|
777
|
-
createdAt
|
|
778
|
-
}));
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Codex persists custom and standard function tools as paired `response_item`
|
|
782
|
-
* records. Their record IDs differ, while `call_id` is the stable identifier
|
|
783
|
-
* shared by invocation and result. Normalize on that key so JSONL fallback
|
|
784
|
-
* history has the same single-card shape as live events.
|
|
785
|
-
*/
|
|
786
|
-
function codexToolUsesOf(record, createdAt) {
|
|
787
|
-
const payload = codexToolPayload(record);
|
|
788
|
-
if (payload?.['type'] !== 'custom_tool_call' && payload?.['type'] !== 'function_call')
|
|
789
|
-
return [];
|
|
790
|
-
const toolUseId = firstString(payload, ['call_id', 'id']);
|
|
791
|
-
const toolName = firstString(payload, ['name', 'tool']);
|
|
792
|
-
if (toolUseId === undefined || toolName === undefined)
|
|
793
|
-
return [];
|
|
794
|
-
const input = payload['input'] ?? payload['arguments'];
|
|
795
|
-
const summary = compactValue(input, 10_000);
|
|
796
|
-
return [
|
|
797
|
-
{
|
|
798
|
-
kind: 'tool_call',
|
|
799
|
-
toolUseId,
|
|
800
|
-
toolName: compact(toolName).slice(0, 128),
|
|
801
|
-
input,
|
|
802
|
-
inputSummary: summary.text || null,
|
|
803
|
-
inputTruncated: summary.truncated,
|
|
804
|
-
outputSummary: null,
|
|
805
|
-
outputTruncated: false,
|
|
806
|
-
failed: false,
|
|
807
|
-
createdAt
|
|
808
|
-
}
|
|
809
|
-
];
|
|
810
|
-
}
|
|
811
|
-
function codexToolResultsOf(record, createdAt) {
|
|
812
|
-
const payload = codexToolPayload(record);
|
|
813
|
-
if (payload?.['type'] !== 'custom_tool_call_output' &&
|
|
814
|
-
payload?.['type'] !== 'function_call_output')
|
|
815
|
-
return [];
|
|
816
|
-
const toolUseId = firstString(payload, ['call_id', 'id']);
|
|
817
|
-
if (toolUseId === undefined)
|
|
818
|
-
return [];
|
|
819
|
-
const output = compactValue(codexToolOutput(payload['output'] ?? payload['contentItems']), 10_000);
|
|
820
|
-
const imageAttachments = codexImagePlaceholders(payload['output'] ?? payload['contentItems']);
|
|
821
|
-
return [
|
|
822
|
-
{
|
|
823
|
-
kind: 'tool_result',
|
|
824
|
-
toolUseId,
|
|
825
|
-
output: output.text,
|
|
826
|
-
failed: payload['status'] === 'failed' || payload['error'] !== undefined,
|
|
827
|
-
truncated: output.truncated,
|
|
828
|
-
...(imageAttachments.length === 0 ? {} : { imageAttachments }),
|
|
829
|
-
createdAt
|
|
830
|
-
}
|
|
831
|
-
];
|
|
832
|
-
}
|
|
833
|
-
function codexToolPayload(record) {
|
|
834
|
-
return record['type'] === 'response_item' && isRecord(record['payload'])
|
|
835
|
-
? record['payload']
|
|
836
|
-
: undefined;
|
|
837
|
-
}
|
|
838
|
-
function codexGeneratedImageEvent(record) {
|
|
839
|
-
if (record['type'] !== 'event_msg' || !isRecord(record['payload']))
|
|
840
|
-
return undefined;
|
|
841
|
-
const payload = record['payload'];
|
|
842
|
-
if (payload['type'] !== 'image_generation_end' ||
|
|
843
|
-
typeof payload['call_id'] !== 'string' ||
|
|
844
|
-
typeof payload['saved_path'] !== 'string')
|
|
845
|
-
return undefined;
|
|
846
|
-
const localPath = payload['saved_path'];
|
|
847
|
-
if (!localPath.includes('/.codex/generated_images/') || !/\.(?:png|jpe?g|webp)$/i.test(localPath))
|
|
848
|
-
return undefined;
|
|
849
|
-
return {
|
|
850
|
-
kind: 'tool_call',
|
|
851
|
-
toolUseId: payload['call_id'],
|
|
852
|
-
toolName: 'imagegen',
|
|
853
|
-
input: null,
|
|
854
|
-
inputSummary: null,
|
|
855
|
-
inputTruncated: false,
|
|
856
|
-
outputSummary: payload['status'] === 'completed' ? 'Image generation completed' : 'Image generation failed',
|
|
857
|
-
outputTruncated: false,
|
|
858
|
-
toolCategory: 'IMAGE_GENERATION',
|
|
859
|
-
imageAttachments: [{ name: basename(localPath), mime: imageMime(localPath), localPath }],
|
|
860
|
-
failed: payload['status'] !== 'completed',
|
|
861
|
-
createdAt: timestampOf(record)
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
/** Extract Codex output text blocks instead of rendering their JSON envelopes. */
|
|
865
|
-
function codexToolOutput(value) {
|
|
866
|
-
if (!Array.isArray(value))
|
|
867
|
-
return value;
|
|
868
|
-
const text = value.flatMap((block) => {
|
|
869
|
-
if (typeof block === 'string')
|
|
870
|
-
return [block];
|
|
871
|
-
if (!isRecord(block))
|
|
872
|
-
return [];
|
|
873
|
-
const direct = firstString(block, ['text', 'content']);
|
|
874
|
-
return direct === undefined ? [] : [codexVisibleToolText(direct)];
|
|
875
|
-
});
|
|
876
|
-
return text.length > 0 ? text.join('') : value;
|
|
877
|
-
}
|
|
878
|
-
function codexVisibleToolText(value) {
|
|
879
|
-
const marker = 'data:image/';
|
|
880
|
-
let cursor = 0;
|
|
881
|
-
let visible = '';
|
|
882
|
-
while (cursor < value.length) {
|
|
883
|
-
const start = value.indexOf(marker, cursor);
|
|
884
|
-
if (start < 0)
|
|
885
|
-
return visible + value.slice(cursor);
|
|
886
|
-
visible += `${value.slice(cursor, start)}[Image content filtered]`;
|
|
887
|
-
const doubleQuote = value.indexOf('"', start);
|
|
888
|
-
const singleQuote = value.indexOf("'", start);
|
|
889
|
-
const end = doubleQuote < 0
|
|
890
|
-
? singleQuote
|
|
891
|
-
: singleQuote < 0
|
|
892
|
-
? doubleQuote
|
|
893
|
-
: Math.min(doubleQuote, singleQuote);
|
|
894
|
-
if (end < 0)
|
|
895
|
-
return visible;
|
|
896
|
-
cursor = end;
|
|
897
|
-
}
|
|
898
|
-
return visible;
|
|
899
|
-
}
|
|
900
|
-
/** Extract only image placeholder metadata from native Codex output. */
|
|
901
|
-
function codexImagePlaceholders(value) {
|
|
902
|
-
const attachments = [];
|
|
903
|
-
let visited = 0;
|
|
904
|
-
const visit = (current, depth) => {
|
|
905
|
-
if (attachments.length >= 5 || visited >= 128 || depth > 5 || current == null)
|
|
906
|
-
return;
|
|
907
|
-
visited += 1;
|
|
908
|
-
if (typeof current === 'string') {
|
|
909
|
-
const paths = [];
|
|
910
|
-
collectGeneratedImagePaths(current, paths);
|
|
911
|
-
for (const localPath of paths) {
|
|
912
|
-
if (attachments.some((attachment) => attachment.localPath === localPath))
|
|
913
|
-
continue;
|
|
914
|
-
attachments.push({ name: basename(localPath), mime: imageMime(localPath), localPath });
|
|
915
|
-
}
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
if (Array.isArray(current)) {
|
|
919
|
-
for (const entry of current)
|
|
920
|
-
visit(entry, depth + 1);
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
if (!isRecord(current))
|
|
924
|
-
return;
|
|
925
|
-
const type = typeof current['type'] === 'string' ? current['type'] : '';
|
|
926
|
-
if (type === 'input_text' && typeof current['text'] === 'string') {
|
|
927
|
-
const mime = /["']image_url["']\s*:\s*["']data:(image\/[^;,"']+)[;,]/.exec(current['text'])?.[1];
|
|
928
|
-
if (mime !== undefined)
|
|
929
|
-
attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
|
|
930
|
-
}
|
|
931
|
-
const imageUrl = isRecord(current['image_url'])
|
|
932
|
-
? firstString(current['image_url'], ['url'])
|
|
933
|
-
: typeof current['image_url'] === 'string'
|
|
934
|
-
? current['image_url']
|
|
935
|
-
: undefined;
|
|
936
|
-
const data = typeof current['data'] === 'string' ? current['data'] : imageUrl;
|
|
937
|
-
if ((type === 'image' || type === 'input_image' || type === 'output_image') &&
|
|
938
|
-
data?.startsWith('data:image/')) {
|
|
939
|
-
const mime = /^data:([^;,]+)[;,]/.exec(data)?.[1] ?? 'image/png';
|
|
940
|
-
attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
|
|
941
|
-
}
|
|
942
|
-
for (const child of Object.values(current))
|
|
943
|
-
visit(child, depth + 1);
|
|
944
|
-
};
|
|
945
|
-
visit(value, 0);
|
|
946
|
-
return attachments;
|
|
947
|
-
}
|
|
948
|
-
function collectGeneratedImagePaths(value, paths) {
|
|
949
|
-
const marker = '/.codex/generated_images/';
|
|
950
|
-
let cursor = 0;
|
|
951
|
-
while (paths.length < 5) {
|
|
952
|
-
const markerIndex = value.indexOf(marker, cursor);
|
|
953
|
-
if (markerIndex < 0)
|
|
954
|
-
return;
|
|
955
|
-
let start = markerIndex;
|
|
956
|
-
while (start > 0 && !/[\s"']/.test(value[start - 1]))
|
|
957
|
-
start -= 1;
|
|
958
|
-
let end = markerIndex + marker.length;
|
|
959
|
-
while (end < value.length && !/[\s"']/.test(value[end]))
|
|
960
|
-
end += 1;
|
|
961
|
-
const path = value.slice(start, end).replace(/[),.;:]+$/, '');
|
|
962
|
-
if (/\.(?:png|jpe?g|webp)$/i.test(path) && !paths.includes(path))
|
|
963
|
-
paths.push(path);
|
|
964
|
-
cursor = end;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
function imageMime(path) {
|
|
968
|
-
const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
|
|
969
|
-
if (extension === 'jpg' || extension === 'jpeg')
|
|
970
|
-
return 'image/jpeg';
|
|
971
|
-
if (extension === 'webp')
|
|
972
|
-
return 'image/webp';
|
|
973
|
-
return 'image/png';
|
|
974
|
-
}
|
|
975
|
-
function imageExtension(mime) {
|
|
976
|
-
if (mime === 'image/jpeg')
|
|
977
|
-
return 'jpg';
|
|
978
|
-
if (mime === 'image/webp')
|
|
979
|
-
return 'webp';
|
|
980
|
-
if (mime === 'image/gif')
|
|
981
|
-
return 'gif';
|
|
982
|
-
return 'png';
|
|
983
|
-
}
|
|
984
|
-
function withToolResult(call, result) {
|
|
985
|
-
const viewedImagePath = codexViewedImagePath(call);
|
|
986
|
-
const imageAttachments = result.imageAttachments?.map((attachment) => attachment.localPath !== undefined || viewedImagePath === undefined
|
|
987
|
-
? attachment
|
|
988
|
-
: {
|
|
989
|
-
name: basename(viewedImagePath),
|
|
990
|
-
mime: imageMime(viewedImagePath),
|
|
991
|
-
localPath: viewedImagePath
|
|
992
|
-
});
|
|
993
|
-
return {
|
|
994
|
-
...call,
|
|
995
|
-
outputSummary: result.output,
|
|
996
|
-
outputTruncated: result.truncated,
|
|
997
|
-
...(result.claudeResult === undefined ? {} : { claudeResult: result.claudeResult }),
|
|
998
|
-
...(imageAttachments === undefined ? {} : { imageAttachments }),
|
|
999
|
-
failed: result.failed
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
function codexViewedImagePath(call) {
|
|
1003
|
-
const toolName = call.toolName.toLowerCase();
|
|
1004
|
-
if (toolName === 'view_image' || toolName === 'imageview') {
|
|
1005
|
-
return isRecord(call.input) && typeof call.input['path'] === 'string'
|
|
1006
|
-
? call.input['path']
|
|
1007
|
-
: undefined;
|
|
1008
|
-
}
|
|
1009
|
-
if (toolName !== 'exec' || typeof call.input !== 'string')
|
|
1010
|
-
return undefined;
|
|
1011
|
-
if (!/tools\.view_image\s*\(/.test(call.input))
|
|
1012
|
-
return undefined;
|
|
1013
|
-
const encodedPath = /["']path["']\s*:\s*("(?:\\.|[^"\\])*")/.exec(call.input)?.[1];
|
|
1014
|
-
if (encodedPath === undefined)
|
|
1015
|
-
return undefined;
|
|
1016
|
-
try {
|
|
1017
|
-
const path = JSON.parse(encodedPath);
|
|
1018
|
-
return typeof path === 'string' ? path : undefined;
|
|
1019
|
-
}
|
|
1020
|
-
catch {
|
|
1021
|
-
return undefined;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
function isInjectedContextPrompt(text) {
|
|
1025
|
-
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
1026
|
-
if (normalized.startsWith('<codex_internal_context'))
|
|
1027
|
-
return true;
|
|
1028
|
-
if (/^<(?:environment_context|subagent_notification|turn_aborted)(?:\s|>)/u.test(normalized))
|
|
1029
|
-
return true;
|
|
1030
|
-
return (normalized.startsWith('# AGENTS.md instructions') &&
|
|
1031
|
-
(normalized.includes('<INSTRUCTIONS>') || normalized.includes('<environment_context>')));
|
|
1032
|
-
}
|
|
1033
|
-
function isClaudeCompactCommand(record) {
|
|
1034
|
-
return textOf(record)?.trim() === '<command-name>/compact</command-name>';
|
|
1035
|
-
}
|
|
1036
|
-
function isClaudeCompactSuccess(record) {
|
|
1037
|
-
return textOf(record)?.trim() === '<local-command-stdout>Compacted</local-command-stdout>';
|
|
1038
|
-
}
|
|
1039
|
-
function isHiddenTranscriptRecord(record) {
|
|
1040
|
-
if (contentBlocks(record).some((block) => block['type'] === 'tool_result'))
|
|
1041
|
-
return false;
|
|
1042
|
-
if (record['interruptedByShutdown'] === true ||
|
|
1043
|
-
record['isMeta'] === true ||
|
|
1044
|
-
record['isVisibleInTranscriptOnly'] === true ||
|
|
1045
|
-
record['isCompactSummary'] === true)
|
|
1046
|
-
return true;
|
|
1047
|
-
const text = textOf(record)?.trim() ?? '';
|
|
1048
|
-
return (text.startsWith('<local-command-') ||
|
|
1049
|
-
text.startsWith('<command-name>') ||
|
|
1050
|
-
text.startsWith('<command-message>'));
|
|
1051
|
-
}
|
|
1052
|
-
function roleOf(record) {
|
|
1053
|
-
const candidates = [
|
|
1054
|
-
firstString(record, ['role']),
|
|
1055
|
-
nestedString(record, ['message', 'role']),
|
|
1056
|
-
nestedString(record, ['payload', 'role']),
|
|
1057
|
-
firstString(record, ['type']),
|
|
1058
|
-
nestedString(record, ['payload', 'type'])
|
|
1059
|
-
];
|
|
1060
|
-
if (candidates.some((value) => value === 'user' || value === 'human' || value === 'input'))
|
|
1061
|
-
return 'USER';
|
|
1062
|
-
if (candidates.some((value) => value === 'assistant' || value === 'agent' || value === 'output'))
|
|
1063
|
-
return 'ASSISTANT';
|
|
1064
|
-
return undefined;
|
|
1065
|
-
}
|
|
1066
|
-
function textOf(record) {
|
|
1067
|
-
return (firstString(record, ['content', 'text']) ??
|
|
1068
|
-
nestedText(record['message']) ??
|
|
1069
|
-
nestedText(record['payload']));
|
|
1070
|
-
}
|
|
1071
|
-
function nestedText(value) {
|
|
1072
|
-
if (typeof value === 'string')
|
|
1073
|
-
return value;
|
|
1074
|
-
if (!isRecord(value))
|
|
1075
|
-
return undefined;
|
|
1076
|
-
const direct = firstString(value, ['content', 'text', 'message']);
|
|
1077
|
-
if (direct !== undefined)
|
|
1078
|
-
return direct;
|
|
1079
|
-
if (Array.isArray(value['content'])) {
|
|
1080
|
-
const parts = value['content'].flatMap((part) => {
|
|
1081
|
-
if (typeof part === 'string')
|
|
1082
|
-
return [part];
|
|
1083
|
-
if (!isRecord(part))
|
|
1084
|
-
return [];
|
|
1085
|
-
return [firstString(part, ['text', 'content'])].filter((text) => text !== undefined);
|
|
1086
|
-
});
|
|
1087
|
-
return parts.join('\n');
|
|
1088
|
-
}
|
|
1089
|
-
return undefined;
|
|
1090
|
-
}
|
|
1091
|
-
function contentBlocks(record) {
|
|
1092
|
-
const values = [record['message'], record['payload']];
|
|
1093
|
-
for (const value of values)
|
|
1094
|
-
if (isRecord(value) && Array.isArray(value['content']))
|
|
1095
|
-
return value['content'].filter(isRecord);
|
|
1096
|
-
return [];
|
|
1097
|
-
}
|
|
1098
|
-
function compactValue(value, limit) {
|
|
1099
|
-
if (value === undefined || value === null)
|
|
1100
|
-
return { text: '', truncated: false };
|
|
1101
|
-
let text;
|
|
1102
|
-
if (typeof value === 'string')
|
|
1103
|
-
text = value;
|
|
1104
|
-
else {
|
|
1105
|
-
try {
|
|
1106
|
-
text = JSON.stringify(value);
|
|
1107
|
-
}
|
|
1108
|
-
catch {
|
|
1109
|
-
return { text: '(Unable to display)', truncated: false };
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
return {
|
|
1113
|
-
text: text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 1))}…`,
|
|
1114
|
-
truncated: text.length > limit
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
function timestampOf(record) {
|
|
1118
|
-
const raw = record['timestamp'] ?? record['created_at'] ?? record['createdAt'];
|
|
1119
|
-
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
1120
|
-
return raw < 10_000_000_000 ? raw * 1_000 : raw;
|
|
1121
|
-
if (typeof raw === 'string') {
|
|
1122
|
-
const value = Date.parse(raw);
|
|
1123
|
-
return Number.isNaN(value) ? null : value;
|
|
1124
|
-
}
|
|
1125
|
-
return null;
|
|
1126
|
-
}
|
|
1127
|
-
function firstString(value, keys) {
|
|
1128
|
-
for (const key of keys)
|
|
1129
|
-
if (typeof value[key] === 'string' && value[key].length > 0)
|
|
1130
|
-
return value[key];
|
|
1131
|
-
return undefined;
|
|
1132
|
-
}
|
|
1133
|
-
function nestedString(value, keys) {
|
|
1134
|
-
let current = value;
|
|
1135
|
-
for (const key of keys) {
|
|
1136
|
-
if (!isRecord(current))
|
|
1137
|
-
return undefined;
|
|
1138
|
-
current = current[key];
|
|
1139
|
-
}
|
|
1140
|
-
return typeof current === 'string' && current.length > 0 ? current : undefined;
|
|
1141
|
-
}
|
|
1142
|
-
function compact(value) {
|
|
1143
|
-
return value.replace(/\s+/g, ' ').trim().slice(0, 16_000);
|
|
1144
|
-
}
|
|
1145
|
-
/** Conversation content can be Markdown, so its internal whitespace must not be collapsed like a title. */
|
|
1146
|
-
function messageText(value) {
|
|
1147
|
-
return value.trim().slice(0, 16_000);
|
|
1148
|
-
}
|
|
1149
333
|
function canonical(value) {
|
|
1150
334
|
try {
|
|
1151
335
|
const normalized = resolve(value).replace(/[\\/]+$/, '');
|
|
@@ -1177,6 +361,25 @@ function deleteTranscriptIndex(key) {
|
|
|
1177
361
|
transcriptIndexTimers.delete(key);
|
|
1178
362
|
transcriptIndex.delete(key);
|
|
1179
363
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
364
|
+
/** Context-only location: history and deletion keep their existing discovery/index paths. */
|
|
365
|
+
export async function locateNativeSessionPath(runner, workspacePath, externalSessionId) {
|
|
366
|
+
const wanted = canonical(workspacePath);
|
|
367
|
+
const key = indexKey(runner, wanted, externalSessionId);
|
|
368
|
+
let path = getTranscriptIndex(key);
|
|
369
|
+
if (path !== undefined)
|
|
370
|
+
return path;
|
|
371
|
+
const candidates = await nativeTranscriptAdapter(runner)?.locateCandidates?.(externalSessionId);
|
|
372
|
+
if (candidates !== undefined) {
|
|
373
|
+
const parsed = await mapConcurrent(candidates, 16, async (path) => ({
|
|
374
|
+
path,
|
|
375
|
+
history: await readTranscript(path, runner, DISCOVERY_READ_BYTES)
|
|
376
|
+
}));
|
|
377
|
+
const match = parsed.find(({ history }) => history?.externalSessionId === externalSessionId && canonical(history.cwd) === wanted);
|
|
378
|
+
if (match !== undefined) {
|
|
379
|
+
setTranscriptIndex(key, match.path);
|
|
380
|
+
return match.path;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
await discoverNativeSessions(runner, workspacePath);
|
|
384
|
+
return getTranscriptIndex(key);
|
|
1182
385
|
}
|