@myagentroam/node 0.1.8 → 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 (79) hide show
  1. package/dist/capabilities.js +6 -3
  2. package/dist/claude-agent-sdk.d.ts +1 -0
  3. package/dist/claude-agent-sdk.js +8 -1
  4. package/dist/codex-app-server.d.ts +6 -0
  5. package/dist/codex-app-server.js +6 -0
  6. package/dist/connector/node-connector-options.d.ts +1 -0
  7. package/dist/connector.d.ts +2 -0
  8. package/dist/connector.js +43 -14
  9. package/dist/database.d.ts +23 -0
  10. package/dist/database.js +84 -2
  11. package/dist/migrations/v004.d.ts +4 -0
  12. package/dist/migrations/v004.js +35 -0
  13. package/dist/native-session-history.js +72 -14
  14. package/dist/opencode-server.d.ts +1 -0
  15. package/dist/opencode-server.js +12 -0
  16. package/dist/runner/abstract-runner.d.ts +13 -3
  17. package/dist/runner/abstract-runner.js +78 -4
  18. package/dist/runner/claude/managed-run-controller.js +18 -7
  19. package/dist/runner/claude-code-runner.d.ts +1 -0
  20. package/dist/runner/claude-code-runner.js +17 -4
  21. package/dist/runner/codex/conversation-parser.d.ts +2 -2
  22. package/dist/runner/codex/conversation-parser.js +30 -12
  23. package/dist/runner/codex/managed-run-controller.js +11 -8
  24. package/dist/runner/codex-runner.d.ts +17 -3
  25. package/dist/runner/codex-runner.js +225 -12
  26. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  27. package/dist/runner/opencode/conversation-parser.js +15 -15
  28. package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
  29. package/dist/runner/opencode/managed-run-controller.js +36 -12
  30. package/dist/runner/opencode-runner.d.ts +2 -1
  31. package/dist/runner/opencode-runner.js +23 -3
  32. package/dist/runner/runner-registry.d.ts +1 -1
  33. package/dist/runner/runner-registry.js +2 -2
  34. package/dist/runner-command-engine.js +1 -1
  35. package/dist/runner-profiles.js +29 -41
  36. package/dist/runner-usage.js +5 -5
  37. package/dist/runtime-command-detector.d.ts +3 -0
  38. package/dist/runtime-command-detector.js +78 -0
  39. package/dist/service/conversation-history-service.js +55 -24
  40. package/dist/service/conversation-segment-service.d.ts +20 -0
  41. package/dist/service/conversation-segment-service.js +155 -0
  42. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  43. package/dist/service/mcp-installation-verifier.js +87 -0
  44. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  45. package/dist/service/mcp-node-operation-service.js +90 -0
  46. package/dist/service/mcp-package-installer.d.ts +8 -0
  47. package/dist/service/mcp-package-installer.js +136 -0
  48. package/dist/service/native-session-watch-service.d.ts +1 -0
  49. package/dist/service/native-session-watch-service.js +21 -4
  50. package/dist/service/node-request-service.js +2 -1
  51. package/dist/service/run-attachment-service.d.ts +3 -2
  52. package/dist/service/run-attachment-service.js +21 -11
  53. package/dist/service/run-event-service.d.ts +1 -0
  54. package/dist/service/run-event-service.js +8 -2
  55. package/dist/service/runner-service.d.ts +2 -0
  56. package/dist/service/runner-service.js +5 -2
  57. package/dist/service/session-catalog-service.js +4 -4
  58. package/dist/service/session-lifecycle-service.js +7 -4
  59. package/dist/service/session-presentation-service.d.ts +1 -0
  60. package/dist/service/session-presentation-service.js +8 -1
  61. package/dist/service/session-query-service.d.ts +4 -0
  62. package/dist/service/session-query-service.js +6 -1
  63. package/dist/service/skill-directory-service.js +3 -26
  64. package/dist/service/skill-install-service.js +4 -74
  65. package/dist/service/skill-node-operation-service.js +2 -1
  66. package/dist/service/workbench-manifest-service.d.ts +2 -0
  67. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  68. package/dist/service/workspace-git-exclude-service.js +119 -0
  69. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  70. package/dist/service/workspace-queue-workbench-service.js +5 -3
  71. package/dist/service/workspace-service.js +2 -0
  72. package/dist/supervisor.js +2 -1
  73. package/dist/terminal.js +1 -1
  74. package/dist/util/node-operation-parsers.js +2 -2
  75. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  76. package/dist/util/runner-native-session-parsers.js +5 -14
  77. package/dist/util/safe-error.d.ts +2 -0
  78. package/dist/util/safe-error.js +5 -0
  79. package/package.json +2 -2
