@myagentroam/node 0.9.0 → 0.9.3

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 (98) hide show
  1. package/dist/connector.d.ts +2 -0
  2. package/dist/connector.js +50 -25
  3. package/dist/control-outbound-scheduler.d.ts +4 -1
  4. package/dist/control-outbound-scheduler.js +39 -6
  5. package/dist/database.d.ts +0 -19
  6. package/dist/database.js +1 -62
  7. package/dist/event-buffer.d.ts +2 -5
  8. package/dist/event-buffer.js +3 -12
  9. package/dist/native-session-history.js +105 -26
  10. package/dist/runner/abstract-runner.d.ts +1 -1
  11. package/dist/runner/abstract-runner.js +2 -2
  12. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  13. package/dist/runner/claude/managed-run-controller.js +32 -17
  14. package/dist/runner/claude-code-runner.js +3 -4
  15. package/dist/runner/codex/conversation-parser.d.ts +3 -3
  16. package/dist/runner/codex/conversation-parser.js +33 -12
  17. package/dist/runner/codex/managed-run-controller.d.ts +2 -2
  18. package/dist/runner/codex/managed-run-controller.js +26 -10
  19. package/dist/runner/codex-runner.d.ts +3 -1
  20. package/dist/runner/codex-runner.js +40 -10
  21. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  22. package/dist/runner/opencode/conversation-parser.js +16 -16
  23. package/dist/runner/opencode/managed-run-controller.d.ts +8 -2
  24. package/dist/runner/opencode/managed-run-controller.js +94 -22
  25. package/dist/runner/opencode-runner.d.ts +3 -0
  26. package/dist/runner/opencode-runner.js +25 -0
  27. package/dist/runner-command-engine.js +1 -1
  28. package/dist/runner-profiles.js +29 -17
  29. package/dist/runner-usage.js +5 -5
  30. package/dist/runtime-state.d.ts +5 -12
  31. package/dist/runtime-state.js +32 -46
  32. package/dist/service/attachment-domain-state.d.ts +2 -0
  33. package/dist/service/attachment-domain-state.js +2 -0
  34. package/dist/service/conversation-history-service.js +55 -24
  35. package/dist/service/conversation-segment-service.d.ts +20 -0
  36. package/dist/service/conversation-segment-service.js +156 -0
  37. package/dist/service/external-session-resume-service.js +9 -0
  38. package/dist/service/fake-managed-run-service.d.ts +0 -1
  39. package/dist/service/fake-managed-run-service.js +0 -2
  40. package/dist/service/mcp-installation-verifier.js +1 -1
  41. package/dist/service/native-session-projection-service.js +5 -0
  42. package/dist/service/native-session-watch-service.d.ts +1 -0
  43. package/dist/service/native-session-watch-service.js +21 -4
  44. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  45. package/dist/service/node-connection-lifecycle-service.js +0 -1
  46. package/dist/service/node-control-channel.js +1 -1
  47. package/dist/service/node-control-message-service.d.ts +0 -1
  48. package/dist/service/node-control-message-service.js +9 -6
  49. package/dist/service/node-request-service.js +2 -1
  50. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  51. package/dist/service/node-workspace-coordinator.js +2 -12
  52. package/dist/service/run-attachment-service.d.ts +3 -2
  53. package/dist/service/run-attachment-service.js +51 -17
  54. package/dist/service/run-event-service.d.ts +2 -0
  55. package/dist/service/run-event-service.js +77 -2
  56. package/dist/service/run-workbench-service.d.ts +1 -1
  57. package/dist/service/run-workbench-service.js +1 -5
  58. package/dist/service/runner-interaction-service.d.ts +1 -2
  59. package/dist/service/runner-interaction-service.js +1 -4
  60. package/dist/service/session-catalog-service.js +4 -4
  61. package/dist/service/session-context-service.d.ts +1 -0
  62. package/dist/service/session-context-service.js +14 -0
  63. package/dist/service/session-domain-state.d.ts +1 -0
  64. package/dist/service/session-domain-state.js +1 -0
  65. package/dist/service/session-message-service.js +5 -10
  66. package/dist/service/session-presentation-service.d.ts +1 -0
  67. package/dist/service/session-presentation-service.js +8 -1
  68. package/dist/service/session-query-service.d.ts +4 -0
  69. package/dist/service/session-query-service.js +6 -1
  70. package/dist/service/skill-directory-service.d.ts +3 -1
  71. package/dist/service/skill-directory-service.js +31 -30
  72. package/dist/service/skill-install-service.js +4 -74
  73. package/dist/service/skill-node-operation-service.d.ts +1 -1
  74. package/dist/service/skill-node-operation-service.js +6 -3
  75. package/dist/service/workbench-event-service.d.ts +1 -13
  76. package/dist/service/workbench-event-service.js +2 -36
  77. package/dist/service/workbench-manifest-service.d.ts +0 -1
  78. package/dist/service/workbench-manifest-service.js +0 -1
  79. package/dist/service/workspace-domain-state.d.ts +1 -0
  80. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  81. package/dist/service/workspace-git-exclude-service.js +119 -0
  82. package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
  83. package/dist/service/workspace-queue-workbench-service.js +4 -2
  84. package/dist/service/workspace-service.js +2 -0
  85. package/dist/service/workspace-upload-service.d.ts +2 -0
  86. package/dist/service/workspace-upload-service.js +28 -6
  87. package/dist/service/workspace-workbench-service.d.ts +0 -3
  88. package/dist/service/workspace-workbench-service.js +0 -17
  89. package/dist/supervisor.js +2 -1
  90. package/dist/terminal.js +1 -1
  91. package/dist/util/node-operation-parsers.js +2 -2
  92. package/dist/util/runner-error.d.ts +6 -0
  93. package/dist/util/runner-error.js +52 -0
  94. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  95. package/dist/util/runner-native-session-parsers.js +48 -20
  96. package/dist/util/safe-error.d.ts +2 -0
  97. package/dist/util/safe-error.js +5 -0
  98. package/package.json +2 -2
