@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
@@ -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: 'Implement', 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);
@@ -325,7 +326,8 @@ export class CodexManagedRunController {
325
326
  effort: payload.effort,
326
327
  access: payload.access,
327
328
  collaborationMode: payload.collaborationMode,
328
- serviceTier: payload.serviceTier
329
+ serviceTier: payload.serviceTier,
330
+ mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment)
329
331
  }, attachments);
330
332
  }
331
333
  async run(payload, attachments) {
@@ -340,7 +342,8 @@ export class CodexManagedRunController {
340
342
  const configuration = {
341
343
  ...(typeof payload.model === 'string' ? { model: payload.model } : {}),
342
344
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
343
- ...(typeof payload.access === 'string' ? { access: payload.access } : {})
345
+ ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
346
+ ...(payload.mcpServers === undefined ? {} : { mcpServers: payload.mcpServers })
344
347
  };
345
348
  const threadResult = typeof payload.externalSessionId === 'string'
346
349
  ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
@@ -379,7 +382,7 @@ export class CodexManagedRunController {
379
382
  this.runner.releaseEnvironment(runId);
380
383
  return;
381
384
  }
382
- 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');
383
386
  this.runner.execution.runs.delete(runId);
384
387
  this.runner.releaseEnvironment(runId);
385
388
  this.host.removeActive(runId);
@@ -5,17 +5,29 @@ import type { NodeAgentSession, NodeDatabase } from '../database.js';
5
5
  import type { NodeWorkspace } from '../database.js';
6
6
  export declare class CodexRunner extends AbstractRunner<'codex'> {
7
7
  private readonly client;
8
+ private readonly profileClientFactory;
8
9
  readonly name: "codex";
9
10
  readonly discoveryScope: "ALL_WORKSPACES";
10
11
  readonly execution: CodexExecutionState;
11
12
  private fastEnabled;
12
13
  private environmentFingerprint;
13
14
  private readonly environmentRuns;
14
- constructor(client?: CodexAppServerClient);
15
+ private readonly profileCache;
16
+ private readonly profileRefreshes;
17
+ private readonly profileClients;
18
+ private readonly profileKeyByEnvironment;
19
+ private latestProfileCache;
20
+ private selectedProfileKey;
21
+ constructor(client?: CodexAppServerClient, profileClientFactory?: () => CodexAppServerClient);
15
22
  profile(capabilities: NodeCapabilities): RunnerProfile;
23
+ refreshProfile(capabilities: NodeCapabilities, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<RunnerProfile>;
24
+ private refreshCodexProfile;
25
+ private selectCachedProfile;
26
+ private fetchCodexProfile;
16
27
  available(capabilities: NodeCapabilities): boolean;
17
28
  executeGlobalCommand(commandId: string, input: string): unknown;
18
29
  serviceTier(): 'fast' | undefined;
30
+ mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
19
31
  usesRunnerTitleForRename(): boolean;
20
32
  renameNative(session: NodeAgentSession, input: {
21
33
  readonly title?: string | null;
@@ -32,10 +44,12 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
32
44
  readExternalActivity(session: NodeAgentSession): Promise<"UNAVAILABLE" | "MANAGED_ACTIVE" | "EXTERNAL_ACTIVE" | "IDLE" | undefined>;
33
45
  acceptsTruncatedNativeHistory(): boolean;
34
46
  readManagedRunnerTitle(session: NodeAgentSession, nativeSessionId: string): Promise<string | undefined>;
35
- nativeImageRoot(session: NodeAgentSession): string | undefined;
47
+ nativeImageRoots(session: NodeAgentSession): readonly string[];
36
48
  managedRunForNativeTurn(nativeTurnId: string): string | undefined;
37
49
  protected profileForValidation(): RunnerProfile;
38
- defaultConfiguration(): RunnerDefaultConfiguration;
50
+ defaultConfiguration(_workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
51
+ supportsConfiguration(configuration: RunnerDefaultConfiguration, _workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
52
+ private profileCacheForEnvironment;
39
53
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext, force?: boolean): Promise<ExternalResumeResult>;
40
54
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
41
55
  readContextUsage(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeCodexContextUsage | undefined>;
@@ -6,27 +6,150 @@ import { resolve } from 'node:path';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { discoverAllNativeSessions } from '../native-session-history.js';
8
8
  import { readCodexNativeContextUsage } from '../native-session-history.js';
9
- import { codexActiveTurnId, codexOfficialConversationPage, codexThreadCwd, codexThreadActivity, deduplicateDirectSessions, externalResumeFailureDetail, extractCodexThreads, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
9
+ import { codexActiveTurnId, codexOfficialConversationPage, codexThreadCwd, codexThreadActivity, deduplicateDirectSessions, extractCodexThreads, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
10
10
  import { codexSessionControl } from '../codex-app-server.js';
11
11
  import { extractId } from '../util/runner-native-session-parsers.js';
12
+ import { nodeLog } from '../operational.js';
13
+ const CODEX_MODEL_CACHE_MS = 10 * 60 * 1_000;
14
+ const CODEX_MODEL_FAILURE_RETRY_MS = 30 * 1_000;
15
+ const CODEX_MODEL_PAGE_SIZE = 100;
12
16
  export class CodexRunner extends AbstractRunner {
13
17
  client;
18
+ profileClientFactory;
14
19
  name = 'codex';
15
20
  discoveryScope = 'ALL_WORKSPACES';
16
21
  execution = new CodexExecutionState();
17
22
  fastEnabled = false;
18
23
  environmentFingerprint;
19
24
  environmentRuns = new Set();
20
- constructor(client = new CodexAppServerClient()) {
25
+ profileCache = new Map();
26
+ profileRefreshes = new Map();
27
+ profileClients = new Set();
28
+ profileKeyByEnvironment = new Map();
29
+ latestProfileCache;
30
+ selectedProfileKey;
31
+ constructor(client = new CodexAppServerClient(), profileClientFactory = () => new CodexAppServerClient()) {
21
32
  super();
22
33
  this.client = client;
34
+ this.profileClientFactory = profileClientFactory;
23
35
  }
24
36
  profile(capabilities) {
25
- return declaredRunnerProfiles({
37
+ const base = declaredRunnerProfiles({
26
38
  codexAvailable: capabilities.codex.available,
27
39
  claudeCodeAvailable: capabilities.claudeCode.available,
28
40
  openCodeAvailable: capabilities.openCode?.available === true
29
41
  }).find((profile) => profile.runner === this.name);
42
+ return { ...base, models: this.latestProfileCache?.profile.models ?? [] };
43
+ }
44
+ async refreshProfile(capabilities, workspace, environment = {}) {
45
+ void workspace;
46
+ const base = this.profile(capabilities);
47
+ if (!capabilities.codex.available)
48
+ return { ...base, models: [] };
49
+ const key = codexEnvironmentFingerprint(environment, capabilities.codex.version);
50
+ this.profileKeyByEnvironment.set(codexSecretEnvironmentFingerprint(environment), key);
51
+ this.selectedProfileKey = key;
52
+ const now = Date.now();
53
+ const cached = this.profileCache.get(key);
54
+ if (cached?.successful === true && now < cached.fetchedAt + CODEX_MODEL_CACHE_MS)
55
+ return this.selectCachedProfile(key, cached);
56
+ if (cached?.successful === true) {
57
+ if (now >= cached.retryAt)
58
+ void this.refreshCodexProfile(key, base, environment);
59
+ return this.selectCachedProfile(key, cached);
60
+ }
61
+ if (cached !== undefined && now < cached.retryAt)
62
+ return cached.profile;
63
+ return this.refreshCodexProfile(key, base, environment);
64
+ }
65
+ refreshCodexProfile(key, base, environment) {
66
+ const existing = this.profileRefreshes.get(key);
67
+ if (existing !== undefined)
68
+ return existing;
69
+ const refresh = this.fetchCodexProfile(base, environment)
70
+ .then((value) => {
71
+ const cache = {
72
+ ...value,
73
+ fetchedAt: Date.now(),
74
+ successful: true,
75
+ retryAt: 0
76
+ };
77
+ this.profileCache.set(key, cache);
78
+ if (this.selectedProfileKey === key)
79
+ this.latestProfileCache = cache;
80
+ return cache.profile;
81
+ })
82
+ .catch((error) => {
83
+ nodeLog('runner.codex.models.refresh-failed', {
84
+ error: error instanceof Error ? error.message : String(error)
85
+ });
86
+ const cached = this.profileCache.get(key);
87
+ if (cached !== undefined) {
88
+ cached.retryAt = Date.now() + CODEX_MODEL_FAILURE_RETRY_MS;
89
+ return cached.profile;
90
+ }
91
+ this.profileCache.set(key, {
92
+ profile: { ...base, models: [] },
93
+ defaultConfiguration: emptyCodexConfiguration(),
94
+ fetchedAt: 0,
95
+ successful: false,
96
+ retryAt: Date.now() + CODEX_MODEL_FAILURE_RETRY_MS
97
+ });
98
+ return { ...base, models: [] };
99
+ })
100
+ .finally(() => this.profileRefreshes.delete(key));
101
+ this.profileRefreshes.set(key, refresh);
102
+ return refresh;
103
+ }
104
+ selectCachedProfile(key, cached) {
105
+ if (cached.successful && this.selectedProfileKey === key)
106
+ this.latestProfileCache = cached;
107
+ return cached.profile;
108
+ }
109
+ async fetchCodexProfile(base, environment) {
110
+ const client = this.profileClientFactory();
111
+ this.profileClients.add(client);
112
+ if (Object.keys(environment).length > 0)
113
+ client.configureEnvironment(environment);
114
+ try {
115
+ await client.start();
116
+ const models = [];
117
+ let cursor = null;
118
+ const cursors = new Set();
119
+ do {
120
+ const page = codexModelPage(await client.listModels({ cursor, limit: CODEX_MODEL_PAGE_SIZE, includeHidden: false }));
121
+ models.push(...page.models);
122
+ cursor = page.nextCursor;
123
+ if (cursor !== null && cursors.has(cursor))
124
+ throw new Error('CODEX_MODELS_CURSOR_REPEATED');
125
+ if (cursor !== null)
126
+ cursors.add(cursor);
127
+ } while (cursor !== null);
128
+ const uniqueModels = models.filter((model, index) => models.findIndex((candidate) => candidate.model === model.model) === index);
129
+ const profileModels = uniqueModels.map((model) => ({
130
+ id: model.model,
131
+ label: model.displayName,
132
+ supportedEfforts: [...model.supportedEfforts],
133
+ isDefault: model.isDefault,
134
+ defaultEffort: model.defaultEffort
135
+ }));
136
+ const selected = uniqueModels.find((model) => model.isDefault) ?? uniqueModels[0];
137
+ return {
138
+ profile: { ...base, models: profileModels },
139
+ defaultConfiguration: selected === undefined
140
+ ? emptyCodexConfiguration()
141
+ : {
142
+ runner: this.name,
143
+ model: selected.model,
144
+ effort: selected.defaultEffort,
145
+ access: 'on-request'
146
+ }
147
+ };
148
+ }
149
+ finally {
150
+ if (this.profileClients.delete(client))
151
+ client.stop();
152
+ }
30
153
  }
31
154
  available(capabilities) {
32
155
  return capabilities.codex.available;
@@ -44,6 +167,19 @@ export class CodexRunner extends AbstractRunner {
44
167
  serviceTier() {
45
168
  return this.fastEnabled ? 'fast' : undefined;
46
169
  }
170
+ mcpConfiguration(raw, secrets) {
171
+ const servers = super.mcpConfiguration(raw, secrets);
172
+ return Object.fromEntries(servers.map((server) => [
173
+ server.name,
174
+ server.transport === 'STDIO'
175
+ ? {
176
+ command: server.command,
177
+ args: [...(server.args ?? [])],
178
+ env: { ...(server.environment ?? {}) }
179
+ }
180
+ : { url: server.url, http_headers: { ...(server.headers ?? {}) } }
181
+ ]));
182
+ }
47
183
  usesRunnerTitleForRename() {
48
184
  return true;
49
185
  }
@@ -112,10 +248,13 @@ export class CodexRunner extends AbstractRunner {
112
248
  await this.start();
113
249
  return extractCodexThreads(await this.listThreads()).find((thread) => thread.id === nativeSessionId)?.title;
114
250
  }
115
- nativeImageRoot(session) {
116
- return session.externalSessionId === null
117
- ? undefined
118
- : resolve(homedir(), '.codex', 'generated_images', session.externalSessionId);
251
+ nativeImageRoots(session) {
252
+ return [
253
+ session.cwd,
254
+ ...(session.externalSessionId === null
255
+ ? []
256
+ : [resolve(homedir(), '.codex', 'generated_images', session.externalSessionId)])
257
+ ];
119
258
  }
120
259
  managedRunForNativeTurn(nativeTurnId) {
121
260
  return this.execution.managedTurns.get(nativeTurnId)?.runId;
@@ -131,8 +270,23 @@ export class CodexRunner extends AbstractRunner {
131
270
  openCode: { available: false }
132
271
  });
133
272
  }
134
- defaultConfiguration() {
135
- return { runner: this.name, model: 'gpt-5.6-sol', effort: 'medium', access: 'on-request' };
273
+ defaultConfiguration(_workspace, environment) {
274
+ return (this.profileCacheForEnvironment(environment)?.defaultConfiguration ??
275
+ emptyCodexConfiguration());
276
+ }
277
+ supportsConfiguration(configuration, _workspace, environment) {
278
+ if (configuration.runner !== this.name)
279
+ return false;
280
+ const profile = this.profileCacheForEnvironment(environment)?.profile;
281
+ return (profile !== undefined &&
282
+ profile.models.some((model) => model.id === configuration.model && model.supportedEfforts.includes(configuration.effort)) &&
283
+ profile.accessOptions.some((option) => option.id === configuration.access));
284
+ }
285
+ profileCacheForEnvironment(environment) {
286
+ if (environment === undefined)
287
+ return this.latestProfileCache;
288
+ const key = this.profileKeyByEnvironment.get(codexSecretEnvironmentFingerprint(environment));
289
+ return key === undefined ? undefined : this.profileCache.get(key);
136
290
  }
137
291
  async resumeExternal(external, context, force = false) {
138
292
  if (external.runner !== this.name ||
@@ -162,11 +316,10 @@ export class CodexRunner extends AbstractRunner {
162
316
  }
163
317
  return { activity: 'IDLE', session: context.create(external) };
164
318
  }
165
- catch (error) {
319
+ catch {
166
320
  return {
167
321
  activity: 'UNAVAILABLE',
168
- failureCode: 'RUNNER_RESUME_FAILED',
169
- failureDetail: externalResumeFailureDetail(error)
322
+ failureCode: 'RUNNER_RESUME_FAILED'
170
323
  };
171
324
  }
172
325
  }
@@ -305,6 +458,9 @@ export class CodexRunner extends AbstractRunner {
305
458
  this.environmentFingerprint = undefined;
306
459
  }
307
460
  stop() {
461
+ for (const profileClient of this.profileClients)
462
+ profileClient.stop();
463
+ this.profileClients.clear();
308
464
  this.client.stop();
309
465
  if (this.environmentFingerprint !== undefined)
310
466
  this.client.clearEnvironment();
@@ -357,6 +513,63 @@ export class CodexRunner extends AbstractRunner {
357
513
  return this.client.listCollaborationModes();
358
514
  }
359
515
  }
516
+ function emptyCodexConfiguration() {
517
+ return { runner: 'codex', model: '', effort: '', access: 'on-request' };
518
+ }
519
+ function codexEnvironmentFingerprint(environment, version) {
520
+ return createHash('sha256')
521
+ .update(JSON.stringify({
522
+ environment: Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)),
523
+ version: version ?? null
524
+ }))
525
+ .digest('base64url');
526
+ }
527
+ function codexSecretEnvironmentFingerprint(environment) {
528
+ return createHash('sha256')
529
+ .update(JSON.stringify(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right))))
530
+ .digest('base64url');
531
+ }
532
+ function codexModelPage(value) {
533
+ if (!isPlainRecord(value) || !Array.isArray(value.data))
534
+ throw new Error('CODEX_MODELS_INVALID');
535
+ return {
536
+ models: value.data.flatMap((entry) => {
537
+ if (!isPlainRecord(entry) || entry.hidden === true)
538
+ return [];
539
+ const model = typeof entry.model === 'string' ? entry.model.trim() : '';
540
+ const displayName = typeof entry.displayName === 'string' ? entry.displayName.trim() : '';
541
+ const defaultEffort = typeof entry.defaultReasoningEffort === 'string' ? entry.defaultReasoningEffort.trim() : '';
542
+ const supportedEfforts = Array.isArray(entry.supportedReasoningEfforts)
543
+ ? entry.supportedReasoningEfforts.flatMap((option) => isPlainRecord(option) && typeof option.reasoningEffort === 'string'
544
+ ? [option.reasoningEffort.trim()]
545
+ : [])
546
+ : [];
547
+ if (model.length === 0 ||
548
+ model.length > 128 ||
549
+ displayName.length === 0 ||
550
+ displayName.length > 128 ||
551
+ defaultEffort.length === 0 ||
552
+ defaultEffort.length > 64 ||
553
+ supportedEfforts.length > 32 ||
554
+ supportedEfforts.some((effort) => effort.length === 0 || effort.length > 64) ||
555
+ !supportedEfforts.includes(defaultEffort))
556
+ return [];
557
+ return [
558
+ {
559
+ model,
560
+ displayName,
561
+ isDefault: entry.isDefault === true,
562
+ defaultEffort,
563
+ supportedEfforts: [...new Set(supportedEfforts.filter(Boolean))]
564
+ }
565
+ ];
566
+ }),
567
+ nextCursor: typeof value.nextCursor === 'string' && value.nextCursor.length > 0 ? value.nextCursor : null
568
+ };
569
+ }
570
+ function isPlainRecord(value) {
571
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
572
+ }
360
573
  export class CodexExecutionState {
361
574
  runs = new Map();
362
575
  managedTurns = new Map();
@@ -18,10 +18,14 @@ export declare function openCodeQuestions(value: unknown): {
18
18
  allowOther: boolean;
19
19
  multiSelect: boolean;
20
20
  }[];
21
- export declare function openCodePatchChanges(value: unknown): {
22
- path: string;
23
- change: string;
24
- additions: null;
25
- deletions: null;
26
- diffAvailable: boolean;
27
- }[];
21
+ export declare function openCodePatchToolPayload(): {
22
+ namespace: string;
23
+ toolName: string;
24
+ title: string;
25
+ inputSummary: null;
26
+ outputSummary: string;
27
+ outputSummaryCode: string;
28
+ progressLabel: null;
29
+ errorCode: null;
30
+ truncated: boolean;
31
+ };
@@ -1,4 +1,3 @@
1
- import { basename } from 'node:path';
2
1
  import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../codex/conversation-parser.js';
3
2
  export function openCodeConversationPage(session, messages, input, managedTurn) {
4
3
  const records = messages.flatMap((value) => {
@@ -114,10 +113,11 @@ function partItem(session, turnId, raw, planMode, startedAt, completedAt, sequen
114
113
  if (part.type === 'compaction')
115
114
  return item(session, turnId, part.id, 'context_compaction', 'COMPLETED', {
116
115
  phase: 'COMPLETED',
117
- summary: '上下文已整理'
116
+ summary: 'Context compacted',
117
+ summaryCode: 'CONTEXT_COMPACTED'
118
118
  }, startedAt, completedAt, sequence);
119
119
  if (part.type === 'patch')
120
- return item(session, turnId, part.id, 'file_change', 'COMPLETED', { changes: openCodePatchChanges(part.files) }, startedAt, completedAt, sequence);
120
+ return item(session, turnId, part.id, 'tool_call', 'COMPLETED', openCodePatchToolPayload(), startedAt, completedAt, sequence);
121
121
  if (part.type === 'step-start' || part.type === 'step-finish')
122
122
  return undefined;
123
123
  if (part.type === 'text' || part.type === 'reasoning')
@@ -162,18 +162,18 @@ export function openCodeQuestions(value) {
162
162
  ];
163
163
  });
164
164
  }
165
- export function openCodePatchChanges(value) {
166
- return (Array.isArray(value) ? value : []).flatMap((path) => typeof path === 'string' && path.length > 0
167
- ? [
168
- {
169
- path: basename(path),
170
- change: 'MODIFIED',
171
- additions: null,
172
- deletions: null,
173
- diffAvailable: false
174
- }
175
- ]
176
- : []);
165
+ export function openCodePatchToolPayload() {
166
+ return {
167
+ namespace: 'opencode',
168
+ toolName: 'patch',
169
+ title: 'patch',
170
+ inputSummary: null,
171
+ outputSummary: 'OpenCode applied the patch',
172
+ outputSummaryCode: 'OPENCODE_PATCH_APPLIED',
173
+ progressLabel: null,
174
+ errorCode: null,
175
+ truncated: false
176
+ };
177
177
  }
178
178
  function item(session, turnId, nativeId, kind, status, payload, startedAt, completedAt, sequence) {
179
179
  return {
@@ -7,6 +7,7 @@ interface OpenCodeManagedRunHost {
7
7
  intercepted(): boolean;
8
8
  readonly active: Set<string>;
9
9
  parseAttachments(value: unknown): readonly RunnerImageAttachment[];
10
+ persistAttachments(cwd: string, runId: string, attachments: readonly RunnerImageAttachment[]): Promise<readonly string[]>;
10
11
  emit(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
11
12
  emitItem(runId: string, item: OpenCodePublicItem): void;
12
13
  adopt(input: {
@@ -40,7 +41,7 @@ interface OpenCodeManagedRunHost {
40
41
  }
41
42
  type OpenCodePublicItem = {
42
43
  readonly itemId: string;
43
- readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'file_change' | 'unknown';
44
+ readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'unknown';
44
45
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
45
46
  readonly payload: Record<string, unknown>;
46
47
  readonly merge?: boolean;
@@ -1,6 +1,7 @@
1
1
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
2
+ import { safeErrorCode } from '../../util/safe-error.js';
2
3
  import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../codex/conversation-parser.js';
3
- import { openCodePatchChanges, openCodeQuestions } from './conversation-parser.js';
4
+ import { openCodePatchToolPayload, openCodeQuestions } from './conversation-parser.js';
4
5
  export class OpenCodeManagedRunController {
5
6
  runner;
6
7
  host;
@@ -29,10 +30,10 @@ export class OpenCodeManagedRunController {
29
30
  commandId: 'plan',
30
31
  active: true,
31
32
  label: 'Plan Mode',
32
- detail: '后续消息将由 OpenCode 原生 Plan Agent 只读分析并制定计划。',
33
+ detail: 'Subsequent messages use the OpenCode Plan Agent for read-only analysis.',
33
34
  closable: true,
34
35
  closeInput: '/plan',
35
- continuation: { label: 'Implement', prompt: '请根据上述计划开始实施。' }
36
+ continuation: { label: 'Implement', prompt: 'Implement the plan above.' }
36
37
  });
37
38
  return { command: command.id, states: this.host.commandStates(session.id) };
38
39
  }
@@ -48,13 +49,15 @@ export class OpenCodeManagedRunController {
48
49
  this.host.active.has(payload.runId))
49
50
  return;
50
51
  let environment;
52
+ let mcp;
51
53
  let attachments;
52
54
  try {
53
55
  environment = parseSecretEnvironment(payload.secretEnvironment);
56
+ mcp = this.runner.mcpConfiguration(payload.mcpInstallations, environment);
54
57
  attachments = this.host.parseAttachments(payload.attachments);
55
58
  }
56
59
  catch (error) {
57
- this.host.emit(payload.runId, 'run.rejected', { code: error instanceof Error ? error.message : 'RUNNER_INPUT_INVALID' }, 'FAILED');
60
+ this.host.emit(payload.runId, 'run.rejected', { code: safeErrorCode(error, 'RUNNER_INPUT_INVALID') }, 'FAILED');
58
61
  return;
59
62
  }
60
63
  void this.run({
@@ -70,7 +73,8 @@ export class OpenCodeManagedRunController {
70
73
  environment,
71
74
  attachments,
72
75
  access: typeof payload.access === 'string' ? payload.access : 'default',
73
- agent: payload.collaborationMode === 'plan' ? 'plan' : 'build'
76
+ agent: payload.collaborationMode === 'plan' ? 'plan' : 'build',
77
+ mcp
74
78
  });
75
79
  }
76
80
  cancel(runId, intent) {
@@ -119,12 +123,16 @@ export class OpenCodeManagedRunController {
119
123
  return true;
120
124
  }
121
125
  async run(input) {
122
- const client = this.runner.managedClient(input.environment);
126
+ // OpenCode dynamic MCP registration belongs to Server process state. Runs that load MCP use an
127
+ // isolated Server so parallel sessions in one workspace do not share MCP processes or context.
128
+ const client = this.runner.managedClient(input.environment, hasMcpConfiguration(input.mcp) ? input.runId : undefined);
123
129
  this.startingClients.set(input.runId, client);
124
130
  this.startingCwds.set(input.runId, input.cwd);
125
131
  this.host.active.add(input.runId);
126
132
  this.host.emit(input.runId, 'run.started', {}, 'STARTING');
127
133
  try {
134
+ if (hasMcpConfiguration(input.mcp))
135
+ await client.configureMcp(input.cwd, input.mcp);
128
136
  const nativeSessionId = input.externalSessionId ?? openCodeSessionId(await client.createSession(input.cwd));
129
137
  if (nativeSessionId === undefined)
130
138
  throw new Error('OPENCODE_SESSION_INVALID');
@@ -174,17 +182,20 @@ export class OpenCodeManagedRunController {
174
182
  return;
175
183
  }
176
184
  this.finish(input.runId, 'FAILED', 'run.failed', {
177
- code: error instanceof Error ? error.message : 'OPENCODE_EVENT_FAILED'
185
+ code: safeErrorCode(error, 'OPENCODE_EVENT_FAILED')
178
186
  });
179
187
  });
180
188
  await subscriptionReady;
181
189
  this.host.emit(input.runId, 'run.running', {}, 'RUNNING');
182
190
  // The initial SSE snapshot can contain an idle event from before this prompt.
183
191
  this.pendingIdle.delete(input.runId);
192
+ const attachmentPaths = input.attachments.length === 0
193
+ ? []
194
+ : await this.host.persistAttachments(input.cwd, input.runId, input.attachments);
184
195
  await client.prompt({
185
196
  directory: input.cwd,
186
197
  sessionID: nativeSessionId,
187
- text: input.input,
198
+ text: openCodeImageInput(input.input, attachmentPaths),
188
199
  model: input.model,
189
200
  variant: input.effort,
190
201
  agent: input.agent,
@@ -196,7 +207,7 @@ export class OpenCodeManagedRunController {
196
207
  }
197
208
  catch (error) {
198
209
  this.finish(input.runId, 'FAILED', 'run.failed', {
199
- code: error instanceof Error ? error.message : 'OPENCODE_RUN_START_FAILED'
210
+ code: safeErrorCode(error, 'OPENCODE_RUN_START_FAILED')
200
211
  });
201
212
  }
202
213
  }
@@ -359,6 +370,15 @@ export class OpenCodeManagedRunController {
359
370
  void this.host.captureSnapshot(runId, cwd);
360
371
  }
361
372
  }
373
+ function openCodeImageInput(input, attachmentPaths) {
374
+ if (attachmentPaths.length === 0)
375
+ return input;
376
+ const paths = attachmentPaths.map((path) => `- ${JSON.stringify(path)}`).join('\n');
377
+ return `${input}\n\nIf an inline image in this message is unavailable, use the read tool on the corresponding copy below. Do not read it again when the inline image is already visible:\n${paths}`;
378
+ }
379
+ function hasMcpConfiguration(value) {
380
+ return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
381
+ }
362
382
  function openCodePart(part, planMode = false) {
363
383
  const id = typeof part?.id === 'string' ? part.id : undefined;
364
384
  if (id === undefined || typeof part?.type !== 'string')
@@ -415,14 +435,18 @@ function openCodePart(part, planMode = false) {
415
435
  itemId,
416
436
  kind: 'context_compaction',
417
437
  status: 'COMPLETED',
418
- payload: { phase: 'COMPLETED', summary: '上下文已整理' }
438
+ payload: {
439
+ phase: 'COMPLETED',
440
+ summary: 'Context compacted',
441
+ summaryCode: 'CONTEXT_COMPACTED'
442
+ }
419
443
  };
420
444
  if (part.type === 'patch')
421
445
  return {
422
446
  itemId,
423
- kind: 'file_change',
447
+ kind: 'tool_call',
424
448
  status: 'COMPLETED',
425
- payload: { changes: openCodePatchChanges(part.files) },
449
+ payload: openCodePatchToolPayload(),
426
450
  merge: true
427
451
  };
428
452
  if (part.type === 'step-start' || part.type === 'step-finish')