@myagentroam/node 0.9.0 → 0.9.2

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 (50) hide show
  1. package/dist/connector.d.ts +2 -0
  2. package/dist/connector.js +38 -12
  3. package/dist/native-session-history.js +72 -14
  4. package/dist/runner/abstract-runner.d.ts +1 -1
  5. package/dist/runner/abstract-runner.js +2 -2
  6. package/dist/runner/claude/managed-run-controller.js +17 -7
  7. package/dist/runner/claude-code-runner.js +3 -4
  8. package/dist/runner/codex/conversation-parser.d.ts +2 -2
  9. package/dist/runner/codex/conversation-parser.js +30 -12
  10. package/dist/runner/codex/managed-run-controller.js +7 -6
  11. package/dist/runner/codex-runner.d.ts +1 -1
  12. package/dist/runner/codex-runner.js +10 -8
  13. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  14. package/dist/runner/opencode/conversation-parser.js +15 -15
  15. package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
  16. package/dist/runner/opencode/managed-run-controller.js +26 -12
  17. package/dist/runner-command-engine.js +1 -1
  18. package/dist/runner-profiles.js +25 -17
  19. package/dist/runner-usage.js +5 -5
  20. package/dist/service/conversation-history-service.js +55 -24
  21. package/dist/service/conversation-segment-service.d.ts +20 -0
  22. package/dist/service/conversation-segment-service.js +155 -0
  23. package/dist/service/mcp-installation-verifier.js +1 -1
  24. package/dist/service/native-session-watch-service.d.ts +1 -0
  25. package/dist/service/native-session-watch-service.js +21 -4
  26. package/dist/service/node-request-service.js +2 -1
  27. package/dist/service/run-attachment-service.d.ts +3 -2
  28. package/dist/service/run-attachment-service.js +21 -11
  29. package/dist/service/run-event-service.d.ts +1 -0
  30. package/dist/service/run-event-service.js +8 -2
  31. package/dist/service/session-catalog-service.js +4 -4
  32. package/dist/service/session-presentation-service.d.ts +1 -0
  33. package/dist/service/session-presentation-service.js +8 -1
  34. package/dist/service/session-query-service.d.ts +4 -0
  35. package/dist/service/session-query-service.js +6 -1
  36. package/dist/service/skill-directory-service.js +3 -26
  37. package/dist/service/skill-install-service.js +4 -74
  38. package/dist/service/skill-node-operation-service.js +2 -1
  39. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  40. package/dist/service/workspace-git-exclude-service.js +119 -0
  41. package/dist/service/workspace-queue-workbench-service.js +3 -2
  42. package/dist/service/workspace-service.js +2 -0
  43. package/dist/supervisor.js +2 -1
  44. package/dist/terminal.js +1 -1
  45. package/dist/util/node-operation-parsers.js +2 -2
  46. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  47. package/dist/util/runner-native-session-parsers.js +5 -14
  48. package/dist/util/safe-error.d.ts +2 -0
  49. package/dist/util/safe-error.js +5 -0
  50. package/package.json +2 -2
