@myagentroam/node 0.9.0 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/connector.d.ts +2 -0
  2. package/dist/connector.js +50 -25
  3. package/dist/control-outbound-scheduler.d.ts +4 -1
  4. package/dist/control-outbound-scheduler.js +39 -6
  5. package/dist/database.d.ts +0 -19
  6. package/dist/database.js +1 -62
  7. package/dist/event-buffer.d.ts +2 -5
  8. package/dist/event-buffer.js +3 -12
  9. package/dist/native-session-history.js +105 -26
  10. package/dist/runner/abstract-runner.d.ts +1 -1
  11. package/dist/runner/abstract-runner.js +2 -2
  12. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  13. package/dist/runner/claude/managed-run-controller.js +32 -17
  14. package/dist/runner/claude-code-runner.js +3 -4
  15. package/dist/runner/codex/conversation-parser.d.ts +3 -3
  16. package/dist/runner/codex/conversation-parser.js +33 -12
  17. package/dist/runner/codex/managed-run-controller.d.ts +2 -2
  18. package/dist/runner/codex/managed-run-controller.js +26 -10
  19. package/dist/runner/codex-runner.d.ts +3 -1
  20. package/dist/runner/codex-runner.js +40 -10
  21. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  22. package/dist/runner/opencode/conversation-parser.js +16 -16
  23. package/dist/runner/opencode/managed-run-controller.d.ts +8 -2
  24. package/dist/runner/opencode/managed-run-controller.js +94 -22
  25. package/dist/runner/opencode-runner.d.ts +3 -0
  26. package/dist/runner/opencode-runner.js +25 -0
  27. package/dist/runner-command-engine.js +1 -1
  28. package/dist/runner-profiles.js +29 -17
  29. package/dist/runner-usage.js +5 -5
  30. package/dist/runtime-state.d.ts +5 -12
  31. package/dist/runtime-state.js +32 -46
  32. package/dist/service/attachment-domain-state.d.ts +2 -0
  33. package/dist/service/attachment-domain-state.js +2 -0
  34. package/dist/service/conversation-history-service.js +55 -24
  35. package/dist/service/conversation-segment-service.d.ts +20 -0
  36. package/dist/service/conversation-segment-service.js +156 -0
  37. package/dist/service/external-session-resume-service.js +9 -0
  38. package/dist/service/fake-managed-run-service.d.ts +0 -1
  39. package/dist/service/fake-managed-run-service.js +0 -2
  40. package/dist/service/mcp-installation-verifier.js +1 -1
  41. package/dist/service/native-session-projection-service.js +5 -0
  42. package/dist/service/native-session-watch-service.d.ts +1 -0
  43. package/dist/service/native-session-watch-service.js +21 -4
  44. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  45. package/dist/service/node-connection-lifecycle-service.js +0 -1
  46. package/dist/service/node-control-channel.js +1 -1
  47. package/dist/service/node-control-message-service.d.ts +0 -1
  48. package/dist/service/node-control-message-service.js +9 -6
  49. package/dist/service/node-request-service.js +2 -1
  50. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  51. package/dist/service/node-workspace-coordinator.js +2 -12
  52. package/dist/service/run-attachment-service.d.ts +3 -2
  53. package/dist/service/run-attachment-service.js +51 -17
  54. package/dist/service/run-event-service.d.ts +2 -0
  55. package/dist/service/run-event-service.js +77 -2
  56. package/dist/service/run-workbench-service.d.ts +1 -1
  57. package/dist/service/run-workbench-service.js +1 -5
  58. package/dist/service/runner-interaction-service.d.ts +1 -2
  59. package/dist/service/runner-interaction-service.js +1 -4
  60. package/dist/service/session-catalog-service.js +4 -4
  61. package/dist/service/session-context-service.d.ts +1 -0
  62. package/dist/service/session-context-service.js +14 -0
  63. package/dist/service/session-domain-state.d.ts +1 -0
  64. package/dist/service/session-domain-state.js +1 -0
  65. package/dist/service/session-message-service.js +5 -10
  66. package/dist/service/session-presentation-service.d.ts +1 -0
  67. package/dist/service/session-presentation-service.js +8 -1
  68. package/dist/service/session-query-service.d.ts +4 -0
  69. package/dist/service/session-query-service.js +6 -1
  70. package/dist/service/skill-directory-service.d.ts +3 -1
  71. package/dist/service/skill-directory-service.js +31 -30
  72. package/dist/service/skill-install-service.js +4 -74
  73. package/dist/service/skill-node-operation-service.d.ts +1 -1
  74. package/dist/service/skill-node-operation-service.js +6 -3
  75. package/dist/service/workbench-event-service.d.ts +1 -13
  76. package/dist/service/workbench-event-service.js +2 -36
  77. package/dist/service/workbench-manifest-service.d.ts +0 -1
  78. package/dist/service/workbench-manifest-service.js +0 -1
  79. package/dist/service/workspace-domain-state.d.ts +1 -0
  80. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  81. package/dist/service/workspace-git-exclude-service.js +119 -0
  82. package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
  83. package/dist/service/workspace-queue-workbench-service.js +4 -2
  84. package/dist/service/workspace-service.js +2 -0
  85. package/dist/service/workspace-upload-service.d.ts +2 -0
  86. package/dist/service/workspace-upload-service.js +28 -6
  87. package/dist/service/workspace-workbench-service.d.ts +0 -3
  88. package/dist/service/workspace-workbench-service.js +0 -17
  89. package/dist/supervisor.js +2 -1
  90. package/dist/terminal.js +1 -1
  91. package/dist/util/node-operation-parsers.js +2 -2
  92. package/dist/util/runner-error.d.ts +6 -0
  93. package/dist/util/runner-error.js +52 -0
  94. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  95. package/dist/util/runner-native-session-parsers.js +48 -20
  96. package/dist/util/safe-error.d.ts +2 -0
  97. package/dist/util/safe-error.js +5 -0
  98. package/package.json +2 -2
