@myagentroam/node 0.9.2 → 0.9.4

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 (81) hide show
  1. package/dist/codex-app-server.d.ts +24 -0
  2. package/dist/codex-app-server.js +100 -5
  3. package/dist/connector.js +18 -15
  4. package/dist/control-outbound-scheduler.d.ts +4 -1
  5. package/dist/control-outbound-scheduler.js +39 -6
  6. package/dist/database.d.ts +2 -19
  7. package/dist/database.js +1 -62
  8. package/dist/event-buffer.d.ts +2 -5
  9. package/dist/event-buffer.js +3 -12
  10. package/dist/native-session-history.js +33 -12
  11. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  12. package/dist/runner/claude/managed-run-controller.js +17 -12
  13. package/dist/runner/codex/conversation-parser.d.ts +1 -1
  14. package/dist/runner/codex/conversation-parser.js +3 -0
  15. package/dist/runner/codex/managed-run-controller.d.ts +11 -2
  16. package/dist/runner/codex/managed-run-controller.js +79 -24
  17. package/dist/runner/codex-runner.d.ts +3 -0
  18. package/dist/runner/codex-runner.js +42 -3
  19. package/dist/runner/opencode/conversation-parser.js +1 -1
  20. package/dist/runner/opencode/managed-run-controller.d.ts +7 -2
  21. package/dist/runner/opencode/managed-run-controller.js +72 -14
  22. package/dist/runner/opencode-runner.d.ts +3 -0
  23. package/dist/runner/opencode-runner.js +25 -0
  24. package/dist/runner-profiles.js +10 -6
  25. package/dist/runtime-state.d.ts +7 -12
  26. package/dist/runtime-state.js +43 -53
  27. package/dist/service/attachment-domain-state.d.ts +2 -0
  28. package/dist/service/attachment-domain-state.js +2 -0
  29. package/dist/service/conversation-history-service.d.ts +8 -0
  30. package/dist/service/conversation-history-service.js +181 -9
  31. package/dist/service/conversation-segment-service.js +17 -10
  32. package/dist/service/external-session-resume-service.js +9 -0
  33. package/dist/service/fake-managed-run-service.d.ts +0 -1
  34. package/dist/service/fake-managed-run-service.js +0 -2
  35. package/dist/service/native-session-projection-service.js +5 -0
  36. package/dist/service/native-session-watch-service.d.ts +8 -2
  37. package/dist/service/native-session-watch-service.js +28 -10
  38. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  39. package/dist/service/node-connection-lifecycle-service.js +0 -1
  40. package/dist/service/node-control-channel.js +1 -1
  41. package/dist/service/node-control-message-service.d.ts +0 -1
  42. package/dist/service/node-control-message-service.js +9 -6
  43. package/dist/service/node-request-service.js +2 -1
  44. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  45. package/dist/service/node-workspace-coordinator.js +2 -12
  46. package/dist/service/run-attachment-service.js +30 -6
  47. package/dist/service/run-event-service.d.ts +1 -0
  48. package/dist/service/run-event-service.js +80 -0
  49. package/dist/service/run-workbench-service.d.ts +1 -1
  50. package/dist/service/run-workbench-service.js +1 -5
  51. package/dist/service/runner-interaction-service.d.ts +1 -2
  52. package/dist/service/runner-interaction-service.js +1 -4
  53. package/dist/service/session-context-service.d.ts +1 -0
  54. package/dist/service/session-context-service.js +14 -0
  55. package/dist/service/session-domain-state.d.ts +1 -0
  56. package/dist/service/session-domain-state.js +1 -0
  57. package/dist/service/session-lifecycle-service.js +4 -12
  58. package/dist/service/session-message-service.d.ts +1 -0
  59. package/dist/service/session-message-service.js +109 -85
  60. package/dist/service/skill-directory-service.d.ts +3 -1
  61. package/dist/service/skill-directory-service.js +28 -4
  62. package/dist/service/skill-node-operation-service.d.ts +1 -1
  63. package/dist/service/skill-node-operation-service.js +4 -2
  64. package/dist/service/workbench-event-service.d.ts +1 -13
  65. package/dist/service/workbench-event-service.js +2 -36
  66. package/dist/service/workbench-manifest-service.d.ts +0 -1
  67. package/dist/service/workbench-manifest-service.js +0 -1
  68. package/dist/service/workspace-change-service.js +1 -1
  69. package/dist/service/workspace-domain-state.d.ts +1 -0
  70. package/dist/service/workspace-queue-workbench-service.d.ts +13 -2
  71. package/dist/service/workspace-queue-workbench-service.js +8 -2
  72. package/dist/service/workspace-upload-service.d.ts +2 -0
  73. package/dist/service/workspace-upload-service.js +28 -6
  74. package/dist/service/workspace-workbench-service.d.ts +0 -3
  75. package/dist/service/workspace-workbench-service.js +0 -17
  76. package/dist/util/runner-error.d.ts +6 -0
  77. package/dist/util/runner-error.js +52 -0
  78. package/dist/util/runner-native-session-parsers.d.ts +2 -1
  79. package/dist/util/runner-native-session-parsers.js +63 -15
  80. package/dist/workspace.js +33 -8
  81. package/package.json +2 -2