@@ -27,6 +27,7 @@ export declare class NodeConnector {
27
27
  private readonly sessionIdentityService;
28
28
  private readonly nativeSessionProjectionService;
29
29
  private readonly conversationHistoryService;
30
+ private readonly conversationSegmentService;
30
31
  private readonly runnerInteractionService;
31
32
  private readonly runnerChannelMessages;
32
33
  private readonly managedRunnerSessions;
@@ -78,6 +79,7 @@ export declare class NodeConnector {
78
79
  private readonly handleMessage;
79
80
  private readonly emitRunEvent;
80
81
  private readonly emitConversationItem;
82
+ private readonly emitConversationTurn;
81
83
  private readonly emitWorkbenchEvent;
82
84
  private readonly discoverWorkspaceSessions;
83
85
  private readonly invalidateWorkspaceWatchTopics;
package/dist/connector.js CHANGED
@@ -44,6 +44,7 @@ import { WorkspaceQueueWorkbenchService } from './service/workspace-queue-workbe
44
44
  import { SessionIdentityService } from './service/session-identity-service.js';
45
45
  import { NativeSessionProjectionService } from './service/native-session-projection-service.js';
46
46
  import { ConversationHistoryService, hasManagedActiveRun } from './service/conversation-history-service.js';
47
+ import { ConversationSegmentService, conversationSegmentIdentityForItem, conversationSegments } from './service/conversation-segment-service.js';
47
48
  import { RunnerInteractionService } from './service/runner-interaction-service.js';
48
49
  import { SessionLifecycleService } from './service/session-lifecycle-service.js';
49
50
  import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
@@ -51,7 +52,7 @@ import { SessionMessageService } from './service/session-message-service.js';
51
52
  import { SessionCatalogService } from './service/session-catalog-service.js';
52
53
  import { SessionCommandService } from './service/session-command-service.js';
53
54
  import { SessionPresentationService } from './service/session-presentation-service.js';
54
- import { isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
55
+ import { isPlainRecord, isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
55
56
  export { nodeConnectEndpoint, validatedCapabilities } from './runner/codex/conversation-parser.js';
56
57
  import { NodeControlChannel } from './service/node-control-channel.js';
57
58
  import { NodeControlMessageService } from './service/node-control-message-service.js';
@@ -114,6 +115,7 @@ export class NodeConnector {
114
115
  sessionIdentityService;
115
116
  nativeSessionProjectionService;
116
117
  conversationHistoryService;
118
+ conversationSegmentService;
117
119
  runnerInteractionService;
118
120
  runnerChannelMessages;
119
121
  managedRunnerSessions;
@@ -173,7 +175,12 @@ export class NodeConnector {
173
175
  isBootstrapSession: (sessionId) => this.sessionIdentityService.bootstrapIds.has(sessionId),
174
176
  presentSession: (session) => this.sessionPresentationService.present(session),
175
177
  rejectBootstrap: (sessionId, code) => this.sessionIdentityService.reject(sessionId, code),
176
- emitWorkbench: (category, payload) => this.emitWorkbenchEvent(category, payload),
178
+ emitWorkbench: (category, payload) => {
179
+ if (category === 'conversation' && isPlainRecord(payload) && 'turn' in payload)
180
+ this.emitConversationTurn(payload.turn);
181
+ else
182
+ this.emitWorkbenchEvent(category, payload);
183
+ },
177
184
  cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
178
185
  expireImages: (runId) => this.runAttachmentService.expire(runId),
179
186
  scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
@@ -190,8 +197,9 @@ export class NodeConnector {
190
197
  refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
191
198
  present: (session) => this.sessionPresentationService.present(session),
192
199
  readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
200
+ projectInitialPage: (session, page, _unit, limit) => this.conversationSegmentService.projectInitial(session, page, limit),
193
201
  emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
194
- emitTurn: (turn) => this.emitWorkbenchEvent('conversation', { turn })
202
+ emitTurn: (turn) => this.emitConversationTurn(turn)
195
203
  });
196
204
  constructor(options = {}) {
197
205
  this.config = options.config;
@@ -230,7 +238,7 @@ export class NodeConnector {
230
238
  this.fakeRunner =
231
239
  options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
232
240
  this.fakeScript = options.fakeScript;
233
- this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoot(session));
241
+ this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session));
234
242
  this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory);
235
243
  this.codexRunController = new CodexManagedRunController(this.codexClient, {
236
244
  available: () => this.capabilities.codex.available,
@@ -252,7 +260,7 @@ export class NodeConnector {
252
260
  cancel: (runId, threadId, turnId, cwd, status) => this.runnerInteractionService.cancelCodexTurn(runId, threadId, turnId, cwd, status),
253
261
  createApproval: (input) => this.runtime.createApproval(input),
254
262
  publishApproval: (input) => this.runEventBridge.publishApproval(input),
255
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
263
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
256
264
  appendImages: (runId, images) => this.runAttachmentService.append(runId, images.filter(isRunnerImageAttachment)),
257
265
  captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
258
266
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
@@ -273,8 +281,9 @@ export class NodeConnector {
273
281
  intercepted: () => this.fakeRunner !== undefined,
274
282
  active: this.runState.active,
275
283
  parseAttachments: parseRunnerImageAttachments,
284
+ persistAttachments: (cwd, runId, attachments) => this.runAttachmentService.persistFiles(cwd, `${runId}-opencode-images`, attachments),
276
285
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
277
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
286
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
278
287
  adopt: (input) => this.runtime.adoptRun(input),
279
288
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
280
289
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
@@ -306,7 +315,7 @@ export class NodeConnector {
306
315
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
307
316
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
308
317
  send: (type, payload) => this.send(type, payload),
309
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
318
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
310
319
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
311
320
  setChannelToken: (sessionId, token) => this.runtime.setChannelToken(sessionId, token),
312
321
  commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
@@ -330,7 +339,7 @@ export class NodeConnector {
330
339
  claude: () => this.claudeClient,
331
340
  runners: this.runners,
332
341
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
333
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
342
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
334
343
  captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
335
344
  approvalTimeoutMs: this.claudeApprovalTimeoutMs,
336
345
  publishApproval: (input) => this.runEventBridge.publishApproval(input)
@@ -368,6 +377,7 @@ export class NodeConnector {
368
377
  hasManagedActiveRun: (session) => hasManagedActiveRun(this.runtime, session),
369
378
  setExternalActivity: (sessionId, activity) => this.sessionState.externalActivity.set(sessionId, activity)
370
379
  });
380
+ this.conversationSegmentService = new ConversationSegmentService((session, input) => this.conversationHistoryService.read(session, input));
371
381
  this.sessionContextService = new SessionContextService({
372
382
  runners: this.runners,
373
383
  session: (sessionId) => this.runtime.getAgentSession(sessionId),
@@ -392,6 +402,7 @@ export class NodeConnector {
392
402
  resolve: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId),
393
403
  present: (session) => this.sessionPresentationService.present(session),
394
404
  history: (session, input) => this.conversationHistoryService.read(session, input),
405
+ segments: (session, input) => this.conversationSegmentService.read(session, input),
395
406
  context: this.sessionContextService,
396
407
  discover: (runner, workspaceId) => this.nativeSessionProjectionService.discover(runner, workspaceId),
397
408
  workspaceIdForCwd: (cwd) => this.database?.listWorkspaces().find((candidate) => candidate.path === cwd)?.id
@@ -401,7 +412,7 @@ export class NodeConnector {
401
412
  emit: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
402
413
  clearImages: (runId) => this.runAttachmentService.clear(runId),
403
414
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
404
- emitConversation: (turn) => this.emitWorkbenchEvent('conversation', { turn }),
415
+ emitConversation: (turn) => this.emitConversationTurn(turn),
405
416
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
406
417
  stopped: () => this.stopped,
407
418
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
@@ -428,7 +439,7 @@ export class NodeConnector {
428
439
  emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
429
440
  emitRun: (run) => this.emitWorkbenchEvent('run', { run }),
430
441
  emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
431
- emitTurn: (turn) => this.emitWorkbenchEvent('conversation', { turn })
442
+ emitTurn: (turn) => this.emitConversationTurn(turn)
432
443
  });
433
444
  this.sessionLifecycleService = new SessionLifecycleService({
434
445
  database: () => {
@@ -596,7 +607,7 @@ export class NodeConnector {
596
607
  this.terminalChannel.disconnect();
597
608
  if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
598
609
  nodeLog('node.connection.replaced', {
599
- message: '当前 Node 已被同一身份的新实例接管,请检查是否启动了重复实例。'
610
+ message: 'This Node connection was replaced by a newer instance with the same identity. Check for duplicate processes.'
600
611
  });
601
612
  if (code === 4003 || code === 1001)
602
613
  void this.terminalManager.shutdown();
@@ -781,7 +792,22 @@ export class NodeConnector {
781
792
  /** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
782
793
  handleMessage = (raw) => this.controlMessages.handle(raw);
783
794
  emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
784
- emitConversationItem = (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item);
795
+ emitConversationItem = (runId, item) => {
796
+ this.runEventService.flushPendingText(runId);
797
+ const itemId = isPlainRecord(item) && typeof item.itemId === 'string' ? item.itemId : undefined;
798
+ const itemKind = isPlainRecord(item) && typeof item.kind === 'string' ? item.kind : undefined;
799
+ const turn = this.runtime.conversationTurnForRun(runId);
800
+ const segment = itemId === undefined || turn === undefined
801
+ ? undefined
802
+ : conversationSegmentIdentityForItem(turn, itemId, itemKind);
803
+ this.runEventBridge.emitRun(runId, 'conversation.item', segment === undefined || !isPlainRecord(item)
804
+ ? item
805
+ : { ...item, segmentId: segment.id, segmentIndex: segment.index });
806
+ };
807
+ emitConversationTurn = (turn) => {
808
+ const segments = isPlainRecord(turn) && Array.isArray(turn.items) ? conversationSegments(turn) : [];
809
+ this.emitWorkbenchEvent('conversation', { turn, segments });
810
+ };
785
811
  emitWorkbenchEvent = (category, payload) => this.runEventBridge.emitWorkbench(category, payload);
786
812
  discoverWorkspaceSessions = (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId);
787
813
  invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
@@ -286,7 +286,7 @@ async function readTranscript(path, runner, byteLimit) {
286
286
  items.push({
287
287
  kind: 'tool_call',
288
288
  toolUseId: result.toolUseId,
289
- toolName: '工具',
289
+ toolName: 'Tool',
290
290
  input: null,
291
291
  inputSummary: null,
292
292
  inputTruncated: false,
@@ -301,7 +301,7 @@ async function readTranscript(path, runner, byteLimit) {
301
301
  if (cwd === undefined)
302
302
  return undefined;
303
303
  const title = items.find((item) => item.kind === 'message' && item.role === 'USER')?.text;
304
- // 标签式 Plan Mode 的指令标签不构成会话标题。
304
+ // Tagged Plan Mode instruction labels do not form a session title.
305
305
  const strippedTitle = title === undefined ? undefined : stripClaudePlanTag(title);
306
306
  return {
307
307
  runner,
@@ -591,7 +591,7 @@ function codexGeneratedImageEvent(record) {
591
591
  input: null,
592
592
  inputSummary: null,
593
593
  inputTruncated: false,
594
- outputSummary: payload['status'] === 'completed' ? '图片生成完成' : '图片生成失败',
594
+ outputSummary: payload['status'] === 'completed' ? 'Image generation completed' : 'Image generation failed',
595
595
  outputTruncated: false,
596
596
  imageAttachments: [{ name: basename(localPath), mime: imageMime(localPath), localPath }],
597
597
  failed: payload['status'] !== 'completed',
@@ -620,7 +620,7 @@ function codexVisibleToolText(value) {
620
620
  const start = value.indexOf(marker, cursor);
621
621
  if (start < 0)
622
622
  return visible + value.slice(cursor);
623
- visible += `${value.slice(cursor, start)}[图片内容已过滤]`;
623
+ visible += `${value.slice(cursor, start)}[Image content filtered]`;
624
624
  const doubleQuote = value.indexOf('"', start);
625
625
  const singleQuote = value.indexOf("'", start);
626
626
  const end = doubleQuote < 0
@@ -636,14 +636,20 @@ function codexVisibleToolText(value) {
636
636
  }
637
637
  /** Extract only image placeholder metadata from native Codex output. */
638
638
  function codexImagePlaceholders(value) {
639
- const paths = [];
639
+ const attachments = [];
640
640
  let visited = 0;
641
641
  const visit = (current, depth) => {
642
- if (paths.length >= 5 || visited >= 128 || depth > 5 || current == null)
642
+ if (attachments.length >= 5 || visited >= 128 || depth > 5 || current == null)
643
643
  return;
644
644
  visited += 1;
645
645
  if (typeof current === 'string') {
646
+ const paths = [];
646
647
  collectGeneratedImagePaths(current, paths);
648
+ for (const localPath of paths) {
649
+ if (attachments.some((attachment) => attachment.localPath === localPath))
650
+ continue;
651
+ attachments.push({ name: basename(localPath), mime: imageMime(localPath), localPath });
652
+ }
647
653
  return;
648
654
  }
649
655
  if (Array.isArray(current)) {
@@ -653,15 +659,28 @@ function codexImagePlaceholders(value) {
653
659
  }
654
660
  if (!isRecord(current))
655
661
  return;
662
+ const type = typeof current['type'] === 'string' ? current['type'] : '';
663
+ if (type === 'input_text' && typeof current['text'] === 'string') {
664
+ const mime = /["']image_url["']\s*:\s*["']data:(image\/[^;,"']+)[;,]/.exec(current['text'])?.[1];
665
+ if (mime !== undefined)
666
+ attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
667
+ }
668
+ const imageUrl = isRecord(current['image_url'])
669
+ ? firstString(current['image_url'], ['url'])
670
+ : typeof current['image_url'] === 'string'
671
+ ? current['image_url']
672
+ : undefined;
673
+ const data = typeof current['data'] === 'string' ? current['data'] : imageUrl;
674
+ if ((type === 'image' || type === 'input_image' || type === 'output_image') &&
675
+ data?.startsWith('data:image/')) {
676
+ const mime = /^data:([^;,]+)[;,]/.exec(data)?.[1] ?? 'image/png';
677
+ attachments.push({ name: `image-${attachments.length + 1}.${imageExtension(mime)}`, mime });
678
+ }
656
679
  for (const child of Object.values(current))
657
680
  visit(child, depth + 1);
658
681
  };
659
682
  visit(value, 0);
660
- return paths.map((localPath) => ({
661
- name: basename(localPath),
662
- mime: imageMime(localPath),
663
- localPath
664
- }));
683
+ return attachments;
665
684
  }
666
685
  function collectGeneratedImagePaths(value, paths) {
667
686
  const marker = '/.codex/generated_images/';
@@ -690,15 +709,54 @@ function imageMime(path) {
690
709
  return 'image/webp';
691
710
  return 'image/png';
692
711
  }
712
+ function imageExtension(mime) {
713
+ if (mime === 'image/jpeg')
714
+ return 'jpg';
715
+ if (mime === 'image/webp')
716
+ return 'webp';
717
+ if (mime === 'image/gif')
718
+ return 'gif';
719
+ return 'png';
720
+ }
693
721
  function withToolResult(call, result) {
722
+ const viewedImagePath = codexViewedImagePath(call);
723
+ const imageAttachments = result.imageAttachments?.map((attachment) => attachment.localPath !== undefined || viewedImagePath === undefined
724
+ ? attachment
725
+ : {
726
+ name: basename(viewedImagePath),
727
+ mime: imageMime(viewedImagePath),
728
+ localPath: viewedImagePath
729
+ });
694
730
  return {
695
731
  ...call,
696
732
  outputSummary: result.output,
697
733
  outputTruncated: result.truncated,
698
- ...(result.imageAttachments === undefined ? {} : { imageAttachments: result.imageAttachments }),
734
+ ...(imageAttachments === undefined ? {} : { imageAttachments }),
699
735
  failed: result.failed
700
736
  };
701
737
  }
738
+ function codexViewedImagePath(call) {
739
+ const toolName = call.toolName.toLowerCase();
740
+ if (toolName === 'view_image' || toolName === 'imageview') {
741
+ return isRecord(call.input) && typeof call.input['path'] === 'string'
742
+ ? call.input['path']
743
+ : undefined;
744
+ }
745
+ if (toolName !== 'exec' || typeof call.input !== 'string')
746
+ return undefined;
747
+ if (!/tools\.view_image\s*\(/.test(call.input))
748
+ return undefined;
749
+ const encodedPath = /["']path["']\s*:\s*("(?:\\.|[^"\\])*")/.exec(call.input)?.[1];
750
+ if (encodedPath === undefined)
751
+ return undefined;
752
+ try {
753
+ const path = JSON.parse(encodedPath);
754
+ return typeof path === 'string' ? path : undefined;
755
+ }
756
+ catch {
757
+ return undefined;
758
+ }
759
+ }
702
760
  function isInjectedContextPrompt(text) {
703
761
  const normalized = text.replace(/\s+/g, ' ').trim();
704
762
  if (normalized.startsWith('<codex_internal_context'))
@@ -776,7 +834,7 @@ function compactValue(value, limit) {
776
834
  text = JSON.stringify(value);
777
835
  }
778
836
  catch {
779
- return { text: '(无法显示)', truncated: false };
837
+ return { text: '(Unable to display)', truncated: false };
780
838
  }
781
839
  }
782
840
  return {
@@ -812,7 +870,7 @@ function nestedString(value, keys) {
812
870
  function compact(value) {
813
871
  return value.replace(/\s+/g, ' ').trim().slice(0, 16_000);
814
872
  }
815
- /** 对话正文可能是 Markdown,不能像标题一样压缩内部空白。 */
873
+ /** Conversation content can be Markdown, so its internal whitespace must not be collapsed like a title. */
816
874
  function messageText(value) {
817
875
  return value.trim().slice(0, 16_000);
818
876
  }
@@ -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;
@@ -4,6 +4,7 @@ import { boundedConversationItemId, claudePlanText, compactRunnerText, compactRu
4
4
  import { isTerminalRunStatus } from '../../util/node-operation-parsers.js';
5
5
  import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
6
6
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
7
+ import { safeErrorCode } from '../../util/safe-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({
@@ -255,14 +256,23 @@ export class ClaudeManagedRunController {
255
256
  const kind = toolKinds.get(record.tool_use_id);
256
257
  if (kind === 'tool_call') {
257
258
  const elapsed = typeof record.elapsed_time_seconds === 'number'
258
- ? `(${Math.max(0, Math.round(record.elapsed_time_seconds))} 秒)`
259
+ ? ` (${Math.max(0, Math.round(record.elapsed_time_seconds))} seconds)`
259
260
  : '';
260
261
  this.host.emitItem(runId, {
261
262
  itemId: boundedConversationItemId('claude-tool', record.tool_use_id),
262
263
  kind,
263
264
  status: 'IN_PROGRESS',
264
265
  payload: {
265
- progressLabel: compactRunnerText(`${typeof record.tool_name === 'string' ? record.tool_name : '工具'}正在执行${elapsed}`, 500)
266
+ progressLabel: compactRunnerText(`${typeof record.tool_name === 'string' ? record.tool_name : 'Tool'} is running${elapsed}`, 500),
267
+ progressLabelCode: typeof record.elapsed_time_seconds === 'number'
268
+ ? 'TOOL_RUNNING_WITH_DURATION'
269
+ : 'TOOL_RUNNING',
270
+ progressLabelParameters: {
271
+ tool: typeof record.tool_name === 'string' ? record.tool_name : 'Tool',
272
+ seconds: typeof record.elapsed_time_seconds === 'number'
273
+ ? Math.max(0, Math.round(record.elapsed_time_seconds))
274
+ : 0
275
+ }
266
276
  },
267
277
  merge: true
268
278
  });
@@ -315,12 +325,12 @@ export class ClaudeManagedRunController {
315
325
  this.host.send('session.channel.ready', { sessionId, token: gate.token });
316
326
  this.host.emit(runId, 'run.running', {}, 'RUNNING');
317
327
  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'
328
+ code: safeErrorCode(error, 'CLAUDE_RUN_FAILED')
319
329
  }));
320
330
  }
321
331
  catch (error) {
322
332
  finish('FAILED', 'run.failed', {
323
- code: error instanceof Error ? error.message : 'CLAUDE_RUN_START_FAILED'
333
+ code: safeErrorCode(error, 'CLAUDE_RUN_START_FAILED')
324
334
  });
325
335
  }
326
336
  }
@@ -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
  }
@@ -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;
@@ -201,6 +201,11 @@ export function codexOfficialItems(session, nativeTurnId, values, turnSequence,
201
201
  const item = codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt);
202
202
  if (item === undefined)
203
203
  continue;
204
+ // Codex persists one compaction as both `compacted` state and a
205
+ // `context_compacted` event. App Server normalizes both records to
206
+ // consecutive `contextCompaction` items without preserving their source.
207
+ if (item.kind === 'context_compaction' && items.at(-1)?.kind === 'context_compaction')
208
+ continue;
204
209
  const key = codexPairedToolCallId(value);
205
210
  if (key === undefined) {
206
211
  items.push(item);
@@ -295,8 +300,8 @@ export function codexPlanText(value) {
295
300
  return plan.length > 0 ? plan : undefined;
296
301
  }
297
302
  /**
298
- * Claude 标签式 Plan Mode 的最终计划以 `<proposed_plan>` 完整包裹输出;
299
- * 未闭合或缺失标签的文本保持普通 assistant 消息。
303
+ * A final Claude tagged Plan Mode plan is fully wrapped in `<proposed_plan>` tags;
304
+ * text with missing or unclosed tags remains a regular assistant message.
300
305
  */
301
306
  export function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
302
307
  if (!isPlainRecord(value) || typeof value.type !== 'string')
@@ -361,7 +366,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
361
366
  payload: {
362
367
  command: typeof value.command === 'string'
363
368
  ? compactRunnerText(value.command, 10_000)
364
- : '(未提供命令)',
369
+ : '(No command provided)',
365
370
  cwd: typeof value.cwd === 'string' ? compactRunnerText(value.cwd, 4_096) : null,
366
371
  outputPreview: compactRunnerText(output, 10_000),
367
372
  outputRef: null,
@@ -415,7 +420,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
415
420
  namespace: 'codex',
416
421
  toolName: 'imagegen',
417
422
  inputSummary: typeof value.revisedPrompt === 'string' ? value.revisedPrompt : null,
418
- outputSummary: value.status === 'completed' ? '图片生成完成' : '图片生成失败',
423
+ outputSummary: value.status === 'completed' ? 'Image generation completed' : 'Image generation failed',
419
424
  progressLabel: null,
420
425
  errorCode: value.status === 'failed' ? 'IMAGE_GENERATION_FAILED' : null,
421
426
  truncated: false
@@ -456,7 +461,11 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
456
461
  return {
457
462
  ...base,
458
463
  kind: 'context_compaction',
459
- payload: { phase: 'COMPLETED', summary: '上下文已整理' }
464
+ payload: {
465
+ phase: 'COMPLETED',
466
+ summary: 'Context compacted',
467
+ summaryCode: 'CONTEXT_COMPACTED'
468
+ }
460
469
  };
461
470
  return codexUnknownOfficialItem(base, value);
462
471
  }
@@ -501,7 +510,7 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
501
510
  payload: {
502
511
  command: typeof value.command === 'string'
503
512
  ? compactRunnerText(value.command, 10_000)
504
- : '(未提供命令)',
513
+ : '(No command provided)',
505
514
  cwd: typeof value.cwd === 'string' ? compactRunnerText(value.cwd, 4_096) : null,
506
515
  outputPreview: compactRunnerText(output, 10_000),
507
516
  outputRef: null,
@@ -550,7 +559,8 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
550
559
  title: 'Web Search',
551
560
  inputSummary: typeof value.query === 'string' ? compactRunnerText(value.query, 10_000) : null,
552
561
  outputSummary: null,
553
- progressLabel: completed ? null : '正在搜索',
562
+ progressLabel: completed ? null : 'Searching',
563
+ progressLabelCode: completed ? null : 'SEARCHING',
554
564
  errorCode: null,
555
565
  truncated: false
556
566
  }
@@ -575,8 +585,10 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
575
585
  inputSummary: typeof value.revisedPrompt === 'string'
576
586
  ? compactRunnerText(value.revisedPrompt, 10_000)
577
587
  : null,
578
- outputSummary: completed ? '图片生成完成' : null,
579
- progressLabel: completed ? null : '正在生成图片',
588
+ outputSummary: completed ? 'Image generation completed' : null,
589
+ outputSummaryCode: completed ? 'IMAGE_GENERATION_COMPLETED' : null,
590
+ progressLabel: completed ? null : 'Generating image',
591
+ progressLabelCode: completed ? null : 'GENERATING_IMAGE',
580
592
  errorCode: value.status === 'failed' ? 'IMAGE_GENERATION_FAILED' : null,
581
593
  truncated: false,
582
594
  ...(imageAttachments.length === 0 ? {} : { attachments: imageAttachments })
@@ -704,7 +716,9 @@ export function codexImageAttachments(value) {
704
716
  totalBytes + Buffer.byteLength(dataBase64, 'base64') <= MAX_COMPOSER_ATTACHMENTS_BYTES) {
705
717
  totalBytes += Buffer.byteLength(dataBase64, 'base64');
706
718
  found.push({
707
- name: typeof current.name === 'string' ? current.name : `生成图片-${found.length + 1}.png`,
719
+ name: typeof current.name === 'string'
720
+ ? current.name
721
+ : `generated-image-${found.length + 1}.png`,
708
722
  mime,
709
723
  dataBase64
710
724
  });
@@ -802,10 +816,14 @@ export function codexToolOutput(value) {
802
816
  return [block.content];
803
817
  return [];
804
818
  });
805
- return text.length > 0 ? text.join('') : value.some(isCodexImageBlock) ? '图片已生成' : value;
819
+ return text.length > 0
820
+ ? text.join('')
821
+ : value.some(isCodexImageBlock)
822
+ ? 'Image generated'
823
+ : value;
806
824
  }
807
825
  export function codexVisibleToolText(value) {
808
- return value.replace(/(["']image_url["']\s*:\s*["'])data:image\/[^"']+(["'])/g, '$1[图片内容已提取]$2');
826
+ return value.replace(/(["']image_url["']\s*:\s*["'])data:image\/[^"']+(["'])/g, '$1[Image content extracted]$2');
809
827
  }
810
828
  export function isCodexImageBlock(value) {
811
829
  if (!isPlainRecord(value))
@@ -1,5 +1,6 @@
1
1
  import { extractId } from '../../util/runner-native-session-parsers.js';
2
2
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
3
+ import { safeErrorCode } from '../../util/safe-error.js';
3
4
  import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
4
5
  export class CodexManagedRunController {
5
6
  runner;
@@ -32,10 +33,10 @@ export class CodexManagedRunController {
32
33
  commandId: 'plan',
33
34
  active: true,
34
35
  label: 'Plan Mode',
35
- detail: '后续消息将以协作规划模式运行。',
36
+ detail: 'Subsequent messages run in collaborative Plan Mode.',
36
37
  closable: true,
37
38
  closeInput: '/plan',
38
- continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
39
+ continuation: { label: 'Implement', prompt: 'Implement the plan above.' }
39
40
  });
40
41
  return { command: command.id, states: this.host.commandStates(session.id) };
41
42
  }
@@ -83,7 +84,7 @@ export class CodexManagedRunController {
83
84
  };
84
85
  }
85
86
  catch (error) {
86
- this.host.emit(runId, 'run.failed', { code: error instanceof Error ? error.message : 'CODEX_COMPACT_FAILED' }, 'FAILED');
87
+ this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, 'CODEX_COMPACT_FAILED') }, 'FAILED');
87
88
  this.runner.execution.runs.delete(runId);
88
89
  this.runner.releaseEnvironment(runId);
89
90
  this.host.removeActive(runId);
@@ -283,7 +284,7 @@ export class CodexManagedRunController {
283
284
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
284
285
  }
285
286
  catch (error) {
286
- this.host.emit(runId, 'run.rejected', { code: error instanceof Error ? error.message : 'MESSAGE_ATTACHMENTS_INVALID' }, 'FAILED');
287
+ this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'MESSAGE_ATTACHMENTS_INVALID') }, 'FAILED');
287
288
  return;
288
289
  }
289
290
  this.host.adopt({
@@ -311,7 +312,7 @@ export class CodexManagedRunController {
311
312
  this.runner.prepareEnvironment(secretEnvironment, runId);
312
313
  }
313
314
  catch (error) {
314
- this.host.emit(runId, 'run.rejected', { code: error instanceof Error ? error.message : 'SECRET_ENVIRONMENT_INVALID' }, 'FAILED');
315
+ this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'SECRET_ENVIRONMENT_INVALID') }, 'FAILED');
315
316
  return;
316
317
  }
317
318
  this.host.addActive(runId);
@@ -381,7 +382,7 @@ export class CodexManagedRunController {
381
382
  this.runner.releaseEnvironment(runId);
382
383
  return;
383
384
  }
384
- this.host.emit(runId, 'run.failed', { code: error instanceof Error ? error.message : 'CODEX_RUN_FAILED' }, 'FAILED');
385
+ this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, 'CODEX_RUN_FAILED') }, 'FAILED');
385
386
  this.runner.execution.runs.delete(runId);
386
387
  this.runner.releaseEnvironment(runId);
387
388
  this.host.removeActive(runId);