@myagentroam/node 0.9.2 → 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 (69) hide show
  1. package/dist/connector.js +12 -13
  2. package/dist/control-outbound-scheduler.d.ts +4 -1
  3. package/dist/control-outbound-scheduler.js +39 -6
  4. package/dist/database.d.ts +0 -19
  5. package/dist/database.js +1 -62
  6. package/dist/event-buffer.d.ts +2 -5
  7. package/dist/event-buffer.js +3 -12
  8. package/dist/native-session-history.js +33 -12
  9. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  10. package/dist/runner/claude/managed-run-controller.js +17 -12
  11. package/dist/runner/codex/conversation-parser.d.ts +1 -1
  12. package/dist/runner/codex/conversation-parser.js +3 -0
  13. package/dist/runner/codex/managed-run-controller.d.ts +2 -2
  14. package/dist/runner/codex/managed-run-controller.js +22 -7
  15. package/dist/runner/codex-runner.d.ts +2 -0
  16. package/dist/runner/codex-runner.js +30 -2
  17. package/dist/runner/opencode/conversation-parser.js +1 -1
  18. package/dist/runner/opencode/managed-run-controller.d.ts +7 -2
  19. package/dist/runner/opencode/managed-run-controller.js +72 -14
  20. package/dist/runner/opencode-runner.d.ts +3 -0
  21. package/dist/runner/opencode-runner.js +25 -0
  22. package/dist/runner-profiles.js +10 -6
  23. package/dist/runtime-state.d.ts +5 -12
  24. package/dist/runtime-state.js +32 -46
  25. package/dist/service/attachment-domain-state.d.ts +2 -0
  26. package/dist/service/attachment-domain-state.js +2 -0
  27. package/dist/service/conversation-segment-service.js +10 -9
  28. package/dist/service/external-session-resume-service.js +9 -0
  29. package/dist/service/fake-managed-run-service.d.ts +0 -1
  30. package/dist/service/fake-managed-run-service.js +0 -2
  31. package/dist/service/native-session-projection-service.js +5 -0
  32. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  33. package/dist/service/node-connection-lifecycle-service.js +0 -1
  34. package/dist/service/node-control-channel.js +1 -1
  35. package/dist/service/node-control-message-service.d.ts +0 -1
  36. package/dist/service/node-control-message-service.js +9 -6
  37. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  38. package/dist/service/node-workspace-coordinator.js +2 -12
  39. package/dist/service/run-attachment-service.js +30 -6
  40. package/dist/service/run-event-service.d.ts +1 -0
  41. package/dist/service/run-event-service.js +69 -0
  42. package/dist/service/run-workbench-service.d.ts +1 -1
  43. package/dist/service/run-workbench-service.js +1 -5
  44. package/dist/service/runner-interaction-service.d.ts +1 -2
  45. package/dist/service/runner-interaction-service.js +1 -4
  46. package/dist/service/session-context-service.d.ts +1 -0
  47. package/dist/service/session-context-service.js +14 -0
  48. package/dist/service/session-domain-state.d.ts +1 -0
  49. package/dist/service/session-domain-state.js +1 -0
  50. package/dist/service/session-message-service.js +5 -10
  51. package/dist/service/skill-directory-service.d.ts +3 -1
  52. package/dist/service/skill-directory-service.js +28 -4
  53. package/dist/service/skill-node-operation-service.d.ts +1 -1
  54. package/dist/service/skill-node-operation-service.js +4 -2
  55. package/dist/service/workbench-event-service.d.ts +1 -13
  56. package/dist/service/workbench-event-service.js +2 -36
  57. package/dist/service/workbench-manifest-service.d.ts +0 -1
  58. package/dist/service/workbench-manifest-service.js +0 -1
  59. package/dist/service/workspace-domain-state.d.ts +1 -0
  60. package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
  61. package/dist/service/workspace-queue-workbench-service.js +1 -0
  62. package/dist/service/workspace-upload-service.d.ts +2 -0
  63. package/dist/service/workspace-upload-service.js +28 -6
  64. package/dist/service/workspace-workbench-service.d.ts +0 -3
  65. package/dist/service/workspace-workbench-service.js +0 -17
  66. package/dist/util/runner-error.d.ts +6 -0
  67. package/dist/util/runner-error.js +52 -0
  68. package/dist/util/runner-native-session-parsers.js +43 -6
  69. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import type { NodeAgentSession, NodeConversationItem, NodeConversationTurn, NodeMessageAttachmentInput } from '../../database.js';