@@ -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
  }
@@ -16,6 +16,7 @@ export declare class OpenCodeServerClient {
16
16
  start(): Promise<void>;
17
17
  private startServer;
18
18
  providers(directory: string): Promise<OpenCodeProviderCatalog>;
19
+ configureMcp(directory: string, mcp: unknown): Promise<void>;
19
20
  sessions(directory: string, limit?: number): Promise<readonly unknown[]>;
20
21
  sessionStatus(directory: string): Promise<Readonly<Record<string, unknown>>>;
21
22
  createSession(directory: string, title?: string): Promise<unknown>;
@@ -77,6 +77,18 @@ export class OpenCodeServerClient {
77
77
  const client = await this.requireClient();
78
78
  return unwrap(await client.provider.list({ directory }));
79
79
  }
80
+ async configureMcp(directory, mcp) {
81
+ const client = await this.requireClient();
82
+ if (typeof mcp !== 'object' || mcp === null || Array.isArray(mcp))
83
+ return;
84
+ for (const [name, config] of Object.entries(mcp)) {
85
+ await client.mcp.add({
86
+ directory,
87
+ name,
88
+ config: config
89
+ });
90
+ }
91
+ }
80
92
  async sessions(directory, limit = 100) {
81
93
  const client = await this.requireClient();
82
94
  return unwrap(await client.session.list({ directory, limit }));
@@ -21,14 +21,15 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
21
21
  abstract profile(capabilities: NodeCapabilities): RunnerProfile | undefined;
22
22
  refreshProfile(capabilities: NodeCapabilities, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<RunnerProfile | undefined>;
23
23
  abstract available(capabilities: NodeCapabilities): boolean;
24
- abstract defaultConfiguration(workspace?: NodeWorkspace): RunnerDefaultConfiguration;
25
- supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace): boolean;
24
+ abstract defaultConfiguration(workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
25
+ supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
26
26
  protected abstract profileForValidation(): RunnerProfile;
27
27
  abstract resumeExternal(session: NodeAgentSession, context: ExternalResumeContext, force?: boolean): Promise<ExternalResumeResult>;
28
28
  abstract discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
29
29
  abstract readContextUsage(session: NodeAgentSession): Promise<RunnerContextUsage | undefined>;
30
30
  presentSession(session: NodeAgentSession, capabilities: NodeCapabilities, context: RunnerSessionPresentationContext): RunnerSessionPresentation;
31
31
  prepareMessageInput(input: string, collaborationMode: 'default' | 'plan'): string;
32
+ mcpConfiguration(rawInstallations: unknown, secretEnvironment: Readonly<Record<string, string>>): unknown;
32
33
  channelToken(sessionId: string): string | undefined;
33
34
  deliverChannel(sessionId: string, messageId: string, senderId: string, token: string, content: string, notify: (runId: string, messageId: string, delivery: ChannelDelivery) => void): ChannelDelivery;
34
35
  executeGlobalCommand(commandId: string, input: string): unknown;
@@ -52,7 +53,7 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
52
53
  }, managedTurn: (nativeTurnId: string) => NodeConversationTurn | undefined): Promise<RunnerOfficialConversationPage | undefined>;
53
54
  readExternalActivity(session: NodeAgentSession): Promise<SessionActivityState | undefined>;
54
55
  acceptsTruncatedNativeHistory(): boolean;
55
- nativeImageRoot(session: NodeAgentSession): string | undefined;
56
+ nativeImageRoots(session: NodeAgentSession): readonly string[];
56
57
  managedRunForNativeTurn(nativeTurnId: string): string | undefined;
57
58
  abstract stop(): void | Promise<void>;
58
59
  bindControl(control: RunnerControl): void;
@@ -66,6 +67,15 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
66
67
  readonly secretEnvironment: Readonly<Record<string, string>>;
67
68
  }): Promise<unknown>;
68
69
  }
70
+ export interface ResolvedMcpServer {
71
+ readonly name: string;
72
+ readonly transport: 'STDIO' | 'HTTP';
73
+ readonly command?: string;
74
+ readonly args?: readonly string[];
75
+ readonly environment?: Readonly<Record<string, string>>;
76
+ readonly url?: string;
77
+ readonly headers?: Readonly<Record<string, string>>;
78
+ }
69
79
  export type RunnerCancellationIntent = 'CANCEL' | 'INTERRUPT';
