@myagentroam/node 0.11.15 → 0.11.17

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.
Files changed (53) hide show
  1. package/dist/connector.js +13 -24
  2. package/dist/native-session-history.js +35 -832
  3. package/dist/native-transcript-adapters.js +9 -0
  4. package/dist/native-transcript-model.js +1 -0
  5. package/dist/runner/abstract-runner.js +12 -0
  6. package/dist/runner/claude/conversation-parser.js +80 -1
  7. package/dist/runner/claude/managed-run-controller.js +4 -2
  8. package/dist/runner/claude/message-parsers.js +37 -0
  9. package/dist/runner/claude/native-context-usage.js +90 -0
  10. package/dist/runner/claude/native-transcript.js +347 -0
  11. package/dist/runner/claude/tool-normalizer.js +12 -2
  12. package/dist/runner/claude-code-runner.js +13 -2
  13. package/dist/runner/codex/conversation-parser.js +67 -3
  14. package/dist/runner/codex/managed-run-controller.js +3 -2
  15. package/dist/runner/codex/native-context-usage.js +42 -0
  16. package/dist/runner/codex/native-transcript.js +575 -0
  17. package/dist/runner/codex-runner.js +18 -3
  18. package/dist/runner/mar-agent/managed-run-controller.js +1 -1
  19. package/dist/runner/mar-agent-runner.js +6 -0
  20. package/dist/runner/opencode/conversation-parser.js +1 -1
  21. package/dist/runner/opencode/managed-run-controller.js +1 -1
  22. package/dist/runner/opencode-runner.js +7 -1
  23. package/dist/runner/runner-registry.js +4 -0
  24. package/dist/runtime-state.js +1 -6
  25. package/dist/service/continuous-run-coordinator.js +1 -0
  26. package/dist/service/conversation-template-manifest.js +179 -0
  27. package/dist/service/conversation-template-node-service.js +10 -132
  28. package/dist/service/external-session-resume-service.js +15 -0
  29. package/dist/service/fake-managed-run-service.js +1 -1
  30. package/dist/service/goal-continuation-coordinator.js +4 -3
  31. package/dist/service/goal-runtime.js +0 -9
  32. package/dist/service/hosted-conversation-node-service.js +16 -179
  33. package/dist/service/hosted-conversation-store.js +2 -33
  34. package/dist/service/native-session-projection-service.js +7 -24
  35. package/dist/service/node-control-channel.js +1 -1
  36. package/dist/service/node-control-message-service.js +0 -8
  37. package/dist/service/node-request-service.js +1 -1
  38. package/dist/service/node-run-event-bridge.js +0 -37
  39. package/dist/service/node-terminal-channel.js +1 -1
  40. package/dist/service/recent-run-projection-service.js +2 -1
  41. package/dist/service/run-workbench-service.js +1 -1
  42. package/dist/service/runner-interaction-service.js +2 -1
  43. package/dist/service/runner-service.js +6 -4
  44. package/dist/service/session-identity-service.js +1 -0
  45. package/dist/service/session-message-service.js +1 -0
  46. package/dist/service/session-query-service.js +1 -1
  47. package/dist/service/workbench-event-projection.js +1 -1
  48. package/dist/service/workbench-event-service.js +1 -1
  49. package/dist/service/workspace-file-service.js +0 -2
  50. package/dist/service/workspace-queue-workbench-service.js +2 -2
  51. package/dist/util/node-operation-parsers.js +0 -45
  52. package/dist/util/runner-native-session-parsers.js +17 -194
  53. package/package.json +5 -5
@@ -1,23 +1,16 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { readFile, readdir, stat, unlink } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
- import { basename, join, resolve } from 'node:path';
5
- import { forEachJsonlRecord, listJsonlFiles, mapConcurrent, readJsonlBackwardWindow, readJsonlFileAnchor, readJsonlWindow } from './native-jsonl-reader.js';
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 !== 'claude-code' || !path.split(/[\\/]/u).includes('subagents'));
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 === 'codex'
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 { content, truncated } = window;
432
- let cwd;
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: id ?? basename(path, '.jsonl'),
519
- cwd,
520
- ...(strippedTitle === undefined || strippedTitle.length === 0
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
- function isRecord(value) {
1181
- return typeof value === 'object' && value !== null && !Array.isArray(value);
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
  }