2
2
  export type PublicConversationItem = {
3
3
  readonly itemId: string;
4
- readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'unknown';
4
+ readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
5
5
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'DECLINED';
6
6
  readonly payload: Record<string, unknown>;
7
7
  readonly merge?: boolean;
@@ -83,6 +83,7 @@ export function codexOfficialTurn(session, value, sequence, managedTurn) {
83
83
  const consumedOfficialIds = new Set();
84
84
  const officialUser = officialItems.find((item) => item.kind === 'user_message');
85
85
  const officialAssistants = officialItems.filter((item) => item.kind === 'assistant_message');
86
+ const officialContextCompactions = officialItems.filter((item) => item.kind === 'context_compaction');
86
87
  const officialItemMarker = `:official:${value.id}:`;
87
88
  const officialByNativeId = new Map(officialItems.flatMap((item) => {
88
89
  const markerIndex = item.id.lastIndexOf(officialItemMarker);
@@ -95,6 +96,8 @@ export function codexOfficialTurn(session, value, sequence, managedTurn) {
95
96
  let replacement;
96
97
  if (managedItem.kind === 'user_message')
97
98
  replacement = officialUser;
99
+ else if (managedItem.kind === 'context_compaction')
100
+ replacement = officialContextCompactions.find((item) => !consumedOfficialIds.has(item.id));
98
101
  else {
99
102
  const nativeId = conversationItemNativeId(managedItem.id);
100
103
  if (nativeId !== undefined)
@@ -44,14 +44,13 @@ export interface CodexManagedRunHost<TAttachment> {
44
44
  }): void;
45
45
  emitItem(runId: string, item: {
46
46
  readonly itemId: string;
47
- readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'unknown';
47
+ readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
48
48
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'DECLINED';
49
49
  readonly payload: Record<string, unknown>;
50
50
  readonly merge?: boolean;
51
51
  readonly append?: 'sections' | 'outputPreview' | 'outputSummary';
52
52
  }): void;
53
53
  appendImages(runId: string, images: readonly NodeMessageAttachmentInput[]): readonly Record<string, unknown>[];
54
- captureSnapshot(runId: string, cwd: string): Promise<void>;
55
54
  syncTitle(sessionId: string, nativeSessionId: string, cwd: string): Promise<void>;
56
55
  commandState(sessionId: string, commandId: string): RunnerCommandState | undefined;
57
56
  setCommandState(sessionId: string, state: RunnerCommandState): void;
@@ -76,5 +75,6 @@ export declare class CodexManagedRunController<TAttachment> {
76
75
  private requestUserInput;
77
76
  start(envelope: NodeEnvelope): void;
78
77
  private run;
78
+ private emitError;
79
79
  }
80
80
  export {};
@@ -1,6 +1,7 @@
1
1
  import { extractId } from '../../util/runner-native-session-parsers.js';
2
2
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
3
3
  import { safeErrorCode } from '../../util/safe-error.js';
4
+ import { runnerErrorProjection } from '../../util/runner-error.js';
4
5
  import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
5
6
  export class CodexManagedRunController {
6
7
  runner;
@@ -84,7 +85,8 @@ export class CodexManagedRunController {
84
85
  };
85
86
  }
86
87
  catch (error) {
87
- this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, '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');
88
90
  this.runner.execution.runs.delete(runId);
89
91
  this.runner.releaseEnvironment(runId);
90
92
  this.host.removeActive(runId);
@@ -184,16 +186,18 @@ export class CodexManagedRunController {
184
186
  if (notification.method === 'turn/completed') {
185
187
  const status = turn?.status;
186
188
  const terminal = status === 'completed' ? 'SUCCEEDED' : status === 'interrupted' ? 'INTERRUPTED' : 'FAILED';
187
- 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);
188
195
  const active = this.runner.execution.runs.get(runId);
189
196
  const titleRefresh = active === undefined ||
190
197
  active.refreshTitle === false ||
191
198
  this.host.session(active.sessionId)?.titleSource !== 'AUTO'
192
199
  ? undefined
193
200
  : this.host.syncTitle(active.sessionId, active.threadId, active.cwd);
194
- if (active !== undefined) {
195
- void this.host.captureSnapshot(runId, active.cwd);
196
- }
197
201
  this.runner.execution.runs.delete(runId);
198
202
  if (titleRefresh === undefined)
199
203
  this.runner.releaseEnvironment(runId);
@@ -284,7 +288,8 @@ export class CodexManagedRunController {
284
288
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
285
289
  }
286
290
  catch (error) {
287
- this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, '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');
288
293
  return;
289
294
  }
290
295
  this.host.adopt({
@@ -382,10 +387,20 @@ export class CodexManagedRunController {
382
387
  this.runner.releaseEnvironment(runId);
383
388
  return;
384
389
  }
385
- this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, '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');
386
392
  this.runner.execution.runs.delete(runId);
387
393
  this.runner.releaseEnvironment(runId);
388
394
  this.host.removeActive(runId);
389
395
  }
390
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
+ }
391
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;
@@ -11,6 +11,7 @@ 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);
@@ -466,6 +487,13 @@ export class CodexRunner extends AbstractRunner {
466
487
  this.client.clearEnvironment();
467
488
  this.environmentFingerprint = undefined;
468
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;
469
497
  }
470
498
  request(method, params) {
471
499
  return this.client.request(method, params);
@@ -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);
@@ -17,10 +17,13 @@ interface OpenCodeManagedRunHost {
17
17
  cwd: string;
18
18
  externalSessionId?: string;
19
19
  }): void;
20
+ run(runId: string): {
21
+ readonly id: string;
22
+ };
23
+ hasActiveSessionLease(sessionId: string): boolean;
20
24
  promote(sessionId: string, nativeSessionId: string): void;
21
25
  rememberNative(nativeSessionId: string, sessionId: string): void;
22
26
  send(type: string, payload: unknown): void;
23
- captureSnapshot(runId: string, cwd: string): Promise<void>;
24
27
  createApproval(input: {
25
28
  readonly runId: string;
26
29
  readonly approvalId: string;
@@ -41,7 +44,7 @@ interface OpenCodeManagedRunHost {
41
44
  }
42
45
  type OpenCodePublicItem = {
43
46
  readonly itemId: string;
44
- readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'unknown';
47
+ readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'error' | 'unknown';
45
48
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
46
49
  readonly payload: Record<string, unknown>;
47
50
  readonly merge?: boolean;
@@ -60,6 +63,8 @@ export declare class OpenCodeManagedRunController {
60
63
  executeCommand(session: NodeAgentSession, command: RunnerCommandDescriptor, context: {
61
64
  readonly secretEnvironment: Readonly<Record<string, string>>;
62
65
  }): Promise<unknown>;
66
+ private compact;
67
+ private runCompact;
63
68
  start(envelope: NodeEnvelope): void;
64
69
  cancel(runId: string, intent: 'CANCEL' | 'INTERRUPT'): boolean;
65
70
  decideApproval(input: {
@@ -1,5 +1,5 @@
1
1
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
2
- import { safeErrorCode } from '../../util/safe-error.js';
2
+ import { runnerErrorProjection } from '../../util/runner-error.js';
3
3
  import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../codex/conversation-parser.js';
4
4
  import { openCodePatchToolPayload, openCodeQuestions } from './conversation-parser.js';
5
5
  export class OpenCodeManagedRunController {
@@ -19,7 +19,7 @@ export class OpenCodeManagedRunController {
19
19
  }
20
20
  async executeCommand(session, command, context) {
21
21
  if (command.id === 'compact')
22
- return this.runner.executeCompact(session, context.secretEnvironment);
22
+ return this.compact(session, context.secretEnvironment);
23
23
  if (command.id !== 'plan')
24
24
  throw new Error('RUNNER_COMMAND_UNAVAILABLE');
25
25
  const current = this.host.commandState(session.id, 'plan');
@@ -37,6 +37,53 @@ export class OpenCodeManagedRunController {
37
37
  });
38
38
  return { command: command.id, states: this.host.commandStates(session.id) };
39
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
+ }
40
87
  start(envelope) {
41
88
  const payload = envelope.payload;
42
89
  if (payload.runner !== 'opencode' ||
@@ -57,7 +104,14 @@ export class OpenCodeManagedRunController {
57
104
  attachments = this.host.parseAttachments(payload.attachments);
58
105
  }
59
106
  catch (error) {
60
- this.host.emit(payload.runId, 'run.rejected', { code: safeErrorCode(error, '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');
61
115
  return;
62
116
  }
63
117
  void this.run({
@@ -181,9 +235,7 @@ export class OpenCodeManagedRunController {
181
235
  rejectReady(error);
182
236
  return;
183
237
  }
184
- this.finish(input.runId, 'FAILED', 'run.failed', {
185
- code: safeErrorCode(error, 'OPENCODE_EVENT_FAILED')
186
- });
238
+ this.finish(input.runId, 'FAILED', 'run.failed', error, 'OPENCODE_EVENT_FAILED');
187
239
  });
188
240
  await subscriptionReady;
189
241
  this.host.emit(input.runId, 'run.running', {}, 'RUNNING');
@@ -206,9 +258,7 @@ export class OpenCodeManagedRunController {
206
258
  this.finish(input.runId, 'SUCCEEDED', 'run.completed', { status: 'SUCCEEDED' });
207
259
  }
208
260
  catch (error) {
209
- this.finish(input.runId, 'FAILED', 'run.failed', {
210
- code: safeErrorCode(error, 'OPENCODE_RUN_START_FAILED')
211
- });
261
+ this.finish(input.runId, 'FAILED', 'run.failed', error, 'OPENCODE_RUN_START_FAILED');
212
262
  }
213
263
  }
214
264
  event(runId, nativeSessionId, value) {
@@ -231,7 +281,7 @@ export class OpenCodeManagedRunController {
231
281
  return;
232
282
  }
233
283
  if (event?.type === 'session.error') {
234
- this.finish(runId, 'FAILED', 'run.failed', { code: 'OPENCODE_SESSION_ERROR' });
284
+ this.finish(runId, 'FAILED', 'run.failed', properties?.error ?? properties);
235
285
  return;
236
286
  }
237
287
  if (event?.type === 'message.part.updated') {
@@ -309,7 +359,7 @@ export class OpenCodeManagedRunController {
309
359
  if (execution === undefined || requestId === undefined)
310
360
  return;
311
361
  const questions = openCodeQuestions(rawQuestions);
312
- if (questions.length === 0 || questions.length > 4 || this.questions.has(requestId))
362
+ if (questions.length === 0 || this.questions.has(requestId))
313
363
  return;
314
364
  const timer = setTimeout(() => {
315
365
  const pending = this.questions.get(requestId);
@@ -340,7 +390,7 @@ export class OpenCodeManagedRunController {
340
390
  payload: { requestId, questions }
341
391
  });
342
392
  }
343
- finish(runId, status, eventType, payload) {
393
+ finish(runId, status, eventType, payload, fallbackCode = 'OPENCODE_RUN_FAILED') {
344
394
  const execution = this.runner.execution.get(runId);
345
395
  const cwd = execution?.cwd ?? this.startingCwds.get(runId);
346
396
  const client = execution?.client ?? this.startingClients.get(runId);
@@ -363,11 +413,19 @@ export class OpenCodeManagedRunController {
363
413
  clearTimeout(pending.timer);
364
414
  this.questions.delete(id);
365
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
+ }
366
426
  this.host.emit(runId, eventType, payload, status);
367
427
  if (client !== undefined)
368
428
  this.runner.releaseManagedClient(client);
369
- if (cwd !== undefined)
370
- void this.host.captureSnapshot(runId, cwd);
371
429
  }
372
430
  }
373
431
  function openCodeImageInput(input, attachmentPaths) {
@@ -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)
@@ -1,3 +1,7 @@
1
+ const COMMON_COMMAND_DESCRIPTIONS = {
2
+ compact: 'Compact the current session context.',
3
+ plan: 'Toggle collaborative Plan Mode.'
4
+ };
1
5
  const CLAUDE_MODEL_ENVIRONMENT = {
2
6
  opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
3
7
  sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
@@ -35,7 +39,7 @@ const definitions = {
35
39
  {
36
40
  id: 'compact',
37
41
  label: '/compact',
38
- description: 'Compact the current session context.',
42
+ description: COMMON_COMMAND_DESCRIPTIONS.compact,
39
43
  inputHint: '/compact',
40
44
  interaction: 'IMMEDIATE_ACTION'
41
45
  },
@@ -43,7 +47,7 @@ const definitions = {
43
47
  id: 'plan',
44
48
  label: '/plan',
45
49
  stateLabel: 'Plan Mode',
46
- description: 'Toggle collaborative Plan Mode.',
50
+ description: COMMON_COMMAND_DESCRIPTIONS.plan,
47
51
  inputHint: '/plan',
48
52
  interaction: 'TOGGLE'
49
53
  },
@@ -96,7 +100,7 @@ const definitions = {
96
100
  id: 'compact',
97
101
  runner: 'claude-code',
98
102
  label: '/compact',
99
- description: 'Compact context through the Claude Agent SDK.',
103
+ description: COMMON_COMMAND_DESCRIPTIONS.compact,
100
104
  inputHint: '/compact',
101
105
  source: 'CLAUDE_AGENT_SDK',
102
106
  interaction: 'IMMEDIATE_ACTION',
@@ -108,7 +112,7 @@ const definitions = {
108
112
  runner: 'claude-code',
109
113
  label: '/plan',
110
114
  stateLabel: 'Plan Mode',
111
- description: 'Toggle read-only tagged Plan Mode.',
115
+ description: COMMON_COMMAND_DESCRIPTIONS.plan,
112
116
  inputHint: '/plan',
113
117
  source: 'CLAUDE_AGENT_SDK',
114
118
  interaction: 'TOGGLE',
@@ -137,7 +141,7 @@ const definitions = {
137
141
  id: 'compact',
138
142
  runner: 'opencode',
139
143
  label: '/compact',
140
- description: 'Compact context through OpenCode.',
144
+ description: COMMON_COMMAND_DESCRIPTIONS.compact,
141
145
  inputHint: '/compact',
142
146
  source: 'OPENCODE_SERVER',
143
147
  interaction: 'IMMEDIATE_ACTION',
@@ -149,7 +153,7 @@ const definitions = {
149
153
  runner: 'opencode',
150
154
  label: '/plan',
151
155
  stateLabel: 'Plan Mode',
152
- description: 'Toggle the native OpenCode Plan Agent.',
156
+ description: COMMON_COMMAND_DESCRIPTIONS.plan,
153
157
  inputHint: '/plan',
154
158
  source: 'OPENCODE_SERVER',
155
159
  interaction: 'TOGGLE',
@@ -1,5 +1,5 @@
1
1
  import type { AgentRunStatus, RunnerName } from '@myagentroam/protocol';
2
- import type { ConversationItemPage, ConversationPage, NativeSessionControl, NodeAgentRun, NodeAgentSession, NodeApproval, NodeConversationTurn, NodeMessageAttachmentInput, NodeRunEvent, NodeWorkspace, WorkspaceActivityPage } from './database.js';
2
+ import type { ConversationItemPage, ConversationPage, NativeSessionControl, NodeAgentRun, NodeAgentSession, NodeApproval, NodeConversationTurn, NodeMessageAttachmentInput, NodeRunEvent, NodeWorkspace } from './database.js';
3
3
  /**
4
4
  * Deliberately process-local runtime domain. No method in this class writes
5
5
  * into Node SQLite: a Node restart loses active runs, approvals, leases and
@@ -10,22 +10,22 @@ export declare class NodeRuntimeState {
10
10
  private readonly nodeId;
11
11
  private readonly getWorkspace;
12
12
  private readonly now;
13
+ private static readonly MAX_COMPLETED_TURNS_PER_SESSION;
13
14
  private readonly sessions;
14
15
  private readonly runs;
15
16
  private readonly messageRuns;
16
17
  private readonly events;
17
18
  private readonly turns;
18
19
  private readonly approvals;
19
- private readonly workspaceEvents;
20
20
  /** A Session owns at most one running turn; Workspace sessions may run concurrently. */
21
21
  private readonly leases;
22
22
  /** Session FIFO metadata is deliberately process-local with the rest of run state. */
23
23
  private readonly queuedRuns;
24
24
  private readonly queueVersions;
25
25
  private readonly pausedQueues;
26
- private readonly snapshots;
26
+ /** Bounded tombstones reject stale internal start envelopes after terminal cleanup. */
27
+ private readonly terminalRunIds;
27
28
  private nextEventId;
28
- private nextWorkspaceEventId;
29
29
  constructor(nodeId: () => string, getWorkspace: (id: string) => NodeWorkspace | undefined, now?: () => number);
30
30
  activeWorkspaceLeaseCount(): number;
31
31
  activeSessionRun(sessionId: string): NodeAgentRun | undefined;
@@ -138,11 +138,6 @@ export declare class NodeRuntimeState {
138
138
  readonly outputPreview: string;
139
139
  readonly failed?: boolean;
140
140
  }): NodeConversationTurn;
141
- listWorkspaceActivity(input: {
142
- readonly workspaceId: string;
143
- readonly cursor?: string;
144
- readonly limit?: number;
145
- }): WorkspaceActivityPage;
146
141
  createApproval(input: {
147
142
  readonly runId: string;
148
143
  readonly approvalId: string;
@@ -156,8 +151,7 @@ export declare class NodeRuntimeState {
156
151
  expireDueApprovals(): readonly NodeApproval[];
157
152
  listNonterminalRuns(): readonly NodeAgentRun[];
158
153
  lastRunSequence(id: string): number;
159
- saveSnapshot(id: string, value: unknown): void;
160
- getSnapshot(id: string): unknown | undefined;
154
+ removeTerminalRun(runId: string): void;
161
155
  hasActiveWorkspaceLease(workspaceId: string): boolean;
162
156
  /** Removes a terminal Session's process-local projection after its native history is deleted. */
163
157
  removeAgentSession(sessionId: string): void;
@@ -168,7 +162,6 @@ export declare class NodeRuntimeState {
168
162
  private requireSession;
169
163
  private requireRun;
170
164
  private requireApproval;
171
- private recordWorkspaceActivity;
172
165
  private updateTurnForRun;
173
166
  private projectEvent;
174
167
  }