@@ -1,5 +1,7 @@
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';
4
+ import { runnerErrorProjection } from '../../util/runner-error.js';
3
5
  import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
4
6
  export class CodexManagedRunController {
5
7
  runner;
@@ -32,10 +34,10 @@ export class CodexManagedRunController {
32
34
  commandId: 'plan',
33
35
  active: true,
34
36
  label: 'Plan Mode',
35
- detail: '后续消息将以协作规划模式运行。',
37
+ detail: 'Subsequent messages run in collaborative Plan Mode.',
36
38
  closable: true,
37
39
  closeInput: '/plan',
38
- continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
40
+ continuation: { label: 'Implement', prompt: 'Implement the plan above.' }
39
41
  });
40
42
  return { command: command.id, states: this.host.commandStates(session.id) };
41
43
  }
@@ -83,7 +85,8 @@ export class CodexManagedRunController {
83
85
  };
84
86
  }
85
87
  catch (error) {
86
- this.host.emit(runId, 'run.failed', { code: error instanceof Error ? error.message : 'CODEX_COMPACT_FAILED' }, 'FAILED');
88
+ this.emitError(runId, error, 'CODEX_COMPACT_FAILED');
89
+ this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_COMPACT_FAILED'), 'FAILED');
87
90
  this.runner.execution.runs.delete(runId);
88
91
  this.runner.releaseEnvironment(runId);
89
92
  this.host.removeActive(runId);
@@ -183,16 +186,18 @@ export class CodexManagedRunController {
183
186
  if (notification.method === 'turn/completed') {
184
187
  const status = turn?.status;
185
188
  const terminal = status === 'completed' ? 'SUCCEEDED' : status === 'interrupted' ? 'INTERRUPTED' : 'FAILED';
186
- this.host.emit(runId, 'run.completed', { status: status ?? 'unknown' }, terminal);
189
+ if (terminal === 'FAILED')
190
+ this.emitError(runId, turn?.error ?? turn, 'CODEX_RUN_FAILED');
191
+ const failure = terminal === 'FAILED'
192
+ ? runnerErrorProjection(turn?.error ?? turn, 'CODEX_RUN_FAILED')
193
+ : { status: status ?? 'unknown' };
194
+ this.host.emit(runId, terminal === 'FAILED' ? 'run.failed' : 'run.completed', failure, terminal);
187
195
  const active = this.runner.execution.runs.get(runId);
188
196
  const titleRefresh = active === undefined ||
189
197
  active.refreshTitle === false ||
190
198
  this.host.session(active.sessionId)?.titleSource !== 'AUTO'
191
199
  ? undefined
192
200
  : this.host.syncTitle(active.sessionId, active.threadId, active.cwd);
193
- if (active !== undefined) {
194
- void this.host.captureSnapshot(runId, active.cwd);
195
- }
196
201
  this.runner.execution.runs.delete(runId);
197
202
  if (titleRefresh === undefined)
198
203
  this.runner.releaseEnvironment(runId);
@@ -283,7 +288,8 @@ export class CodexManagedRunController {
283
288
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
284
289
  }
285
290
  catch (error) {
286
- this.host.emit(runId, 'run.rejected', { code: error instanceof Error ? error.message : 'MESSAGE_ATTACHMENTS_INVALID' }, 'FAILED');
291
+ this.emitError(runId, error, 'CODEX_RUN_FAILED');
292
+ this.host.emit(runId, 'run.rejected', runnerErrorProjection(error, 'MESSAGE_ATTACHMENTS_INVALID'), 'FAILED');
287
293
  return;
288
294
  }
289
295
  this.host.adopt({
@@ -311,7 +317,7 @@ export class CodexManagedRunController {
311
317
  this.runner.prepareEnvironment(secretEnvironment, runId);
312
318
  }
313
319
  catch (error) {
314
- this.host.emit(runId, 'run.rejected', { code: error instanceof Error ? error.message : 'SECRET_ENVIRONMENT_INVALID' }, 'FAILED');
320
+ this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'SECRET_ENVIRONMENT_INVALID') }, 'FAILED');
315
321
  return;
316
322
  }
317
323
  this.host.addActive(runId);
@@ -381,10 +387,20 @@ export class CodexManagedRunController {
381
387
  this.runner.releaseEnvironment(runId);
382
388
  return;
383
389
  }
384
- this.host.emit(runId, 'run.failed', { code: error instanceof Error ? error.message : 'CODEX_RUN_FAILED' }, 'FAILED');
390
+ this.emitError(runId, error, 'CODEX_RUN_FAILED');
391
+ this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_RUN_FAILED'), 'FAILED');
385
392
  this.runner.execution.runs.delete(runId);
386
393
  this.runner.releaseEnvironment(runId);
387
394
  this.host.removeActive(runId);
388
395
  }
389
396
  }