70
80
  export interface RunnerControl {
71
81
  start(envelope: NodeEnvelope): void;
@@ -1,3 +1,4 @@
1
+ import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
1
2
  /**
2
3
  * Stable lifecycle boundary for every Node Runner implementation.
3
4
  *
@@ -15,8 +16,8 @@ export class AbstractRunner {
15
16
  void [workspace, environment];
16
17
  return this.profile(capabilities);
17
18
  }
18
- supportsConfiguration(configuration, workspace) {
19
- void workspace;
19
+ supportsConfiguration(configuration, workspace, environment) {
20
+ void [workspace, environment];
20
21
  const profile = this.profileForValidation();
21
22
  const model = profile.models.find((candidate) => candidate.id === configuration.model);
22
23
  return (configuration.runner === this.name &&
@@ -32,6 +33,9 @@ export class AbstractRunner {
32
33
  void collaborationMode;
33
34
  return input;
34
35
  }
36
+ mcpConfiguration(rawInstallations, secretEnvironment) {
37
+ return resolvedMcpServers(rawInstallations, secretEnvironment);
38
+ }
35
39
  channelToken(sessionId) {
36
40
  void sessionId;
37
41
  return undefined;
@@ -102,9 +106,9 @@ export class AbstractRunner {
102
106
  acceptsTruncatedNativeHistory() {
103
107
  return true;
104
108
  }
105
- nativeImageRoot(session) {
109
+ nativeImageRoots(session) {
106
110
  void session;
107
- return undefined;
111
+ return [];
108
112
  }
109
113
  managedRunForNativeTurn(nativeTurnId) {
110
114
  void nativeTurnId;
@@ -143,6 +147,76 @@ export class AbstractRunner {
143
147
  return this.commandControl.execute(session, command, input, context);
144
148
  }
145
149
  }
150
+ function resolvedMcpServers(raw, secrets) {
151
+ if (!Array.isArray(raw))
152
+ return [];
153
+ const result = [];
154
+ for (const value of raw) {
155
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
156
+ continue;
157
+ const record = value;
158
+ if (typeof record['serverName'] !== 'string' || record['enabled'] !== true)
159
+ continue;
160
+ const runtime = mcpRuntimeDescriptorSchema.safeParse(record['runtime']);
161
+ if (!runtime.success)
162
+ continue;
163
+ const configuration = isRecord(record['configuration']) ? record['configuration'] : {};
164
+ const resolveReferences = (candidate) => {
165
+ if (!isRecord(candidate))
166
+ return {};
167
+ const result = {};
168
+ for (const [name, reference] of Object.entries(candidate)) {
169
+ if (!isRecord(reference) || typeof reference['secretName'] !== 'string')
170
+ return undefined;
171
+ const secret = secrets[reference['secretName']];
172
+ if (secret === undefined)
173
+ return undefined;
174
+ result[name] = secret;
175
+ }
176
+ return result;
177
+ };
178
+ if (runtime.data.transport === 'STDIO') {
179
+ const runtimeEnvironment = resolveReferences(runtime.data.environment);
180
+ const configuredEnvironment = resolveReferences(configuration['environment']);
181
+ if (runtimeEnvironment === undefined || configuredEnvironment === undefined)
182
+ continue;
183
+ result.push({
184
+ name: record['serverName'],
185
+ transport: 'STDIO',
186
+ command: runtime.data.command,
187
+ args: [
188
+ ...runtime.data.args,
189
+ ...(Array.isArray(configuration['arguments'])
190
+ ? configuration['arguments'].filter((item) => typeof item === 'string')
191
+ : [])
192
+ ],
193
+ environment: {
194
+ ...runtimeEnvironment,
195
+ ...configuredEnvironment
196
+ }
197
+ });
198
+ }
199
+ else {
200
+ const runtimeHeaders = resolveReferences(runtime.data.headers);
201
+ const configuredHeaders = resolveReferences(configuration['headers']);
202
+ if (runtimeHeaders === undefined || configuredHeaders === undefined)
203
+ continue;
204
+ result.push({
205
+ name: record['serverName'],
206
+ transport: 'HTTP',
207
+ url: runtime.data.url,
208
+ headers: {
209
+ ...runtimeHeaders,
210
+ ...configuredHeaders
211
+ }
212
+ });
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+ function isRecord(value) {
218
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
219
+ }
146
220
  export class RunnerCapabilityError extends Error {
147
221
  runner;
148
222
  capability;
@@ -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: 'Implement', 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({
@@ -138,6 +139,7 @@ export class ClaudeManagedRunController {
138
139
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
139
140
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
140
141
  environment: secretEnvironment,
142
+ mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment),
141
143
  ...(attachments.length === 0
142
144
  ? {}
143
145
  : {
@@ -254,14 +256,23 @@ export class ClaudeManagedRunController {
254
256
  const kind = toolKinds.get(record.tool_use_id);
255
257
  if (kind === 'tool_call') {
256
258
  const elapsed = typeof record.elapsed_time_seconds === 'number'
257
- ? `(${Math.max(0, Math.round(record.elapsed_time_seconds))} 秒)`
259
+ ? ` (${Math.max(0, Math.round(record.elapsed_time_seconds))} seconds)`
258
260
  : '';
259
261
  this.host.emitItem(runId, {
260
262
  itemId: boundedConversationItemId('claude-tool', record.tool_use_id),
261
263
  kind,
262
264
  status: 'IN_PROGRESS',
263
265
  payload: {
264
- 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
+ }
265
276
  },
266
277
  merge: true
267
278
  });
@@ -314,12 +325,12 @@ export class ClaudeManagedRunController {
314
325
  this.host.send('session.channel.ready', { sessionId, token: gate.token });
315
326
  this.host.emit(runId, 'run.running', {}, 'RUNNING');
316
327
  void handle.completed.then(() => finish('SUCCEEDED', 'run.completed', { subtype: 'stream_closed' }), (error) => finish('FAILED', 'run.failed', {
317
- code: error instanceof Error ? error.message : 'CLAUDE_RUN_FAILED'
328
+ code: safeErrorCode(error, 'CLAUDE_RUN_FAILED')
318
329
  }));
319
330
  }
320
331
  catch (error) {
321
332
  finish('FAILED', 'run.failed', {
322
- code: error instanceof Error ? error.message : 'CLAUDE_RUN_START_FAILED'
333
+ code: safeErrorCode(error, 'CLAUDE_RUN_START_FAILED')
323
334
  });
324
335
  }
325
336
  }
@@ -17,6 +17,7 @@ export declare class ClaudeCodeRunner extends AbstractRunner<'claude-code'> {
17
17
  available(capabilities: NodeCapabilities): boolean;
18
18
  protected profileForValidation(): RunnerProfile;
19
19
  defaultConfiguration(): RunnerDefaultConfiguration;
20
+ mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
20
21
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext): Promise<ExternalResumeResult>;
21
22
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
22
23
  readContextUsage(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeClaudeContextUsage | undefined>;
@@ -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;
@@ -45,6 +45,20 @@ export class ClaudeCodeRunner extends AbstractRunner {
45
45
  access: 'default'
46
46
  };
47
47
  }
48
+ mcpConfiguration(raw, secrets) {
49
+ const servers = super.mcpConfiguration(raw, secrets);
50
+ return Object.fromEntries(servers.map((server) => [
51
+ server.name,
52
+ server.transport === 'STDIO'
53
+ ? {
54
+ type: 'stdio',
55
+ command: server.command,
56
+ args: [...(server.args ?? [])],
57
+ env: { ...(server.environment ?? {}) }
58
+ }
59
+ : { type: 'http', url: server.url, headers: { ...(server.headers ?? {}) } }
60
+ ]));
61
+ }
48
62
  async resumeExternal(external, context) {
49
63
  if (external.runner !== this.name ||
50
64
  external.nativeControl !== 'EXTERNAL' ||
@@ -88,11 +102,10 @@ export class ClaudeCodeRunner extends AbstractRunner {
88
102
  context.rememberManagedNative(external.externalSessionId, configured.id);
89
103
  return { session: configured, activity: 'IDLE' };
90
104
  }
91
- catch (error) {
105
+ catch {
92
106
  return {
93
107
  failureCode: 'RUNNER_RESUME_FAILED',
94
- messageFailureCode: 'SESSION_RESUME_REJECTED',
95
- failureDetail: externalResumeFailureDetail(error)
108
+ messageFailureCode: 'SESSION_RESUME_REJECTED'
96
109
  };
97
110
  }
98
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))