package/dist/database.js CHANGED
@@ -552,11 +552,6 @@ export class NodeDatabase {
552
552
  this.raw
553
553
  .prepare('UPDATE legacy_agent_sessions SET last_activity_at = ?, updated_at = ? WHERE id = ?')
554
554
  .run(value.createdAt, value.createdAt, value.sessionId);
555
- this.recordWorkspaceActivity(value.workspaceId, 'run.created', {
556
- runId: value.id,
557
- sessionId: value.sessionId,
558
- runner: value.runner
559
- });
560
555
  this.raw.exec('COMMIT');
561
556
  return { run: value, created: true };
562
557
  }
@@ -632,14 +627,6 @@ export class NodeDatabase {
632
627
  .get(input.runId, input.sequence);
633
628
  const value = event(stored);
634
629
  this.projectRunEvent(value, input.status);
635
- const run = this.getRun(input.runId);
636
- if (run !== undefined) {
637
- this.recordWorkspaceActivity(run.workspaceId, 'run.updated', {
638
- runId: run.id,
639
- eventType: input.eventType,
640
- status: input.status ?? null
641
- });
642
- }
643
630
  return value;
644
631
  }
645
632
  listRunEvents(runId, after = 0) {
@@ -709,25 +696,6 @@ export class NodeDatabase {
709
696
  : null
710
697
  };
711
698
  }