397
+ emitError(runId, error, fallbackCode) {
398
+ const projected = runnerErrorProjection(error, fallbackCode);
399
+ this.host.emitItem(runId, {
400
+ itemId: boundedConversationItemId('runner-error', runId),
401
+ kind: 'error',
402
+ status: 'FAILED',
403
+ payload: { ...projected, recoverable: true, retryAfterMs: null }
404
+ });
405
+ }
390
406
  }
@@ -13,6 +13,7 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
13
13
  private environmentFingerprint;
14
14
  private readonly environmentRuns;
15
15
  private readonly profileCache;
16
+ private readonly profileCacheTimers;
16
17
  private readonly profileRefreshes;
17
18
  private readonly profileClients;
18
19
  private readonly profileKeyByEnvironment;
@@ -23,6 +24,7 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
23
24
  refreshProfile(capabilities: NodeCapabilities, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<RunnerProfile>;
24
25
  private refreshCodexProfile;
25
26
  private selectCachedProfile;
27
+ private cacheProfile;
26
28
  private fetchCodexProfile;
27
29
  available(capabilities: NodeCapabilities): boolean;
28
30
  executeGlobalCommand(commandId: string, input: string): unknown;
@@ -44,7 +46,7 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
44
46
  readExternalActivity(session: NodeAgentSession): Promise<"UNAVAILABLE" | "MANAGED_ACTIVE" | "EXTERNAL_ACTIVE" | "IDLE" | undefined>;
45
47
  acceptsTruncatedNativeHistory(): boolean;
46
48
  readManagedRunnerTitle(session: NodeAgentSession, nativeSessionId: string): Promise<string | undefined>;
47
- nativeImageRoot(session: NodeAgentSession): string | undefined;
49
+ nativeImageRoots(session: NodeAgentSession): readonly string[];
48
50
  managedRunForNativeTurn(nativeTurnId: string): string | undefined;
49
51
  protected profileForValidation(): RunnerProfile;
50
52
  defaultConfiguration(_workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
@@ -6,11 +6,12 @@ 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
12
  import { nodeLog } from '../operational.js';
13
13
  const CODEX_MODEL_CACHE_MS = 10 * 60 * 1_000;
14
+ const CODEX_PROFILE_ENTRY_TTL_MS = 30 * 60 * 1_000;
14
15
  const CODEX_MODEL_FAILURE_RETRY_MS = 30 * 1_000;
15
16
  const CODEX_MODEL_PAGE_SIZE = 100;
16
17
  export class CodexRunner extends AbstractRunner {
@@ -23,6 +24,7 @@ export class CodexRunner extends AbstractRunner {
23
24
  environmentFingerprint;
24
25
  environmentRuns = new Set();
25
26
  profileCache = new Map();
27
+ profileCacheTimers = new Map();
26
28
  profileRefreshes = new Map();
27
29
  profileClients = new Set();
28
30
  profileKeyByEnvironment = new Map();
@@ -74,7 +76,7 @@ export class CodexRunner extends AbstractRunner {
74
76
  successful: true,
75
77
  retryAt: 0
76
78
  };
77
- this.profileCache.set(key, cache);
79
+ this.cacheProfile(key, cache);
78
80
  if (this.selectedProfileKey === key)
79
81
  this.latestProfileCache = cache;
80
82
  return cache.profile;
@@ -88,7 +90,7 @@ export class CodexRunner extends AbstractRunner {
88
90
  cached.retryAt = Date.now() + CODEX_MODEL_FAILURE_RETRY_MS;
89
91
  return cached.profile;
90
92
  }
91
- this.profileCache.set(key, {
93
+ this.cacheProfile(key, {
92
94
  profile: { ...base, models: [] },
93
95
  defaultConfiguration: emptyCodexConfiguration(),
94
96
  fetchedAt: 0,
@@ -106,6 +108,25 @@ export class CodexRunner extends AbstractRunner {
106
108
  this.latestProfileCache = cached;
107
109
  return cached.profile;
108
110
  }
111
+ cacheProfile(key, cache) {
112
+ const previousTimer = this.profileCacheTimers.get(key);
113
+ if (previousTimer !== undefined)
114
+ clearTimeout(previousTimer);
115
+ this.profileCache.set(key, cache);
116
+ const timer = setTimeout(() => {
117
+ this.profileCacheTimers.delete(key);
118
+ this.profileCache.delete(key);
119
+ for (const [environmentKey, profileKey] of this.profileKeyByEnvironment)
120
+ if (profileKey === key)
121
+ this.profileKeyByEnvironment.delete(environmentKey);
122
+ if (this.selectedProfileKey === key) {
123
+ this.selectedProfileKey = undefined;
124
+ this.latestProfileCache = undefined;
125
+ }
126
+ }, CODEX_PROFILE_ENTRY_TTL_MS);
127
+ timer.unref();
128
+ this.profileCacheTimers.set(key, timer);
129
+ }
109
130
  async fetchCodexProfile(base, environment) {
110
131
  const client = this.profileClientFactory();
111
132
  this.profileClients.add(client);
@@ -248,10 +269,13 @@ export class CodexRunner extends AbstractRunner {
248
269
  await this.start();
249
270
  return extractCodexThreads(await this.listThreads()).find((thread) => thread.id === nativeSessionId)?.title;
250
271
  }
251
- nativeImageRoot(session) {
252
- return session.externalSessionId === null
253
- ? undefined
254
- : resolve(homedir(), '.codex', 'generated_images', session.externalSessionId);
272
+ nativeImageRoots(session) {
273
+ return [
274
+ session.cwd,
275
+ ...(session.externalSessionId === null
276
+ ? []
277
+ : [resolve(homedir(), '.codex', 'generated_images', session.externalSessionId)])
278
+ ];
255
279
  }
256
280
  managedRunForNativeTurn(nativeTurnId) {
257
281
  return this.execution.managedTurns.get(nativeTurnId)?.runId;
@@ -313,11 +337,10 @@ export class CodexRunner extends AbstractRunner {
313
337
  }
314
338
  return { activity: 'IDLE', session: context.create(external) };
315
339
  }
316
- catch (error) {
340
+ catch {
317
341
  return {
318
342
  activity: 'UNAVAILABLE',
319
- failureCode: 'RUNNER_RESUME_FAILED',
320
- failureDetail: externalResumeFailureDetail(error)
343
+ failureCode: 'RUNNER_RESUME_FAILED'
321
344
  };
322
345
  }
323
346
  }
@@ -464,6 +487,13 @@ export class CodexRunner extends AbstractRunner {
464
487
  this.client.clearEnvironment();
465
488
  this.environmentFingerprint = undefined;
466
489
  this.environmentRuns.clear();
490
+ for (const timer of this.profileCacheTimers.values())
491
+ clearTimeout(timer);
492
+ this.profileCacheTimers.clear();
493
+ this.profileCache.clear();
494
+ this.profileKeyByEnvironment.clear();
495
+ this.latestProfileCache = undefined;
496
+ this.selectedProfileKey = undefined;
467
497
  }
468
498
  request(method, params) {
469
499
  return this.client.request(method, params);
@@ -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')
@@ -128,7 +128,7 @@ function partItem(session, turnId, raw, planMode, startedAt, completedAt, sequen
128
128
  }, startedAt, completedAt, sequence);
129
129
  }
130
130
  export function openCodeQuestions(value) {
131
- if (!Array.isArray(value) || value.length === 0 || value.length > 4)
131
+ if (!Array.isArray(value) || value.length === 0)
132
132
  return [];
133
133
  return value.flatMap((value, index) => {
134
134
  const question = record(value);
@@ -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: {
@@ -16,10 +17,13 @@ interface OpenCodeManagedRunHost {
16
17
  cwd: string;
17
18
  externalSessionId?: string;
18
19
  }): void;
20
+ run(runId: string): {
21
+ readonly id: string;
22
+ };
23
+ hasActiveSessionLease(sessionId: string): boolean;
19
24
  promote(sessionId: string, nativeSessionId: string): void;
20
25
  rememberNative(nativeSessionId: string, sessionId: string): void;
21
26
  send(type: string, payload: unknown): void;
22
- captureSnapshot(runId: string, cwd: string): Promise<void>;
23
27
  createApproval(input: {
24
28
  readonly runId: string;
25
29
  readonly approvalId: string;
@@ -40,7 +44,7 @@ interface OpenCodeManagedRunHost {
40
44
  }
41
45
  type OpenCodePublicItem = {
42
46
  readonly itemId: string;
43
- readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'file_change' | 'unknown';
47
+ readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'error' | 'unknown';
44
48
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
45
49
  readonly payload: Record<string, unknown>;
46
50
  readonly merge?: boolean;
@@ -59,6 +63,8 @@ export declare class OpenCodeManagedRunController {
59
63
  executeCommand(session: NodeAgentSession, command: RunnerCommandDescriptor, context: {
60
64
  readonly secretEnvironment: Readonly<Record<string, string>>;
61
65
  }): Promise<unknown>;
66
+ private compact;
67
+ private runCompact;
62
68
  start(envelope: NodeEnvelope): void;
63
69
  cancel(runId: string, intent: 'CANCEL' | 'INTERRUPT'): boolean;
64
70
  decideApproval(input: {
@@ -1,6 +1,7 @@
1
1
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
2
+ import { runnerErrorProjection } from '../../util/runner-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;
@@ -18,7 +19,7 @@ export class OpenCodeManagedRunController {
18
19
  }
19
20
  async executeCommand(session, command, context) {
20
21
  if (command.id === 'compact')
21
- return this.runner.executeCompact(session, context.secretEnvironment);
22
+ return this.compact(session, context.secretEnvironment);
22
23
  if (command.id !== 'plan')
23
24
  throw new Error('RUNNER_COMMAND_UNAVAILABLE');
24
25
  const current = this.host.commandState(session.id, 'plan');
@@ -29,13 +30,60 @@ 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: '执行', 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
  }
40
+ compact(session, environment) {
41
+ if (session.externalSessionId === null ||
42
+ session.model === null ||
43
+ session.nativeControl !== 'MAR_MANAGED' ||
44
+ this.host.hasActiveSessionLease(session.id))
45
+ throw new Error('RUNNER_COMMAND_UNAVAILABLE');
46
+ const runId = crypto.randomUUID();
47
+ const client = this.runner.managedClient(environment);
48
+ this.host.adopt({
49
+ runId,
50
+ sessionId: session.id,
51
+ runner: 'opencode',
52
+ cwd: session.cwd,
53
+ externalSessionId: session.externalSessionId
54
+ });
55
+ this.host.active.add(runId);
56
+ this.runner.execution.set(runId, {
57
+ runId,
58
+ sessionId: session.id,
59
+ nativeSessionId: session.externalSessionId,
60
+ cwd: session.cwd,
61
+ client,
62
+ abort: new AbortController(),
63
+ access: session.access ?? 'default',
64
+ agent: 'build'
65
+ });
66
+ this.host.emit(runId, 'run.started', { command: 'compact' }, 'STARTING');
67
+ void this.runCompact(runId, session.model);
68
+ return {
69
+ command: 'compact',
70
+ run: this.host.run(runId),
71
+ states: this.host.commandStates(session.id)
72
+ };
73
+ }
74
+ async runCompact(runId, model) {
75
+ const execution = this.runner.execution.get(runId);
76
+ if (execution === undefined)
77
+ return;
78
+ this.host.emit(runId, 'run.running', { command: 'compact' }, 'RUNNING');
79
+ try {
80
+ await execution.client.summarize(execution.cwd, execution.nativeSessionId, model);
81
+ this.finish(runId, 'SUCCEEDED', 'run.completed', { status: 'SUCCEEDED' });
82
+ }
83
+ catch (error) {
84
+ this.finish(runId, 'FAILED', 'run.failed', error, 'OPENCODE_COMPACT_FAILED');
85
+ }
86
+ }
39
87
  start(envelope) {
40
88
  const payload = envelope.payload;
41
89
  if (payload.runner !== 'opencode' ||
@@ -56,7 +104,14 @@ export class OpenCodeManagedRunController {
56
104
  attachments = this.host.parseAttachments(payload.attachments);
57
105
  }
58
106
  catch (error) {
59
- this.host.emit(payload.runId, 'run.rejected', { code: error instanceof Error ? error.message : 'RUNNER_INPUT_INVALID' }, 'FAILED');
107
+ const projected = runnerErrorProjection(error, 'RUNNER_INPUT_INVALID');
108
+ this.host.emitItem(payload.runId, {
109
+ itemId: boundedConversationItemId('runner-error', payload.runId),
110
+ kind: 'error',
111
+ status: 'FAILED',
112
+ payload: { ...projected, recoverable: true, retryAfterMs: null }
113
+ });
114
+ this.host.emit(payload.runId, 'run.rejected', projected, 'FAILED');
60
115
  return;
61
116
  }
62
117
  void this.run({
@@ -122,8 +177,8 @@ export class OpenCodeManagedRunController {
122
177
  return true;
123
178
  }
124
179
  async run(input) {
125
- // OpenCode 的动态 MCP 注册属于 Server 进程状态。加载 MCP Run 使用独立 Server,
126
- // 避免同一工作区的并行会话共享 MCP 进程、配置或浏览器上下文。
180
+ // OpenCode dynamic MCP registration belongs to Server process state. Runs that load MCP use an
181
+ // isolated Server so parallel sessions in one workspace do not share MCP processes or context.
127
182
  const client = this.runner.managedClient(input.environment, hasMcpConfiguration(input.mcp) ? input.runId : undefined);
128
183
  this.startingClients.set(input.runId, client);
129
184
  this.startingCwds.set(input.runId, input.cwd);
@@ -180,18 +235,19 @@ export class OpenCodeManagedRunController {
180
235
  rejectReady(error);
181
236
  return;
182
237
  }
183
- this.finish(input.runId, 'FAILED', 'run.failed', {
184
- code: error instanceof Error ? error.message : 'OPENCODE_EVENT_FAILED'
185
- });
238
+ this.finish(input.runId, 'FAILED', 'run.failed', error, 'OPENCODE_EVENT_FAILED');
186
239
  });
187
240
  await subscriptionReady;
188
241
  this.host.emit(input.runId, 'run.running', {}, 'RUNNING');
189
242
  // The initial SSE snapshot can contain an idle event from before this prompt.
190
243
  this.pendingIdle.delete(input.runId);
244
+ const attachmentPaths = input.attachments.length === 0
245
+ ? []
246
+ : await this.host.persistAttachments(input.cwd, input.runId, input.attachments);
191
247
  await client.prompt({
192
248
  directory: input.cwd,
193
249
  sessionID: nativeSessionId,
194
- text: input.input,
250
+ text: openCodeImageInput(input.input, attachmentPaths),
195
251
  model: input.model,
196
252
  variant: input.effort,
197
253
  agent: input.agent,
@@ -202,9 +258,7 @@ export class OpenCodeManagedRunController {
202
258
  this.finish(input.runId, 'SUCCEEDED', 'run.completed', { status: 'SUCCEEDED' });
203
259
  }
204
260
  catch (error) {
205
- this.finish(input.runId, 'FAILED', 'run.failed', {
206
- code: error instanceof Error ? error.message : 'OPENCODE_RUN_START_FAILED'
207
- });
261
+ this.finish(input.runId, 'FAILED', 'run.failed', error, 'OPENCODE_RUN_START_FAILED');
208
262
  }
209
263
  }
210
264
  event(runId, nativeSessionId, value) {
@@ -227,7 +281,7 @@ export class OpenCodeManagedRunController {
227
281
  return;
228
282
  }
229
283
  if (event?.type === 'session.error') {
230
- this.finish(runId, 'FAILED', 'run.failed', { code: 'OPENCODE_SESSION_ERROR' });
284
+ this.finish(runId, 'FAILED', 'run.failed', properties?.error ?? properties);
231
285
  return;
232
286
  }
233
287
  if (event?.type === 'message.part.updated') {
@@ -305,7 +359,7 @@ export class OpenCodeManagedRunController {
305
359
  if (execution === undefined || requestId === undefined)
306
360
  return;
307
361
  const questions = openCodeQuestions(rawQuestions);
308
- if (questions.length === 0 || questions.length > 4 || this.questions.has(requestId))
362
+ if (questions.length === 0 || this.questions.has(requestId))
309
363
  return;
310
364
  const timer = setTimeout(() => {
311
365
  const pending = this.questions.get(requestId);
@@ -336,7 +390,7 @@ export class OpenCodeManagedRunController {
336
390
  payload: { requestId, questions }
337
391
  });
338
392
  }
339
- finish(runId, status, eventType, payload) {
393
+ finish(runId, status, eventType, payload, fallbackCode = 'OPENCODE_RUN_FAILED') {
340
394
  const execution = this.runner.execution.get(runId);
341
395
  const cwd = execution?.cwd ?? this.startingCwds.get(runId);
342
396
  const client = execution?.client ?? this.startingClients.get(runId);
@@ -359,13 +413,27 @@ export class OpenCodeManagedRunController {
359
413
  clearTimeout(pending.timer);
360
414
  this.questions.delete(id);
361
415
  }
416
+ if (status === 'FAILED') {
417
+ const projected = runnerErrorProjection(payload, fallbackCode);
418
+ this.host.emitItem(runId, {
419
+ itemId: boundedConversationItemId('runner-error', runId),
420
+ kind: 'error',
421
+ status: 'FAILED',
422
+ payload: { ...projected, recoverable: true, retryAfterMs: null }
423
+ });
424
+ payload = projected;
425
+ }
362
426
  this.host.emit(runId, eventType, payload, status);
363
427
  if (client !== undefined)
364
428
  this.runner.releaseManagedClient(client);
365
- if (cwd !== undefined)
366
- void this.host.captureSnapshot(runId, cwd);
367
429
  }
368
430
  }
431
+ function openCodeImageInput(input, attachmentPaths) {
432
+ if (attachmentPaths.length === 0)
433
+ return input;
434
+ const paths = attachmentPaths.map((path) => `- ${JSON.stringify(path)}`).join('\n');
435
+ 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}`;
436
+ }
369
437
  function hasMcpConfiguration(value) {
370
438
  return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
371
439
  }
@@ -425,14 +493,18 @@ function openCodePart(part, planMode = false) {
425
493
  itemId,
426
494
  kind: 'context_compaction',
427
495
  status: 'COMPLETED',
428
- payload: { phase: 'COMPLETED', summary: '上下文已整理' }
496
+ payload: {
497
+ phase: 'COMPLETED',
498
+ summary: 'Context compacted',
499
+ summaryCode: 'CONTEXT_COMPACTED'
500
+ }
429
501
  };
430
502
  if (part.type === 'patch')
431
503
  return {
432
504
  itemId,
433
- kind: 'file_change',
505
+ kind: 'tool_call',
434
506
  status: 'COMPLETED',
435
- payload: { changes: openCodePatchChanges(part.files) },
507
+ payload: openCodePatchToolPayload(),
436
508
  merge: true
437
509
  };
438
510
  if (part.type === 'step-start' || part.type === 'step-finish')
@@ -19,6 +19,7 @@ export declare class OpenCodeRunner extends AbstractRunner<'opencode'> {
19
19
  readonly discoveryScope: "WORKSPACE_REQUIRED";
20
20
  private readonly modelsByWorkspace;
21
21
  private readonly contextLimitsByWorkspace;
22
+ private readonly profileTimersByWorkspace;
22
23
  readonly execution: Map<string, OpenCodeManagedExecution>;
23
24
  readonly managedTurns: Map<string, string>;
24
25
  private readonly environmentClients;
@@ -56,6 +57,8 @@ export declare class OpenCodeRunner extends AbstractRunner<'opencode'> {
56
57
  maxTokens: number;
57
58
  } | undefined>;
58
59
  stop(): Promise<void>;
60
+ private scheduleProfileExpiry;
61
+ private clearProfileTimer;
59
62
  managedClient(environment: Readonly<Record<string, string>>, isolationKey?: string): OpenCodeServerClient;
60
63
  releaseManagedClient(client: OpenCodeServerClient): void;
61
64
  }
@@ -5,6 +5,7 @@ import { AbstractRunner } from './abstract-runner.js';
5
5
  import { isWithinWorkspace } from './codex/conversation-parser.js';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { openCodeConversationPage } from './opencode/conversation-parser.js';
8
+ const PROFILE_ENTRY_TTL_MS = 30 * 60_000;
8
9
  export class OpenCodeRunner extends AbstractRunner {
9
10
  client;
10
11
  clientFactory;
@@ -12,6 +13,7 @@ export class OpenCodeRunner extends AbstractRunner {
12
13
  discoveryScope = 'WORKSPACE_REQUIRED';
13
14
  modelsByWorkspace = new Map();
14
15
  contextLimitsByWorkspace = new Map();
16
+ profileTimersByWorkspace = new Map();
15
17
  execution = new Map();
16
18
  managedTurns = new Map();
17
19
  environmentClients = new Map();
@@ -39,11 +41,13 @@ export class OpenCodeRunner extends AbstractRunner {
39
41
  const models = openCodeProfileModels(catalog);
40
42
  this.modelsByWorkspace.set(workspace.path, models);
41
43
  this.contextLimitsByWorkspace.set(workspace.path, openCodeContextLimits(catalog));
44
+ this.scheduleProfileExpiry(workspace.path);
42
45
  return { ...this.profile(capabilities), models };
43
46
  }
44
47
  catch {
45
48
  this.modelsByWorkspace.delete(workspace.path);
46
49
  this.contextLimitsByWorkspace.delete(workspace.path);
50
+ this.clearProfileTimer(workspace.path);
47
51
  }
48
52
  finally {
49
53
  this.releaseManagedClient(client);
@@ -279,6 +283,11 @@ export class OpenCodeRunner extends AbstractRunner {
279
283
  execution.abort.abort();
280
284
  this.execution.clear();
281
285
  this.managedTurns.clear();
286
+ for (const timer of this.profileTimersByWorkspace.values())
287
+ clearTimeout(timer);
288
+ this.profileTimersByWorkspace.clear();
289
+ this.modelsByWorkspace.clear();
290
+ this.contextLimitsByWorkspace.clear();
282
291
  const stopping = [
283
292
  this.client.stop(),
284
293
  ...[...this.environmentClients.values()].map(({ client }) => client.stop()),
@@ -288,6 +297,22 @@ export class OpenCodeRunner extends AbstractRunner {
288
297
  await Promise.allSettled(stopping);
289
298
  this.stoppingClients.clear();
290
299
  }
300
+ scheduleProfileExpiry(workspacePath) {
301
+ this.clearProfileTimer(workspacePath);
302
+ const timer = setTimeout(() => {
303
+ this.profileTimersByWorkspace.delete(workspacePath);
304
+ this.modelsByWorkspace.delete(workspacePath);
305
+ this.contextLimitsByWorkspace.delete(workspacePath);
306
+ }, PROFILE_ENTRY_TTL_MS);
307
+ timer.unref();
308
+ this.profileTimersByWorkspace.set(workspacePath, timer);
309
+ }
310
+ clearProfileTimer(workspacePath) {
311
+ const timer = this.profileTimersByWorkspace.get(workspacePath);
312
+ if (timer !== undefined)
313
+ clearTimeout(timer);
314
+ this.profileTimersByWorkspace.delete(workspacePath);
315
+ }
291
316
  managedClient(environment, isolationKey) {
292
317
  const entries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
293
318
  if (entries.length === 0 && isolationKey === undefined)