@@ -9,7 +9,9 @@ const DISCOVERY_READ_BYTES = 32 * 1024;
9
9
  const TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
10
10
  const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
11
11
  const CLAUDE_CONTEXT_USAGE_READ_BYTES = [CONTEXT_USAGE_READ_BYTES, 1024 * 1024, 4 * 1024 * 1024];
12
+ const TRANSCRIPT_INDEX_TTL_MS = 60 * 60_000;
12
13
  const transcriptIndex = new Map();
14
+ const transcriptIndexTimers = new Map();
13
15
  const discoveryReads = new Map();
14
16
  const DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS = 200_000;
15
17
  const CLAUDE_CONTEXT_WINDOW_ENV = 'MAR_CLAUDE_CONTEXT_WINDOW_TOKENS';
@@ -53,7 +55,7 @@ async function performNativeSessionDiscovery(runner) {
53
55
  if (parsed === undefined)
54
56
  continue;
55
57
  const cwd = canonical(parsed.cwd);
56
- transcriptIndex.set(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
58
+ setTranscriptIndex(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
57
59
  found.set(`${cwd}\u0000${parsed.externalSessionId}`, parsed);
58
60
  }
59
61
  nodeLog('native.session.discovery.completed', {
@@ -83,14 +85,14 @@ async function readDiscoveryHeaders(files, runner) {
83
85
  /** Reads one already-discovered external session again to follow appended JSONL records. */
84
86
  export async function readNativeSession(runner, workspacePath, externalSessionId) {
85
87
  const wanted = canonical(workspacePath);
86
- const path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
88
+ const path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
87
89
  if (path === undefined)
88
90
  return undefined;
89
91
  const parsed = await readTranscript(path, runner, TRANSCRIPT_READ_BYTES);
90
92
  if (parsed === undefined ||
91
93
  parsed.externalSessionId !== externalSessionId ||
92
94
  canonical(parsed.cwd) !== wanted) {
93
- transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
95
+ deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
94
96
  return undefined;
95
97
  }
96
98
  return parsed;
@@ -103,10 +105,10 @@ export async function readNativeSession(runner, workspacePath, externalSessionId
103
105
  */
104
106
  export async function readCodexNativeContextUsage(workspacePath, externalSessionId) {
105
107
  const wanted = canonical(workspacePath);
106
- let path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
108
+ let path = getTranscriptIndex(indexKey('codex', wanted, externalSessionId));
107
109
  if (path === undefined) {
108
110
  await discoverNativeSessions('codex', workspacePath);
109
- path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
111
+ path = getTranscriptIndex(indexKey('codex', wanted, externalSessionId));
110
112
  }
111
113
  if (path === undefined)
112
114
  return undefined;
@@ -124,11 +126,11 @@ export async function readCodexNativeContextUsage(workspacePath, externalSession
124
126
  /** Reads the latest actual Claude prompt occupancy without starting a CLI/SDK control stream. */
125
127
  export async function readClaudeNativeContextUsage(workspacePath, externalSessionId, options = {}) {
126
128
  const wanted = canonical(workspacePath);
127
- let path = transcriptIndex.get(indexKey('claude-code', wanted, externalSessionId));
129
+ let path = getTranscriptIndex(indexKey('claude-code', wanted, externalSessionId));
128
130
  path ??= await locateClaudeTranscript(wanted, externalSessionId);
129
131
  if (path === undefined) {
130
132
  await discoverNativeSessions('claude-code', workspacePath);
131
- path = transcriptIndex.get(indexKey('claude-code', wanted, externalSessionId));
133
+ path = getTranscriptIndex(indexKey('claude-code', wanted, externalSessionId));
132
134
  }
133
135
  if (path === undefined)
134
136
  return undefined;
@@ -178,7 +180,7 @@ async function locateClaudeTranscript(wantedCwd, externalSessionId) {
178
180
  const match = parsed.find(({ history }) => history?.externalSessionId === externalSessionId && canonical(history.cwd) === wantedCwd);
179
181
  if (match === undefined)
180
182
  return undefined;
181
- transcriptIndex.set(indexKey('claude-code', wantedCwd, externalSessionId), match.path);
183
+ setTranscriptIndex(indexKey('claude-code', wantedCwd, externalSessionId), match.path);
182
184
  return match.path;
183
185
  }
184
186
  export async function claudeContextWindowTokens(options = {}) {
@@ -207,10 +209,10 @@ export async function claudeContextWindowTokens(options = {}) {
207
209
  */
208
210
  export async function removeNativeSession(runner, workspacePath, externalSessionId) {
209
211
  const wanted = canonical(workspacePath);
210
- let path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
212
+ let path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
211
213
  if (path === undefined) {
212
214
  await discoverNativeSessions(runner, workspacePath);
213
- path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
215
+ path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
214
216
  }
215
217
  if (path === undefined)
216
218
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
@@ -218,7 +220,7 @@ export async function removeNativeSession(runner, workspacePath, externalSession
218
220
  if (parsed === undefined ||
219
221
  parsed.externalSessionId !== externalSessionId ||
220
222
  canonical(parsed.cwd) !== wanted) {
221
- transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
223
+ deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
222
224
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
223
225
  }
224
226
  try {
@@ -229,7 +231,7 @@ export async function removeNativeSession(runner, workspacePath, externalSession
229
231
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
230
232
  throw error;
231
233
  }
232
- transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
234
+ deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
233
235
  }
234
236
  function transcriptRoot(runner) {
235
237
  return runner === 'codex'
@@ -286,7 +288,7 @@ async function readTranscript(path, runner, byteLimit) {
286
288
  items.push({
287
289
  kind: 'tool_call',
288
290
  toolUseId: result.toolUseId,
289
- toolName: '工具',
291
+ toolName: 'Tool',
290
292
  input: null,
291
293
  inputSummary: null,
292
294
  inputTruncated: false,
@@ -301,7 +303,7 @@ async function readTranscript(path, runner, byteLimit) {
301
303
  if (cwd === undefined)
302
304
  return undefined;
303
305
  const title = items.find((item) => item.kind === 'message' && item.role === 'USER')?.text;
304
- // 标签式 Plan Mode 的指令标签不构成会话标题。
306
+ // Tagged Plan Mode instruction labels do not form a session title.
305
307
  const strippedTitle = title === undefined ? undefined : stripClaudePlanTag(title);
306
308
  return {
307
309
  runner,
@@ -591,7 +593,7 @@ function codexGeneratedImageEvent(record) {
591
593
  input: null,
592
594
  inputSummary: null,
593
595
  inputTruncated: false,
594
- outputSummary: payload['status'] === 'completed' ? '图片生成完成' : '图片生成失败',
596
+ outputSummary: payload['status'] === 'completed' ? 'Image generation completed' : 'Image generation failed',
595
597
  outputTruncated: false,
596
598
  imageAttachments: [{ name: basename(localPath), mime: imageMime(localPath), localPath }],
597
599
  failed: payload['status'] !== 'completed',
@@ -620,7 +622,7 @@ function codexVisibleToolText(value) {
620
622
  const start = value.indexOf(marker, cursor);
621
623
  if (start < 0)
622
624
  return visible + value.slice(cursor);
623
- visible += `${value.slice(cursor, start)}[图片内容已过滤]`;
625
+ visible += `${value.slice(cursor, start)}[Image content filtered]`;
624
626
  const doubleQuote = value.indexOf('"', start);
625
627
  const singleQuote = value.indexOf("'", start);
626
628
  const end = doubleQuote < 0
@@ -636,14 +638,20 @@ function codexVisibleToolText(value) {
636
638
  }
637
639
  /** Extract only image placeholder metadata from native Codex output. */
638
640
  function codexImagePlaceholders(value) {
639
- const paths = [];
641
+ const attachments = [];
640
642
  let visited = 0;
641
643
  const visit = (current, depth) => {
642
- if (paths.length >= 5 || visited >= 128 || depth > 5 || current == null)
644
+ if (attachments.length >= 5 || visited >= 128 || depth > 5 || current == null)
643
645
  return;
644
646
  visited += 1;
645
647
  if (typeof current === 'string') {
648
+ const paths = [];
646
649
  collectGeneratedImagePaths(current, paths);
650
+ for (const localPath of paths) {
651
+ if (attachments.some((attachment) => attachment.localPath === localPath))
652
+ continue;
653
+ attachments.push({ name: basename(localPath), mime: imageMime(localPath), localPath });
654
+ }
647
655
  return;
648
656
  }
649
657
  if (Array.isArray(current)) {
@@ -653,15 +661,28 @@ function codexImagePlaceholders(value) {
653
661
  }
654
662
  if (!isRecord(current))
655
663
  return;
664
+ const type = typeof current['type'] === 'string' ? current['type'] : '';
665
+ if (type === 'input_text' && typeof current['text'] === 'string') {
666
+ const mime = /["']image_url["']\s*:\s*["']data:(image\/[^;,"']+)[;,]/.exec(current['text'])?.[1];
667
+ if (mime !== undefined)
668
+ attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
669
+ }
670
+ const imageUrl = isRecord(current['image_url'])
671
+ ? firstString(current['image_url'], ['url'])
672
+ : typeof current['image_url'] === 'string'
673
+ ? current['image_url']
674
+ : undefined;
675
+ const data = typeof current['data'] === 'string' ? current['data'] : imageUrl;
676
+ if ((type === 'image' || type === 'input_image' || type === 'output_image') &&
677
+ data?.startsWith('data:image/')) {
678
+ const mime = /^data:([^;,]+)[;,]/.exec(data)?.[1] ?? 'image/png';
679
+ attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
680
+ }
656
681
  for (const child of Object.values(current))
657
682
  visit(child, depth + 1);
658
683
  };
659
684
  visit(value, 0);
660
- return paths.map((localPath) => ({
661
- name: basename(localPath),
662
- mime: imageMime(localPath),
663
- localPath
664
- }));
685
+ return attachments;
665
686
  }
666
687
  function collectGeneratedImagePaths(value, paths) {
667
688
  const marker = '/.codex/generated_images/';
@@ -690,15 +711,54 @@ function imageMime(path) {
690
711
  return 'image/webp';
691
712
  return 'image/png';
692
713
  }
714
+ function imageExtension(mime) {
715
+ if (mime === 'image/jpeg')
716
+ return 'jpg';
717
+ if (mime === 'image/webp')
718
+ return 'webp';
719
+ if (mime === 'image/gif')
720
+ return 'gif';
721
+ return 'png';
722
+ }
693
723
  function withToolResult(call, result) {
724
+ const viewedImagePath = codexViewedImagePath(call);
725
+ const imageAttachments = result.imageAttachments?.map((attachment) => attachment.localPath !== undefined || viewedImagePath === undefined
726
+ ? attachment
727
+ : {
728
+ name: basename(viewedImagePath),
729
+ mime: imageMime(viewedImagePath),
730
+ localPath: viewedImagePath
731
+ });
694
732
  return {
695
733
  ...call,
696
734
  outputSummary: result.output,
697
735
  outputTruncated: result.truncated,
698
- ...(result.imageAttachments === undefined ? {} : { imageAttachments: result.imageAttachments }),
736
+ ...(imageAttachments === undefined ? {} : { imageAttachments }),
699
737
  failed: result.failed
700
738
  };
701
739
  }
740
+ function codexViewedImagePath(call) {
741
+ const toolName = call.toolName.toLowerCase();
742
+ if (toolName === 'view_image' || toolName === 'imageview') {
743
+ return isRecord(call.input) && typeof call.input['path'] === 'string'
744
+ ? call.input['path']
745
+ : undefined;
746
+ }
747
+ if (toolName !== 'exec' || typeof call.input !== 'string')
748
+ return undefined;
749
+ if (!/tools\.view_image\s*\(/.test(call.input))
750
+ return undefined;
751
+ const encodedPath = /["']path["']\s*:\s*("(?:\\.|[^"\\])*")/.exec(call.input)?.[1];
752
+ if (encodedPath === undefined)
753
+ return undefined;
754
+ try {
755
+ const path = JSON.parse(encodedPath);
756
+ return typeof path === 'string' ? path : undefined;
757
+ }
758
+ catch {
759
+ return undefined;
760
+ }
761
+ }
702
762
  function isInjectedContextPrompt(text) {
703
763
  const normalized = text.replace(/\s+/g, ' ').trim();
704
764
  if (normalized.startsWith('<codex_internal_context'))
@@ -776,7 +836,7 @@ function compactValue(value, limit) {
776
836
  text = JSON.stringify(value);
777
837
  }
778
838
  catch {
779
- return { text: '(无法显示)', truncated: false };
839
+ return { text: '(Unable to display)', truncated: false };
780
840
  }
781
841
  }
782
842
  return {
@@ -812,7 +872,7 @@ function nestedString(value, keys) {
812
872
  function compact(value) {
813
873
  return value.replace(/\s+/g, ' ').trim().slice(0, 16_000);
814
874
  }
815
- /** 对话正文可能是 Markdown,不能像标题一样压缩内部空白。 */
875
+ /** Conversation content can be Markdown, so its internal whitespace must not be collapsed like a title. */
816
876
  function messageText(value) {
817
877
  return value.trim().slice(0, 16_000);
818
878
  }
@@ -828,6 +888,25 @@ function canonical(value) {
828
888
  function indexKey(runner, cwd, externalSessionId) {
829
889
  return `${runner}\u0000${cwd}\u0000${externalSessionId}`;
830
890
  }
891
+ function setTranscriptIndex(key, path) {
892
+ const previousTimer = transcriptIndexTimers.get(key);
893
+ if (previousTimer !== undefined)
894
+ clearTimeout(previousTimer);
895
+ transcriptIndex.set(key, path);
896
+ const timer = setTimeout(() => deleteTranscriptIndex(key), TRANSCRIPT_INDEX_TTL_MS);
897
+ timer.unref();
898
+ transcriptIndexTimers.set(key, timer);
899
+ }
900
+ function getTranscriptIndex(key) {
901
+ return transcriptIndex.get(key);
902
+ }
903
+ function deleteTranscriptIndex(key) {
904
+ const timer = transcriptIndexTimers.get(key);
905
+ if (timer !== undefined)
906
+ clearTimeout(timer);
907
+ transcriptIndexTimers.delete(key);
908
+ transcriptIndex.delete(key);
909
+ }
831
910
  function isRecord(value) {
832
911
  return typeof value === 'object' && value !== null && !Array.isArray(value);
833
912
  }
@@ -53,7 +53,7 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
53
53
  }, managedTurn: (nativeTurnId: string) => NodeConversationTurn | undefined): Promise<RunnerOfficialConversationPage | undefined>;
54
54
  readExternalActivity(session: NodeAgentSession): Promise<SessionActivityState | undefined>;
55
55
  acceptsTruncatedNativeHistory(): boolean;
56
- nativeImageRoot(session: NodeAgentSession): string | undefined;
56
+ nativeImageRoots(session: NodeAgentSession): readonly string[];
57
57
  managedRunForNativeTurn(nativeTurnId: string): string | undefined;
58
58
  abstract stop(): void | Promise<void>;
59
59
  bindControl(control: RunnerControl): void;
@@ -106,9 +106,9 @@ export class AbstractRunner {
106
106
  acceptsTruncatedNativeHistory() {
107
107
  return true;
108
108
  }
109
- nativeImageRoot(session) {
109
+ nativeImageRoots(session) {
110
110
  void session;
111
- return undefined;
111
+ return [];
112
112
  }
113
113
  managedRunForNativeTurn(nativeTurnId) {
114
114
  void nativeTurnId;
@@ -26,7 +26,6 @@ export interface ClaudeManagedRunHost<TAttachment extends ClaudeRunnerImageAttac
26
26
  hasActiveSessionLease(sessionId: string): boolean;
27
27
  session(sessionId: string): NodeAgentSession | undefined;
28
28
  completeCancelled(runId: string, cwd: string): boolean;
29
- captureSnapshot(runId: string, cwd: string): Promise<void>;
30
29
  requestUserInput(runId: string, input: Record<string, unknown>, toolUseId: string): Promise<ReturnType<typeof denyPermission>>;
31
30
  requestApproval(runId: string, toolName: string, input: Record<string, unknown>): Promise<ReturnType<typeof denyPermission>>;
32
31
  rememberNative(nativeSessionId: string, sessionId: string): void;
@@ -1,9 +1,10 @@
1
1
  import { createEnvelope } from '@myagentroam/protocol';
2
2
  import { ClaudeChannelGate } from '../../claude-channel.js';
3
3
  import { boundedConversationItemId, claudePlanText, compactRunnerText, compactRunnerValue, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolResults, extractClaudeToolUses, isClaudeCommandTool } from '../codex/conversation-parser.js';
4
- import { isTerminalRunStatus } from '../../util/node-operation-parsers.js';
5
4
  import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
6
5
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
6
+ import { safeErrorCode } from '../../util/safe-error.js';
7
+ import { runnerErrorProjection } from '../../util/runner-error.js';
7
8
  export class ClaudeManagedRunController {
8
9
  runner;
9
10
  host;
@@ -24,10 +25,10 @@ export class ClaudeManagedRunController {
24
25
  commandId: 'plan',
25
26
  active: true,
26
27
  label: 'Plan Mode',
27
- detail: '后续消息将以标签式规划模式运行,只读规划并输出计划卡。',
28
+ detail: 'Subsequent messages run in read-only tagged Plan Mode and produce a plan card.',
28
29
  closable: true,
29
30
  closeInput: '/plan',
30
- continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
31
+ continuation: { label: 'Implement', prompt: 'Implement the plan above.' }
31
32
  });
32
33
  return { command: command.id, states: this.host.commandStates(session.id) };
33
34
  }
@@ -83,7 +84,7 @@ export class ClaudeManagedRunController {
83
84
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
84
85
  }
85
86
  catch (error) {
86
- this.host.emit(runId, 'run.rejected', { code: error instanceof Error ? error.message : 'MESSAGE_ATTACHMENTS_INVALID' }, 'FAILED');
87
+ this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'MESSAGE_ATTACHMENTS_INVALID') }, 'FAILED');
87
88
  return;
88
89
  }
89
90
  this.host.adopt({
@@ -115,16 +116,25 @@ export class ClaudeManagedRunController {
115
116
  let terminal = false;
116
117
  let handle;
117
118
  const toolKinds = new Map();
118
- const finish = (status, eventType, eventPayload) => {
119
+ const finish = (status, eventType, eventPayload, failure, fallbackCode = 'CLAUDE_RUN_FAILED') => {
119
120
  if (terminal)
120
121
  return;
121
122
  terminal = true;
122
- this.host.emit(runId, eventType, eventPayload, status);
123
+ const projected = status === 'FAILED'
124
+ ? runnerErrorProjection(failure ?? eventPayload, fallbackCode)
125
+ : undefined;
126
+ if (projected !== undefined) {
127
+ this.host.emitItem(runId, {
128
+ itemId: boundedConversationItemId('runner-error', runId),
129
+ kind: 'error',
130
+ status: 'FAILED',
131
+ payload: { ...projected, recoverable: true, retryAfterMs: null }
132
+ });
133
+ }
134
+ this.host.emit(runId, eventType, projected ?? eventPayload, status);
123
135
  this.runner.execution.runs.delete(runId);
124
136
  handle?.cancel();
125
137
  this.host.active.delete(runId);
126
- if (isTerminalRunStatus(status))
127
- void this.host.captureSnapshot(runId, cwd);
128
138
  };
129
139
  try {
130
140
  const gate = new ClaudeChannelGate(sessionId);
@@ -255,14 +265,23 @@ export class ClaudeManagedRunController {
255
265
  const kind = toolKinds.get(record.tool_use_id);
256
266
  if (kind === 'tool_call') {
257
267
  const elapsed = typeof record.elapsed_time_seconds === 'number'
258
- ? `(${Math.max(0, Math.round(record.elapsed_time_seconds))} 秒)`
268
+ ? ` (${Math.max(0, Math.round(record.elapsed_time_seconds))} seconds)`
259
269
  : '';
260
270
  this.host.emitItem(runId, {
261
271
  itemId: boundedConversationItemId('claude-tool', record.tool_use_id),
262
272
  kind,
263
273
  status: 'IN_PROGRESS',
264
274
  payload: {
265
- progressLabel: compactRunnerText(`${typeof record.tool_name === 'string' ? record.tool_name : '工具'}正在执行${elapsed}`, 500)
275
+ progressLabel: compactRunnerText(`${typeof record.tool_name === 'string' ? record.tool_name : 'Tool'} is running${elapsed}`, 500),
276
+ progressLabelCode: typeof record.elapsed_time_seconds === 'number'
277
+ ? 'TOOL_RUNNING_WITH_DURATION'
278
+ : 'TOOL_RUNNING',
279
+ progressLabelParameters: {
280
+ tool: typeof record.tool_name === 'string' ? record.tool_name : 'Tool',
281
+ seconds: typeof record.elapsed_time_seconds === 'number'
282
+ ? Math.max(0, Math.round(record.elapsed_time_seconds))
283
+ : 0
284
+ }
266
285
  },
267
286
  merge: true
268
287
  });
@@ -304,7 +323,7 @@ export class ClaudeManagedRunController {
304
323
  // result completes this AgentRun even though that iterable can
305
324
  // remain open waiting for another user turn; waiting only for
306
325
  // stream closure would leave the WorkspaceLease permanently held.
307
- finish(subtype === 'success' && record.is_error !== true ? 'SUCCEEDED' : 'FAILED', subtype === 'success' && record.is_error !== true ? 'run.completed' : 'run.failed', { subtype });
326
+ finish(subtype === 'success' && record.is_error !== true ? 'SUCCEEDED' : 'FAILED', subtype === 'success' && record.is_error !== true ? 'run.completed' : 'run.failed', { subtype }, record);
308
327
  if (completedNativeSessionId !== undefined)
309
328
  void this.host.syncTitle(sessionId, completedNativeSessionId, cwd);
310
329
  }
@@ -314,14 +333,10 @@ export class ClaudeManagedRunController {
314
333
  this.host.setChannelToken(sessionId, gate.token);
315
334
  this.host.send('session.channel.ready', { sessionId, token: gate.token });
316
335
  this.host.emit(runId, 'run.running', {}, 'RUNNING');
317
- void handle.completed.then(() => finish('SUCCEEDED', 'run.completed', { subtype: 'stream_closed' }), (error) => finish('FAILED', 'run.failed', {
318
- code: error instanceof Error ? error.message : 'CLAUDE_RUN_FAILED'
319
- }));
336
+ void handle.completed.then(() => finish('SUCCEEDED', 'run.completed', { subtype: 'stream_closed' }), (error) => finish('FAILED', 'run.failed', { code: safeErrorCode(error, 'CLAUDE_RUN_FAILED') }, error));
320
337
  }
321
338
  catch (error) {
322
- finish('FAILED', 'run.failed', {
323
- code: error instanceof Error ? error.message : 'CLAUDE_RUN_START_FAILED'
324
- });
339
+ finish('FAILED', 'run.failed', { code: safeErrorCode(error, 'CLAUDE_RUN_START_FAILED') }, error, 'CLAUDE_RUN_START_FAILED');
325
340
  }
326
341
  }
327
342
  }
@@ -3,7 +3,7 @@ import { AbstractRunner } from './abstract-runner.js';
3
3
  import { claudeDefaultModel, declaredRunnerProfiles } from '../runner-profiles.js';
4
4
  import { withClaudePlanTag } from '../runner-command-engine.js';
5
5
  import { discoverNativeSessions, readNativeSession, readClaudeNativeContextUsage } from '../native-session-history.js';
6
- import { claudeDiscoveredSession, deduplicateDirectSessions, externalResumeFailureDetail, isNativeHistory, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
6
+ import { claudeDiscoveredSession, deduplicateDirectSessions, isNativeHistory, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
7
7
  export class ClaudeCodeRunner extends AbstractRunner {
8
8
  client;
9
9
  environment;
@@ -102,11 +102,10 @@ export class ClaudeCodeRunner extends AbstractRunner {
102
102
  context.rememberManagedNative(external.externalSessionId, configured.id);
103
103
  return { session: configured, activity: 'IDLE' };
104
104
  }
105
- catch (error) {
105
+ catch {
106
106
  return {
107
107
  failureCode: 'RUNNER_RESUME_FAILED',
108
- messageFailureCode: 'SESSION_RESUME_REJECTED',
109
- failureDetail: externalResumeFailureDetail(error)
108
+ messageFailureCode: 'SESSION_RESUME_REJECTED'
110
109
  };
111
110
  }
112
111
  }
@@ -1,7 +1,7 @@
1
1
  import type { NodeAgentSession, NodeConversationItem, NodeConversationTurn, NodeMessageAttachmentInput } from '../../database.js';
2
2
  export type PublicConversationItem = {
3
3
  readonly itemId: string;
4
- readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'unknown';
4
+ readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
5
5
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'DECLINED';
6
6
  readonly payload: Record<string, unknown>;
7
7
  readonly merge?: boolean;
@@ -32,8 +32,8 @@ export declare function mergeCodexCustomToolItems(invocation: NodeConversationIt
32
32
  export declare function coalesceImageGenerationItems(items: readonly NodeConversationItem[]): readonly NodeConversationItem[];
33
33
  export declare function codexPlanText(value: Record<string, unknown>): string | undefined;
34
34
  /**
35
- * Claude 标签式 Plan Mode 的最终计划以 `<proposed_plan>` 完整包裹输出;
36
- * 未闭合或缺失标签的文本保持普通 assistant 消息。
35
+ * A final Claude tagged Plan Mode plan is fully wrapped in `<proposed_plan>` tags;
36
+ * text with missing or unclosed tags remains a regular assistant message.
37
37
  */
38
38
  export declare function codexOfficialItem(session: NodeAgentSession, nativeTurnId: string, value: unknown, turnSequence: number, itemSequence: number, startedAt: number | null, completedAt: number | null): NodeConversationItem | undefined;
39
39
  export declare function codexLiveConversationItem(value: Record<string, unknown>, completed: boolean, imageAttachments?: readonly Record<string, unknown>[]): PublicConversationItem | undefined;
@@ -83,6 +83,7 @@ export function codexOfficialTurn(session, value, sequence, managedTurn) {
83
83
  const consumedOfficialIds = new Set();
84
84
  const officialUser = officialItems.find((item) => item.kind === 'user_message');
85
85
  const officialAssistants = officialItems.filter((item) => item.kind === 'assistant_message');
86
+ const officialContextCompactions = officialItems.filter((item) => item.kind === 'context_compaction');
86
87
  const officialItemMarker = `:official:${value.id}:`;
87
88
  const officialByNativeId = new Map(officialItems.flatMap((item) => {
88
89
  const markerIndex = item.id.lastIndexOf(officialItemMarker);
@@ -95,6 +96,8 @@ export function codexOfficialTurn(session, value, sequence, managedTurn) {
95
96
  let replacement;
96
97
  if (managedItem.kind === 'user_message')
97
98
  replacement = officialUser;
99
+ else if (managedItem.kind === 'context_compaction')
100
+ replacement = officialContextCompactions.find((item) => !consumedOfficialIds.has(item.id));
98
101
  else {
99
102
  const nativeId = conversationItemNativeId(managedItem.id);
100
103
  if (nativeId !== undefined)
@@ -201,6 +204,11 @@ export function codexOfficialItems(session, nativeTurnId, values, turnSequence,
201
204
  const item = codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt);
202
205
  if (item === undefined)
203
206
  continue;
207
+ // Codex persists one compaction as both `compacted` state and a
208
+ // `context_compacted` event. App Server normalizes both records to
209
+ // consecutive `contextCompaction` items without preserving their source.
210
+ if (item.kind === 'context_compaction' && items.at(-1)?.kind === 'context_compaction')
211
+ continue;
204
212
  const key = codexPairedToolCallId(value);
205
213
  if (key === undefined) {
206
214
  items.push(item);
@@ -295,8 +303,8 @@ export function codexPlanText(value) {
295
303
  return plan.length > 0 ? plan : undefined;
296
304
  }
297
305
  /**
298
- * Claude 标签式 Plan Mode 的最终计划以 `<proposed_plan>` 完整包裹输出;
299
- * 未闭合或缺失标签的文本保持普通 assistant 消息。
306
+ * A final Claude tagged Plan Mode plan is fully wrapped in `<proposed_plan>` tags;
307
+ * text with missing or unclosed tags remains a regular assistant message.
300
308
  */
301
309
  export function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
302
310
  if (!isPlainRecord(value) || typeof value.type !== 'string')
@@ -361,7 +369,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
361
369
  payload: {
362
370
  command: typeof value.command === 'string'
363
371
  ? compactRunnerText(value.command, 10_000)
364
- : '(未提供命令)',
372
+ : '(No command provided)',
365
373
  cwd: typeof value.cwd === 'string' ? compactRunnerText(value.cwd, 4_096) : null,
366
374
  outputPreview: compactRunnerText(output, 10_000),
367
375
  outputRef: null,
@@ -415,7 +423,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
415
423
  namespace: 'codex',
416
424
  toolName: 'imagegen',
417
425
  inputSummary: typeof value.revisedPrompt === 'string' ? value.revisedPrompt : null,
418
- outputSummary: value.status === 'completed' ? '图片生成完成' : '图片生成失败',
426
+ outputSummary: value.status === 'completed' ? 'Image generation completed' : 'Image generation failed',
419
427
  progressLabel: null,
420
428
  errorCode: value.status === 'failed' ? 'IMAGE_GENERATION_FAILED' : null,
421
429
  truncated: false
@@ -456,7 +464,11 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
456
464
  return {
457
465
  ...base,
458
466
  kind: 'context_compaction',
459
- payload: { phase: 'COMPLETED', summary: '上下文已整理' }
467
+ payload: {
468
+ phase: 'COMPLETED',
469
+ summary: 'Context compacted',
470
+ summaryCode: 'CONTEXT_COMPACTED'
471
+ }
460
472
  };
461
473
  return codexUnknownOfficialItem(base, value);
462
474
  }
@@ -501,7 +513,7 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
501
513
  payload: {
502
514
  command: typeof value.command === 'string'
503
515
  ? compactRunnerText(value.command, 10_000)
504
- : '(未提供命令)',
516
+ : '(No command provided)',
505
517
  cwd: typeof value.cwd === 'string' ? compactRunnerText(value.cwd, 4_096) : null,
506
518
  outputPreview: compactRunnerText(output, 10_000),
507
519
  outputRef: null,
@@ -550,7 +562,8 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
550
562
  title: 'Web Search',
551
563
  inputSummary: typeof value.query === 'string' ? compactRunnerText(value.query, 10_000) : null,
552
564
  outputSummary: null,
553
- progressLabel: completed ? null : '正在搜索',
565
+ progressLabel: completed ? null : 'Searching',
566
+ progressLabelCode: completed ? null : 'SEARCHING',
554
567
  errorCode: null,
555
568
  truncated: false
556
569
  }
@@ -575,8 +588,10 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
575
588
  inputSummary: typeof value.revisedPrompt === 'string'
576
589
  ? compactRunnerText(value.revisedPrompt, 10_000)
577
590
  : null,
578
- outputSummary: completed ? '图片生成完成' : null,
579
- progressLabel: completed ? null : '正在生成图片',
591
+ outputSummary: completed ? 'Image generation completed' : null,
592
+ outputSummaryCode: completed ? 'IMAGE_GENERATION_COMPLETED' : null,
593
+ progressLabel: completed ? null : 'Generating image',
594
+ progressLabelCode: completed ? null : 'GENERATING_IMAGE',
580
595
  errorCode: value.status === 'failed' ? 'IMAGE_GENERATION_FAILED' : null,
581
596
  truncated: false,
582
597
  ...(imageAttachments.length === 0 ? {} : { attachments: imageAttachments })
@@ -704,7 +719,9 @@ export function codexImageAttachments(value) {
704
719
  totalBytes + Buffer.byteLength(dataBase64, 'base64') <= MAX_COMPOSER_ATTACHMENTS_BYTES) {
705
720
  totalBytes += Buffer.byteLength(dataBase64, 'base64');
706
721
  found.push({
707
- name: typeof current.name === 'string' ? current.name : `生成图片-${found.length + 1}.png`,
722
+ name: typeof current.name === 'string'
723
+ ? current.name
724
+ : `generated-image-${found.length + 1}.png`,
708
725
  mime,
709
726
  dataBase64
710
727
  });
@@ -802,10 +819,14 @@ export function codexToolOutput(value) {
802
819
  return [block.content];
803
820
  return [];
804
821
  });
805
- return text.length > 0 ? text.join('') : value.some(isCodexImageBlock) ? '图片已生成' : value;
822
+ return text.length > 0
823
+ ? text.join('')
824
+ : value.some(isCodexImageBlock)
825
+ ? 'Image generated'
826
+ : value;
806
827
  }
807
828
  export function codexVisibleToolText(value) {
808
- return value.replace(/(["']image_url["']\s*:\s*["'])data:image\/[^"']+(["'])/g, '$1[图片内容已提取]$2');
829
+ return value.replace(/(["']image_url["']\s*:\s*["'])data:image\/[^"']+(["'])/g, '$1[Image content extracted]$2');
809
830
  }
810
831
  export function isCodexImageBlock(value) {
811
832
  if (!isPlainRecord(value))
@@ -44,14 +44,13 @@ export interface CodexManagedRunHost<TAttachment> {
44
44
  }): void;
45
45
  emitItem(runId: string, item: {
46
46
  readonly itemId: string;
47
- readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'unknown';
47
+ readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
48
48
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'DECLINED';
49
49
  readonly payload: Record<string, unknown>;
50
50
  readonly merge?: boolean;
51
51
  readonly append?: 'sections' | 'outputPreview' | 'outputSummary';
52
52
  }): void;
53
53
  appendImages(runId: string, images: readonly NodeMessageAttachmentInput[]): readonly Record<string, unknown>[];
54
- captureSnapshot(runId: string, cwd: string): Promise<void>;
55
54
  syncTitle(sessionId: string, nativeSessionId: string, cwd: string): Promise<void>;
56
55
  commandState(sessionId: string, commandId: string): RunnerCommandState | undefined;
57
56
  setCommandState(sessionId: string, state: RunnerCommandState): void;
@@ -76,5 +75,6 @@ export declare class CodexManagedRunController<TAttachment> {
76
75
  private requestUserInput;
77
76
  start(envelope: NodeEnvelope): void;
78
77
  private run;
78
+ private emitError;
79
79
  }
80
80
  export {};