712
- listWorkspaceActivity(input) {
713
- if (this.getWorkspace(input.workspaceId) === undefined)
714
- throw new Error('WORKSPACE_NOT_FOUND');
715
- const limit = pageLimit(input.limit);
716
- const cursor = decodeCursor(input.cursor, 'workspace-activity', input.workspaceId);
717
- const rows = this.raw
718
- .prepare(`SELECT id, workspace_id, event_type, payload, created_at FROM workspace_activity_events
719
- WHERE workspace_id = ? AND (? IS NULL OR id > ?)
720
- ORDER BY id ASC LIMIT ?`)
721
- .all(input.workspaceId, cursor === undefined ? null : cursor.value, cursor?.value ?? 0, limit + 1);
722
- const page = rows.slice(0, limit);
723
- const last = page.at(-1);
724
- return {
725
- events: page.map((row) => workspaceActivityEvent(row)),
726
- nextCursor: rows.length > limit && last !== undefined
727
- ? encodeCursor('workspace-activity', input.workspaceId, numberValue(last, 'id'), '')
728
- : null
729
- };
730
- }
731
699
  createApproval(input) {
732
700
  if (this.getRun(input.runId) === undefined)
733
701
  throw new Error('RUN_NOT_FOUND');
@@ -828,18 +796,6 @@ export class NodeDatabase {
828
796
  .get(runId);
829
797
  return row['sequence'] === null ? 0 : numberValue(row, 'sequence');
830
798
  }
831
- saveSnapshot(runId, snapshot) {
832
- this.raw
833
- .prepare(`INSERT INTO run_snapshots (run_id, snapshot, created_at) VALUES (?, ?, ?)
834
- ON CONFLICT(run_id) DO UPDATE SET snapshot = excluded.snapshot, created_at = excluded.created_at`)
835
- .run(runId, JSON.stringify(snapshot), this.now());
836
- }
837
- getSnapshot(runId) {
838
- const row = this.raw
839
- .prepare('SELECT snapshot FROM run_snapshots WHERE run_id = ?')
840
- .get(runId);
841
- return row === undefined ? undefined : JSON.parse(stringValue(row, 'snapshot'));
842
- }
843
799
  markActiveRunsSuspect() {
844
800
  this.raw
845
801
  .prepare("UPDATE agent_runs SET recovery_state = 'SUSPECT', recovery_updated_at = ? WHERE status NOT IN ('SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED')")
@@ -877,12 +833,6 @@ export class NodeDatabase {
877
833
  .prepare("UPDATE workspace_leases SET state = 'RELEASED', updated_at = ? WHERE run_id = ? AND state IN ('ACTIVE', 'SUSPECT')")
878
834
  .run(this.now(), runId);
879
835
  }
880
- recordWorkspaceActivity(workspaceId, eventType, payload) {
881
- this.raw
882
- .prepare(`INSERT INTO workspace_activity_events (workspace_id, event_type, payload, created_at)
883
- VALUES (?, ?, ?, ?)`)
884
- .run(workspaceId, eventType, JSON.stringify(payload), this.now());
885
- }
886
836
  recordConversationItemChange(sessionId, itemId, revision) {
887
837
  this.raw
888
838
  .prepare(`INSERT INTO conversation_item_events (session_id, item_id, revision, created_at)
@@ -1206,10 +1156,8 @@ function initializeEphemeralRuntimeTables(database) {
1206
1156
  'conversation_turns',
1207
1157
  'conversation_items',
1208
1158
  'conversation_item_events',
1209
- 'workspace_activity_events',
1210
1159
  'approvals',
1211
- 'workspace_leases',
1212
- 'run_snapshots'
1160
+ 'workspace_leases'
1213
1161
  ]);
1214
1162
  for (const raw of NODE_SCHEMA_V1.split(';')) {
1215
1163
  const statement = raw.trim();
@@ -1341,15 +1289,6 @@ function conversationItem(row) {
1341
1289
  completedAt: nullableNumber(row, 'completed_at')
1342
1290
  };
1343
1291
  }
1344
- function workspaceActivityEvent(row) {
1345
- return {
1346
- id: numberValue(row, 'id'),
1347
- workspaceId: stringValue(row, 'workspace_id'),
1348
- eventType: stringValue(row, 'event_type'),
1349
- payload: JSON.parse(stringValue(row, 'payload')),
1350
- createdAt: numberValue(row, 'created_at')
1351
- };
1352
- }
1353
1292
  function pageLimit(value) {
1354
1293
  if (value === undefined)
1355
1294
  return 100;
@@ -1,12 +1,9 @@
1
1
  import { type NodeEnvelope, type WorkbenchEvent } from '@myagentroam/protocol';
2
- /**
3
- * Keeps exactly the page-level business frames that have not received a Server
4
- * ACK. Node-global sequences make a reconnect replay harmless.
5
- */
2
+ /** Keeps live page-level frames only until the current Server connection ACKs them. */
6
3
  export declare class ReliableWorkbenchEventBuffer {
7
4
  private readonly pending;
8
5
  queue(event: WorkbenchEvent): NodeEnvelope;
9
6
  acknowledge(replyTo: string | undefined): boolean;
10
- replay(send: (envelope: NodeEnvelope) => void): void;
7
+ clear(): void;
11
8
  get size(): number;
12
9
  }
@@ -1,8 +1,5 @@
1
1
  import { createEnvelope } from '@myagentroam/protocol';
2
- /**
3
- * Keeps exactly the page-level business frames that have not received a Server
4
- * ACK. Node-global sequences make a reconnect replay harmless.
5
- */
2
+ /** Keeps live page-level frames only until the current Server connection ACKs them. */
6
3
  export class ReliableWorkbenchEventBuffer {
7
4
  pending = new Map();
8
5
  queue(event) {
@@ -13,14 +10,8 @@ export class ReliableWorkbenchEventBuffer {
13
10
  acknowledge(replyTo) {
14
11
  return replyTo !== undefined && this.pending.delete(replyTo);
15
12
  }
16
- replay(send) {
17
- const ordered = [...this.pending.values()].sort((left, right) => {
18
- const a = left.payload;
19
- const b = right.payload;
20
- return a.sequence - b.sequence;
21
- });
22
- for (const envelope of ordered)
23
- send(envelope);
13
+ clear() {
14
+ this.pending.clear();
24
15
  }
25
16
  get size() {
26
17
  return this.pending.size;
@@ -9,7 +9,9 @@ const DISCOVERY_READ_BYTES = 32 * 1024;
9
9
  const TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
10
10
  const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
11
11
  const CLAUDE_CONTEXT_USAGE_READ_BYTES = [CONTEXT_USAGE_READ_BYTES, 1024 * 1024, 4 * 1024 * 1024];
12
+ const TRANSCRIPT_INDEX_TTL_MS = 60 * 60_000;
12
13
  const transcriptIndex = new Map();
14
+ const transcriptIndexTimers = new Map();
13
15
  const discoveryReads = new Map();
14
16
  const DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS = 200_000;
15
17
  const CLAUDE_CONTEXT_WINDOW_ENV = 'MAR_CLAUDE_CONTEXT_WINDOW_TOKENS';
@@ -53,7 +55,7 @@ async function performNativeSessionDiscovery(runner) {
53
55
  if (parsed === undefined)
54
56
  continue;
55
57
  const cwd = canonical(parsed.cwd);
56
- transcriptIndex.set(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
58
+ setTranscriptIndex(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
57
59
  found.set(`${cwd}\u0000${parsed.externalSessionId}`, parsed);
58
60
  }
59
61
  nodeLog('native.session.discovery.completed', {
@@ -83,14 +85,14 @@ async function readDiscoveryHeaders(files, runner) {
83
85
  /** Reads one already-discovered external session again to follow appended JSONL records. */
84
86
  export async function readNativeSession(runner, workspacePath, externalSessionId) {
85
87
  const wanted = canonical(workspacePath);
86
- const path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
88
+ const path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
87
89
  if (path === undefined)
88
90
  return undefined;
89
91
  const parsed = await readTranscript(path, runner, TRANSCRIPT_READ_BYTES);
90
92
  if (parsed === undefined ||
91
93
  parsed.externalSessionId !== externalSessionId ||
92
94
  canonical(parsed.cwd) !== wanted) {
93
- transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
95
+ deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
94
96
  return undefined;
95
97
  }
96
98
  return parsed;
@@ -103,10 +105,10 @@ export async function readNativeSession(runner, workspacePath, externalSessionId
103
105
  */
104
106
  export async function readCodexNativeContextUsage(workspacePath, externalSessionId) {
105
107
  const wanted = canonical(workspacePath);
106
- let path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
108
+ let path = getTranscriptIndex(indexKey('codex', wanted, externalSessionId));
107
109
  if (path === undefined) {
108
110
  await discoverNativeSessions('codex', workspacePath);
109
- path = transcriptIndex.get(indexKey('codex', wanted, externalSessionId));
111
+ path = getTranscriptIndex(indexKey('codex', wanted, externalSessionId));
110
112
  }
111
113
  if (path === undefined)
112
114
  return undefined;
@@ -124,11 +126,11 @@ export async function readCodexNativeContextUsage(workspacePath, externalSession
124
126
  /** Reads the latest actual Claude prompt occupancy without starting a CLI/SDK control stream. */
125
127
  export async function readClaudeNativeContextUsage(workspacePath, externalSessionId, options = {}) {
126
128
  const wanted = canonical(workspacePath);
127
- let path = transcriptIndex.get(indexKey('claude-code', wanted, externalSessionId));
129
+ let path = getTranscriptIndex(indexKey('claude-code', wanted, externalSessionId));
128
130
  path ??= await locateClaudeTranscript(wanted, externalSessionId);
129
131
  if (path === undefined) {
130
132
  await discoverNativeSessions('claude-code', workspacePath);
131
- path = transcriptIndex.get(indexKey('claude-code', wanted, externalSessionId));
133
+ path = getTranscriptIndex(indexKey('claude-code', wanted, externalSessionId));
132
134
  }
133
135
  if (path === undefined)
134
136
  return undefined;
@@ -178,7 +180,7 @@ async function locateClaudeTranscript(wantedCwd, externalSessionId) {
178
180
  const match = parsed.find(({ history }) => history?.externalSessionId === externalSessionId && canonical(history.cwd) === wantedCwd);
179
181
  if (match === undefined)
180
182
  return undefined;
181
- transcriptIndex.set(indexKey('claude-code', wantedCwd, externalSessionId), match.path);
183
+ setTranscriptIndex(indexKey('claude-code', wantedCwd, externalSessionId), match.path);
182
184
  return match.path;
183
185
  }
184
186
  export async function claudeContextWindowTokens(options = {}) {
@@ -207,10 +209,10 @@ export async function claudeContextWindowTokens(options = {}) {
207
209
  */
208
210
  export async function removeNativeSession(runner, workspacePath, externalSessionId) {
209
211
  const wanted = canonical(workspacePath);
210
- let path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
212
+ let path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
211
213
  if (path === undefined) {
212
214
  await discoverNativeSessions(runner, workspacePath);
213
- path = transcriptIndex.get(indexKey(runner, wanted, externalSessionId));
215
+ path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
214
216
  }
215
217
  if (path === undefined)
216
218
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
@@ -218,7 +220,7 @@ export async function removeNativeSession(runner, workspacePath, externalSession
218
220
  if (parsed === undefined ||
219
221
  parsed.externalSessionId !== externalSessionId ||
220
222
  canonical(parsed.cwd) !== wanted) {
221
- transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
223
+ deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
222
224
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
223
225
  }
224
226
  try {
@@ -229,7 +231,7 @@ export async function removeNativeSession(runner, workspacePath, externalSession
229
231
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
230
232
  throw error;
231
233
  }
232
- transcriptIndex.delete(indexKey(runner, wanted, externalSessionId));
234
+ deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
233
235
  }
234
236
  function transcriptRoot(runner) {
235
237
  return runner === 'codex'
@@ -886,6 +888,25 @@ function canonical(value) {
886
888
  function indexKey(runner, cwd, externalSessionId) {
887
889
  return `${runner}\u0000${cwd}\u0000${externalSessionId}`;
888
890
  }
891
+ function setTranscriptIndex(key, path) {
892
+ const previousTimer = transcriptIndexTimers.get(key);
893
+ if (previousTimer !== undefined)
894
+ clearTimeout(previousTimer);
895
+ transcriptIndex.set(key, path);
896
+ const timer = setTimeout(() => deleteTranscriptIndex(key), TRANSCRIPT_INDEX_TTL_MS);
897
+ timer.unref();
898
+ transcriptIndexTimers.set(key, timer);
899
+ }
900
+ function getTranscriptIndex(key) {
901
+ return transcriptIndex.get(key);
902
+ }
903
+ function deleteTranscriptIndex(key) {
904
+ const timer = transcriptIndexTimers.get(key);
905
+ if (timer !== undefined)
906
+ clearTimeout(timer);
907
+ transcriptIndexTimers.delete(key);
908
+ transcriptIndex.delete(key);
909
+ }
889
910
  function isRecord(value) {
890
911
  return typeof value === 'object' && value !== null && !Array.isArray(value);
891
912
  }
@@ -26,7 +26,6 @@ export interface ClaudeManagedRunHost<TAttachment extends ClaudeRunnerImageAttac
26
26
  hasActiveSessionLease(sessionId: string): boolean;
27
27
  session(sessionId: string): NodeAgentSession | undefined;
28
28
  completeCancelled(runId: string, cwd: string): boolean;
29
- captureSnapshot(runId: string, cwd: string): Promise<void>;
30
29
  requestUserInput(runId: string, input: Record<string, unknown>, toolUseId: string): Promise<ReturnType<typeof denyPermission>>;
31
30
  requestApproval(runId: string, toolName: string, input: Record<string, unknown>): Promise<ReturnType<typeof denyPermission>>;
32
31
  rememberNative(nativeSessionId: string, sessionId: string): void;
@@ -1,10 +1,10 @@
1
1
  import { createEnvelope } from '@myagentroam/protocol';
2
2
  import { ClaudeChannelGate } from '../../claude-channel.js';
3
3
  import { boundedConversationItemId, claudePlanText, compactRunnerText, compactRunnerValue, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolResults, extractClaudeToolUses, isClaudeCommandTool } from '../codex/conversation-parser.js';
4
- import { isTerminalRunStatus } from '../../util/node-operation-parsers.js';
5
4
  import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
6
5
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
7
6
  import { safeErrorCode } from '../../util/safe-error.js';
7
+ import { runnerErrorProjection } from '../../util/runner-error.js';
8
8
  export class ClaudeManagedRunController {
9
9
  runner;
10
10
  host;
@@ -116,16 +116,25 @@ export class ClaudeManagedRunController {
116
116
  let terminal = false;
117
117
  let handle;
118
118
  const toolKinds = new Map();
119
- const finish = (status, eventType, eventPayload) => {
119
+ const finish = (status, eventType, eventPayload, failure, fallbackCode = 'CLAUDE_RUN_FAILED') => {
120
120
  if (terminal)
121
121
  return;
122
122
  terminal = true;
123
- this.host.emit(runId, eventType, eventPayload, status);
123
+ const projected = status === 'FAILED'
124
+ ? runnerErrorProjection(failure ?? eventPayload, fallbackCode)
125
+ : undefined;
126
+ if (projected !== undefined) {
127
+ this.host.emitItem(runId, {
128
+ itemId: boundedConversationItemId('runner-error', runId),
129
+ kind: 'error',
130
+ status: 'FAILED',
131
+ payload: { ...projected, recoverable: true, retryAfterMs: null }
132
+ });
133
+ }
134
+ this.host.emit(runId, eventType, projected ?? eventPayload, status);
124
135
  this.runner.execution.runs.delete(runId);
125
136
  handle?.cancel();
126
137
  this.host.active.delete(runId);
127
- if (isTerminalRunStatus(status))
128
- void this.host.captureSnapshot(runId, cwd);
129
138
  };
130
139
  try {
131
140
  const gate = new ClaudeChannelGate(sessionId);
@@ -314,7 +323,7 @@ export class ClaudeManagedRunController {
314
323
  // result completes this AgentRun even though that iterable can
315
324
  // remain open waiting for another user turn; waiting only for
316
325
  // stream closure would leave the WorkspaceLease permanently held.
317
- finish(subtype === 'success' && record.is_error !== true ? 'SUCCEEDED' : 'FAILED', subtype === 'success' && record.is_error !== true ? 'run.completed' : 'run.failed', { subtype });
326
+ finish(subtype === 'success' && record.is_error !== true ? 'SUCCEEDED' : 'FAILED', subtype === 'success' && record.is_error !== true ? 'run.completed' : 'run.failed', { subtype }, record);
318
327
  if (completedNativeSessionId !== undefined)
319
328
  void this.host.syncTitle(sessionId, completedNativeSessionId, cwd);
320
329
  }
@@ -324,14 +333,10 @@ export class ClaudeManagedRunController {
324
333
  this.host.setChannelToken(sessionId, gate.token);
325
334
  this.host.send('session.channel.ready', { sessionId, token: gate.token });
326
335
  this.host.emit(runId, 'run.running', {}, 'RUNNING');
327
- void handle.completed.then(() => finish('SUCCEEDED', 'run.completed', { subtype: 'stream_closed' }), (error) => finish('FAILED', 'run.failed', {
328
- code: safeErrorCode(error, 'CLAUDE_RUN_FAILED')
329
- }));
336
+ void handle.completed.then(() => finish('SUCCEEDED', 'run.completed', { subtype: 'stream_closed' }), (error) => finish('FAILED', 'run.failed', { code: safeErrorCode(error, 'CLAUDE_RUN_FAILED') }, error));
330
337
  }
331
338
  catch (error) {
332
- finish('FAILED', 'run.failed', {
333
- code: safeErrorCode(error, 'CLAUDE_RUN_START_FAILED')
334
- });
339
+ finish('FAILED', 'run.failed', { code: safeErrorCode(error, 'CLAUDE_RUN_START_FAILED') }, error, 'CLAUDE_RUN_START_FAILED');
335
340
  }
336
341
  }
337
342
  }
@@ -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)
@@ -6,6 +6,15 @@ import { CodexRunner } from '../codex-runner.js';
6
6
  type CodexImageInput = Extract<CodexComposerInput, {
7
7
  readonly type: 'localImage';
8
8
  }>;
9
+ interface CodexNotificationRun {
10
+ readonly threadId: string;
11
+ readonly turnId?: string;
12
+ }
13
+ export declare function resolveCodexNotificationRun<T extends CodexNotificationRun>(runs: ReadonlyMap<string, T>, input: {
14
+ readonly threadId?: string;
15
+ readonly turnId?: string;
16
+ readonly allowUnboundFallback?: boolean;
17
+ }): readonly [string, T] | undefined;
9
18
  export interface CodexManagedRunHost<TAttachment> {
10
19
  readonly available: () => boolean;
11
20
  readonly intercepted: () => boolean;
@@ -44,14 +53,13 @@ export interface CodexManagedRunHost<TAttachment> {
44
53
  }): void;
45
54
  emitItem(runId: string, item: {
46
55
  readonly itemId: string;
47
- readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'unknown';
56
+ readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
48
57
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'DECLINED';
49
58
  readonly payload: Record<string, unknown>;
50
59
  readonly merge?: boolean;
51
60
  readonly append?: 'sections' | 'outputPreview' | 'outputSummary';
52
61
  }): void;
53
62
  appendImages(runId: string, images: readonly NodeMessageAttachmentInput[]): readonly Record<string, unknown>[];
54
- captureSnapshot(runId: string, cwd: string): Promise<void>;
55
63
  syncTitle(sessionId: string, nativeSessionId: string, cwd: string): Promise<void>;
56
64
  commandState(sessionId: string, commandId: string): RunnerCommandState | undefined;
57
65
  setCommandState(sessionId: string, state: RunnerCommandState): void;
@@ -76,5 +84,6 @@ export declare class CodexManagedRunController<TAttachment> {
76
84
  private requestUserInput;
77
85
  start(envelope: NodeEnvelope): void;
78
86
  private run;
87
+ private emitError;
79
88
  }
80
89
  export {};
@@ -1,7 +1,27 @@
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';
6
+ export function resolveCodexNotificationRun(runs, input) {
7
+ const entries = [...runs.entries()];
8
+ if (input.turnId !== undefined) {
9
+ const exact = entries.find(([, value]) => value.turnId === input.turnId &&
10
+ (input.threadId === undefined || value.threadId === input.threadId));
11
+ if (exact !== undefined)
12
+ return exact;
13
+ if (input.allowUnboundFallback !== true)
14
+ return undefined;
15
+ // `turn/started` can arrive before the `turn/start` response binds its native Turn ID. Only
16
+ // an unbound Run on the same thread is a valid fallback; an older bound Run is never valid.
17
+ return entries
18
+ .reverse()
19
+ .find(([, value]) => value.threadId === input.threadId && value.turnId === undefined);
20
+ }
21
+ if (input.threadId === undefined)
22
+ return undefined;
23
+ return entries.reverse().find(([, value]) => value.threadId === input.threadId);
24
+ }
5
25
  export class CodexManagedRunController {
6
26
  runner;
7
27
  host;
@@ -84,7 +104,8 @@ export class CodexManagedRunController {
84
104
  };
85
105
  }
86
106
  catch (error) {
87
- this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, 'CODEX_COMPACT_FAILED') }, 'FAILED');
107
+ this.emitError(runId, error, 'CODEX_COMPACT_FAILED');
108
+ this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_COMPACT_FAILED'), 'FAILED');
88
109
  this.runner.execution.runs.delete(runId);
89
110
  this.runner.releaseEnvironment(runId);
90
111
  this.host.removeActive(runId);
@@ -102,8 +123,11 @@ export class CodexManagedRunController {
102
123
  const threadId = typeof params.threadId === 'string' ? params.threadId : undefined;
103
124
  if (turnId === undefined && threadId === undefined)
104
125
  return;
105
- const run = [...this.runner.execution.runs.entries()].find(([, value]) => (turnId !== undefined && value.turnId === turnId) ||
106
- (threadId !== undefined && value.threadId === threadId));
126
+ const run = resolveCodexNotificationRun(this.runner.execution.runs, {
127
+ ...(turnId === undefined ? {} : { turnId }),
128
+ ...(threadId === undefined ? {} : { threadId }),
129
+ ...(notification.method === 'turn/started' ? { allowUnboundFallback: true } : {})
130
+ });
107
131
  if (run === undefined)
108
132
  return;
109
133
  const [runId] = run;
@@ -184,16 +208,18 @@ export class CodexManagedRunController {
184
208
  if (notification.method === 'turn/completed') {
185
209
  const status = turn?.status;
186
210
  const terminal = status === 'completed' ? 'SUCCEEDED' : status === 'interrupted' ? 'INTERRUPTED' : 'FAILED';
187
- this.host.emit(runId, 'run.completed', { status: status ?? 'unknown' }, terminal);
211
+ if (terminal === 'FAILED')
212
+ this.emitError(runId, turn?.error ?? turn, 'CODEX_RUN_FAILED');
213
+ const failure = terminal === 'FAILED'
214
+ ? runnerErrorProjection(turn?.error ?? turn, 'CODEX_RUN_FAILED')
215
+ : { status: status ?? 'unknown' };
216
+ this.host.emit(runId, terminal === 'FAILED' ? 'run.failed' : 'run.completed', failure, terminal);
188
217
  const active = this.runner.execution.runs.get(runId);
189
218
  const titleRefresh = active === undefined ||
190
219
  active.refreshTitle === false ||
191
220
  this.host.session(active.sessionId)?.titleSource !== 'AUTO'
192
221
  ? undefined
193
222
  : this.host.syncTitle(active.sessionId, active.threadId, active.cwd);
194
- if (active !== undefined) {
195
- void this.host.captureSnapshot(runId, active.cwd);
196
- }
197
223
  this.runner.execution.runs.delete(runId);
198
224
  if (titleRefresh === undefined)
199
225
  this.runner.releaseEnvironment(runId);
@@ -284,7 +310,8 @@ export class CodexManagedRunController {
284
310
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
285
311
  }
286
312
  catch (error) {
287
- this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'MESSAGE_ATTACHMENTS_INVALID') }, 'FAILED');
313
+ this.emitError(runId, error, 'CODEX_RUN_FAILED');
314
+ this.host.emit(runId, 'run.rejected', runnerErrorProjection(error, 'MESSAGE_ATTACHMENTS_INVALID'), 'FAILED');
288
315
  return;
289
316
  }
290
317
  this.host.adopt({
@@ -345,28 +372,46 @@ export class CodexManagedRunController {
345
372
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
346
373
  ...(payload.mcpServers === undefined ? {} : { mcpServers: payload.mcpServers })
347
374
  };
348
- const threadResult = typeof payload.externalSessionId === 'string'
349
- ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
350
- : await this.runner.startThread(cwd, configuration);
351
- if (this.host.completeCancelled(runId, cwd)) {
352
- this.runner.releaseEnvironment(runId);
353
- return;
375
+ const collaborationMode = payload.collaborationMode === 'plan' || payload.collaborationMode === 'default'
376
+ ? payload.collaborationMode
377
+ : undefined;
378
+ const turnConfiguration = {
379
+ ...configuration,
380
+ ...(collaborationMode === undefined ? {} : { collaborationMode }),
381
+ ...(payload.serviceTier === 'fast' ? { serviceTier: 'fast' } : {})
382
+ };
383
+ const materializedAttachments = await this.host.materialize(runId, attachments);
384
+ let threadId;
385
+ let turnResult;
386
+ for (let attempt = 0; attempt < 2; attempt += 1) {
387
+ try {
388
+ const threadResult = typeof payload.externalSessionId === 'string'
389
+ ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
390
+ : await this.runner.startThread(cwd, configuration);
391
+ if (this.host.completeCancelled(runId, cwd)) {
392
+ this.runner.releaseEnvironment(runId);
393
+ return;
394
+ }
395
+ threadId = extractId(threadResult, 'thread');
396
+ this.runner.execution.runs.set(runId, { sessionId, threadId, cwd });
397
+ turnResult = await this.runner.startTurn(threadId, input, cwd, turnConfiguration, materializedAttachments);
398
+ break;
399
+ }
400
+ catch (error) {
401
+ this.runner.execution.runs.delete(runId);
402
+ if (attempt === 0 && (await this.runner.recoverMissingRollout(runId, error)))
403
+ continue;
404
+ throw error;
405
+ }
354
406
  }
355
- const threadId = extractId(threadResult, 'thread');
407
+ if (threadId === undefined || turnResult === undefined)
408
+ throw new Error('CODEX_RUN_FAILED');
356
409
  let publicSessionId = sessionId;
357
410
  if (typeof payload.externalSessionId !== 'string') {
358
411
  this.host.promoteSession(sessionId, threadId);
359
412
  publicSessionId = threadId;
360
413
  this.host.send('session.external-id', { sessionId, externalSessionId: threadId });
361
414
  }
362
- this.runner.execution.runs.set(runId, { sessionId: publicSessionId, threadId, cwd });
363
- const turnResult = await this.runner.startTurn(threadId, input, cwd, {
364
- ...configuration,
365
- ...(payload.collaborationMode === 'plan' || payload.collaborationMode === 'default'
366
- ? { collaborationMode: payload.collaborationMode }
367
- : {}),
368
- ...(payload.serviceTier === 'fast' ? { serviceTier: 'fast' } : {})
369
- }, await this.host.materialize(runId, attachments));
370
415
  const turnId = extractId(turnResult, 'turn');
371
416
  this.runner.execution.runs.set(runId, { sessionId: publicSessionId, threadId, turnId, cwd });
372
417
  this.runner.execution.managedTurns.set(turnId, { sessionId: publicSessionId, runId });
@@ -382,10 +427,20 @@ export class CodexManagedRunController {
382
427
  this.runner.releaseEnvironment(runId);
383
428
  return;
384
429
  }
385
- this.host.emit(runId, 'run.failed', { code: safeErrorCode(error, 'CODEX_RUN_FAILED') }, 'FAILED');
430
+ this.emitError(runId, error, 'CODEX_RUN_FAILED');
431
+ this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_RUN_FAILED'), 'FAILED');
386
432
  this.runner.execution.runs.delete(runId);
387
433
  this.runner.releaseEnvironment(runId);
388
434
  this.host.removeActive(runId);
389
435
  }
390
436
  }
437
+ emitError(runId, error, fallbackCode) {
438
+ const projected = runnerErrorProjection(error, fallbackCode);
439
+ this.host.emitItem(runId, {
440
+ itemId: boundedConversationItemId('runner-error', runId),
441
+ kind: 'error',
442
+ status: 'FAILED',
443
+ payload: { ...projected, recoverable: true, retryAfterMs: null }
444
+ });
445
+ }
391
446
  }
@@ -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;
@@ -56,6 +58,7 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
56
58
  presentSession(session: NodeAgentSession, capabilities: NodeCapabilities, context: RunnerSessionPresentationContext): RunnerSessionPresentation;
57
59
  onNotification(listener: (notification: JsonRpcNotification) => void): () => void;
58
60
  start(): Promise<void>;
61
+ recoverMissingRollout(runId: string, error: unknown): Promise<boolean>;
59
62
  prepareEnvironment(environment: Readonly<Record<string, string>>, runId: string): void;
60
63
  releaseEnvironment(runId: string): void;
61
64
  stop(): void;