@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
package/dist/connector.js CHANGED
@@ -253,7 +253,7 @@ export class NodeConnector {
253
253
  interrupted: (runId) => this.runState.interruptRequested.has(runId),
254
254
  parseAttachments: parseRunnerImageAttachments,
255
255
  materialize: (runId, attachments) => this.runAttachmentService.materializeCodex(runId, attachments),
256
- completeCancelled: (runId, cwd) => this.runnerInteractionService.completeCancelledRun(runId, cwd),
256
+ completeCancelled: (runId) => this.runnerInteractionService.completeCancelledRun(runId),
257
257
  promoteSession: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
258
258
  send: (type, payload) => this.send(type, payload),
259
259
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
@@ -262,7 +262,6 @@ export class NodeConnector {
262
262
  publishApproval: (input) => this.runEventBridge.publishApproval(input),
263
263
  emitItem: (runId, item) => this.emitConversationItem(runId, item),
264
264
  appendImages: (runId, images) => this.runAttachmentService.append(runId, images.filter(isRunnerImageAttachment)),
265
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
266
265
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
267
266
  commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
268
267
  setCommandState: (sessionId, state) => this.commandStates.set(sessionId, state),
@@ -285,10 +284,16 @@ export class NodeConnector {
285
284
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
286
285
  emitItem: (runId, item) => this.emitConversationItem(runId, item),
287
286
  adopt: (input) => this.runtime.adoptRun(input),
287
+ run: (runId) => {
288
+ const run = this.runtime.getRun(runId);
289
+ if (run === undefined)
290
+ throw new Error('RUN_NOT_FOUND');
291
+ return run;
292
+ },
293
+ hasActiveSessionLease: (sessionId) => this.runtime.hasActiveSessionLease(sessionId),
288
294
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
289
295
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
290
296
  send: (type, payload) => this.send(type, payload),
291
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
292
297
  createApproval: (input) => this.runtime.createApproval(input),
293
298
  expireApproval: (approvalId) => {
294
299
  this.runtime.expireApproval(approvalId);
@@ -308,8 +313,7 @@ export class NodeConnector {
308
313
  run: (runId) => this.runtime.getRun(runId),
309
314
  hasActiveSessionLease: (sessionId) => this.runtime.hasActiveSessionLease(sessionId),
310
315
  session: (sessionId) => this.runtime.getAgentSession(sessionId),
311
- completeCancelled: (runId, cwd) => this.runnerInteractionService.completeCancelledRun(runId, cwd),
312
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
316
+ completeCancelled: (runId) => this.runnerInteractionService.completeCancelledRun(runId),
313
317
  requestUserInput: (runId, input, toolUseId) => this.runnerInteractionService.requestClaudeUserInput(runId, input, toolUseId),
314
318
  requestApproval: (runId, toolName, input) => this.runnerInteractionService.requestClaudeApproval(runId, toolName, input),
315
319
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
@@ -340,7 +344,6 @@ export class NodeConnector {
340
344
  runners: this.runners,
341
345
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
342
346
  emitItem: (runId, item) => this.emitConversationItem(runId, item),
343
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
344
347
  approvalTimeoutMs: this.claudeApprovalTimeoutMs,
345
348
  publishApproval: (input) => this.runEventBridge.publishApproval(input)
346
349
  });
@@ -391,6 +394,7 @@ export class NodeConnector {
391
394
  respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
392
395
  deleteQueuedStart: (runId) => this.workspaceQueueWorkbenchService.remove(runId),
393
396
  clearImages: (runId) => this.runAttachmentService.clear(runId),
397
+ cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
394
398
  emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
395
399
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
396
400
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
@@ -411,6 +415,7 @@ export class NodeConnector {
411
415
  runtime: this.runtime,
412
416
  emit: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
413
417
  clearImages: (runId) => this.runAttachmentService.clear(runId),
418
+ cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
414
419
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
415
420
  emitConversation: (turn) => this.emitConversationTurn(turn),
416
421
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
@@ -565,7 +570,6 @@ export class NodeConnector {
565
570
  state: this.runState,
566
571
  emitRun: (runId, type, payload, status) => this.runEventBridge.emitRun(runId, type, payload, status),
567
572
  publishApproval: (input) => this.runEventBridge.publishApproval(input),
568
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
569
573
  disconnect: () => this.controlChannel.close(4010, 'FAKE_RUNNER_DISCONNECT')
570
574
  });
571
575
  this.fakeRunner?.bindControl({
@@ -620,14 +624,13 @@ export class NodeConnector {
620
624
  close: (code, reason) => this.controlChannel.close(code, reason),
621
625
  inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
622
626
  handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
623
- acknowledgeWorkbenchEvent: (replyTo) => this.workbenchEventService.acknowledge(replyTo),
624
627
  reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
625
628
  runners: this.runners,
626
629
  deliverSessionChannelMessage: (envelope) => this.runnerChannelMessages.handle(envelope),
627
630
  completeUncontrolledCancellation: (runId) => {
628
631
  const run = this.runtime.getRun(runId);
629
632
  if (run?.status === 'CANCELLING')
630
- this.runnerInteractionService.completeCancelledRun(runId, undefined);
633
+ this.runnerInteractionService.completeCancelledRun(runId);
631
634
  },
632
635
  send: (type, payload, replyTo) => this.send(type, payload, replyTo)
633
636
  });
@@ -640,7 +643,6 @@ export class NodeConnector {
640
643
  this.workspaceCoordinator = new NodeWorkspaceCoordinator({
641
644
  config: () => this.config,
642
645
  database: () => this.database,
643
- runtime: this.runtime,
644
646
  watches: this.workspaceWatchService,
645
647
  sessions: this.workspaceSessionService,
646
648
  changes: this.workspaceChangeService,
@@ -672,7 +674,6 @@ export class NodeConnector {
672
674
  }),
673
675
  control: this.controlChannel,
674
676
  terminal: this.terminalChannel,
675
- events: this.workbenchEventService,
676
677
  metrics: this.metrics,
677
678
  heartbeatMs: HEARTBEAT_MS,
678
679
  refreshMs: CAPABILITY_REFRESH_MS
@@ -701,7 +702,6 @@ export class NodeConnector {
701
702
  });
702
703
  this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
703
704
  requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
704
- runtime: this.runtime,
705
705
  watch: this.workspaceWatchService,
706
706
  files: this.workspaceFileService,
707
707
  uploads: this.workspaceUploadService,
@@ -715,7 +715,6 @@ export class NodeConnector {
715
715
  this.operationRouter.registerAll(this.workspaceWorkbenchService.operations());
716
716
  this.operationRouter.registerAll(this.runWorkbenchService.operations());
717
717
  this.operationRouter.registerAll(this.nativeSessionWatchService.operations());
718
- this.operationRouter.registerAll(this.workbenchEventService.operations());
719
718
  this.operationRouter.registerAll(this.sessionQueryService.operations());
720
719
  this.operationRouter.registerAll(this.sessionLifecycleService.operations());
721
720
  this.operationRouter.registerAll(this.sessionMessageService.operations());
@@ -7,20 +7,23 @@ interface WritableControlSocket {
7
7
  /** Keeps control responses responsive while reliable Workbench events are busy. */
8
8
  export declare class ControlOutboundScheduler {
9
9
  private readonly socket;
10
+ private readonly onExpired;
10
11
  private readonly high;
11
12
  private readonly normal;
12
13
  private scheduled;
13
14
  private retryTimer;
15
+ private expiryTimer;
14
16
  private backpressureStartedAt;
15
17
  private backpressureLogged;
16
18
  private maximumHighDepth;
17
19
  private maximumNormalDepth;
18
- constructor(socket: () => WritableControlSocket | undefined);
20
+ constructor(socket: () => WritableControlSocket | undefined, onExpired?: () => void);
19
21
  enqueue(envelope: NodeEnvelope, priority: 'high' | 'normal'): void;
20
22
  reset(): void;
21
23
  private schedule;
22
24
  private flush;
23
25
  private waitForBackpressure;
24
26
  private finishBackpressure;
27
+ private scheduleExpiry;
25
28
  }
26
29
  export {};
@@ -4,19 +4,23 @@ const MAX_BYTES_PER_TURN = 256 * 1024;
4
4
  const SOCKET_NORMAL_DIRECT_WATER_BYTES = 256 * 1024;
5
5
  const SOCKET_HIGH_WATER_BYTES = 1024 * 1024;
6
6
  const BACKPRESSURE_RETRY_MS = 5;
7
+ const OUTBOUND_QUEUE_TTL_MS = 30_000;
7
8
  /** Keeps control responses responsive while reliable Workbench events are busy. */
8
9
  export class ControlOutboundScheduler {
9
10
  socket;
11
+ onExpired;
10
12
  high = [];
11
13
  normal = [];
12
14
  scheduled = false;
13
15
  retryTimer;
16
+ expiryTimer;
14
17
  backpressureStartedAt;
15
18
  backpressureLogged = false;
16
19
  maximumHighDepth = 0;
17
20
  maximumNormalDepth = 0;
18
- constructor(socket) {
21
+ constructor(socket, onExpired = () => undefined) {
19
22
  this.socket = socket;
23
+ this.onExpired = onExpired;
20
24
  }
21
25
  enqueue(envelope, priority) {
22
26
  const socket = this.socket();
@@ -29,9 +33,13 @@ export class ControlOutboundScheduler {
29
33
  socket.send(JSON.stringify(envelope));
30
34
  return;
31
35
  }
32
- (priority === 'high' ? this.high : this.normal).push(envelope);
36
+ (priority === 'high' ? this.high : this.normal).push({
37
+ envelope,
38
+ expiresAt: Date.now() + OUTBOUND_QUEUE_TTL_MS
39
+ });
33
40
  this.maximumHighDepth = Math.max(this.maximumHighDepth, this.high.length);
34
41
  this.maximumNormalDepth = Math.max(this.maximumNormalDepth, this.normal.length);
42
+ this.scheduleExpiry();
35
43
  this.schedule();
36
44
  }
37
45
  reset() {
@@ -41,6 +49,9 @@ export class ControlOutboundScheduler {
41
49
  if (this.retryTimer !== undefined)
42
50
  clearTimeout(this.retryTimer);
43
51
  this.retryTimer = undefined;
52
+ if (this.expiryTimer !== undefined)
53
+ clearTimeout(this.expiryTimer);
54
+ this.expiryTimer = undefined;
44
55
  this.finishBackpressure('reset');
45
56
  }
46
57
  schedule() {
@@ -62,8 +73,8 @@ export class ControlOutboundScheduler {
62
73
  let bytes = 0;
63
74
  while (frames < MAX_FRAMES_PER_TURN) {
64
75
  const queue = this.high.length > 0 ? this.high : this.normal;
65
- const envelope = queue[0];
66
- if (envelope === undefined)
76
+ const queued = queue[0];
77
+ if (queued === undefined)
67
78
  break;
68
79
  // Reliable Workbench events may be replayed in a large burst after a
69
80
  // reconnect. Stop normal traffic early so a later HTTP/control response
@@ -73,7 +84,7 @@ export class ControlOutboundScheduler {
73
84
  this.waitForBackpressure(socket, 'normal');
74
85
  return;
75
86
  }
76
- const serialized = JSON.stringify(envelope);
87
+ const serialized = JSON.stringify(queued.envelope);
77
88
  const size = Buffer.byteLength(serialized);
78
89
  if (frames > 0 && bytes + size > MAX_BYTES_PER_TURN)
79
90
  break;
@@ -86,8 +97,12 @@ export class ControlOutboundScheduler {
86
97
  }
87
98
  if (this.high.length > 0 || this.normal.length > 0)
88
99
  this.schedule();
89
- else
100
+ else {
101
+ if (this.expiryTimer !== undefined)
102
+ clearTimeout(this.expiryTimer);
103
+ this.expiryTimer = undefined;
90
104
  this.finishBackpressure('drained');
105
+ }
91
106
  }
92
107
  waitForBackpressure(socket, level) {
93
108
  this.backpressureStartedAt ??= Date.now();
@@ -118,4 +133,22 @@ export class ControlOutboundScheduler {
118
133
  this.maximumHighDepth = 0;
119
134
  this.maximumNormalDepth = 0;
120
135
  }
136
+ scheduleExpiry() {
137
+ if (this.expiryTimer !== undefined)
138
+ return;
139
+ const expiresAt = Math.min(this.high[0]?.expiresAt ?? Number.POSITIVE_INFINITY, this.normal[0]?.expiresAt ?? Number.POSITIVE_INFINITY);
140
+ if (!Number.isFinite(expiresAt))
141
+ return;
142
+ this.expiryTimer = setTimeout(() => {
143
+ this.expiryTimer = undefined;
144
+ if ((this.high[0]?.expiresAt ?? Number.POSITIVE_INFINITY) > Date.now() &&
145
+ (this.normal[0]?.expiresAt ?? Number.POSITIVE_INFINITY) > Date.now()) {
146
+ this.scheduleExpiry();
147
+ return;
148
+ }
149
+ this.reset();
150
+ this.onExpired();
151
+ }, Math.max(0, expiresAt - Date.now()));
152
+ this.expiryTimer.unref();
153
+ }
121
154
  }
@@ -80,13 +80,6 @@ export interface NodeRunEvent {
80
80
  readonly status?: AgentRunStatus;
81
81
  readonly createdAt: number;
82
82
  }
83
- export interface WorkspaceActivityEvent {
84
- readonly id: number;
85
- readonly workspaceId: string;
86
- readonly eventType: string;
87
- readonly payload: unknown;
88
- readonly createdAt: number;
89
- }
90
83
  export interface ConversationPage {
91
84
  readonly turns: readonly NodeConversationTurn[];
92
85
  readonly nextCursor: string | null;
@@ -132,10 +125,6 @@ export interface NodeMessageAttachmentInput {
132
125
  readonly mime: string;
133
126
  readonly dataBase64: string;
134
127
  }
135
- export interface WorkspaceActivityPage {
136
- readonly events: readonly WorkspaceActivityEvent[];
137
- readonly nextCursor: string | null;
138
- }
139
128
  export interface NodeApproval {
140
129
  readonly id: string;
141
130
  readonly runId: string;
@@ -288,11 +277,6 @@ export declare class NodeDatabase {
288
277
  readonly cursor?: string;
289
278
  readonly limit?: number;
290
279
  }): ConversationItemPage;
291
- listWorkspaceActivity(input: {
292
- readonly workspaceId: string;
293
- readonly cursor?: string;
294
- readonly limit?: number;
295
- }): WorkspaceActivityPage;
296
280
  createApproval(input: {
297
281
  readonly runId: string;
298
282
  readonly approvalId: string;
@@ -307,13 +291,10 @@ export declare class NodeDatabase {
307
291
  decideApproval(id: string, decision: 'APPROVED' | 'REJECTED'): NodeApproval;
308
292
  listNonterminalRuns(): readonly NodeAgentRun[];
309
293
  lastRunSequence(runId: string): number;
310
- saveSnapshot(runId: string, snapshot: unknown): void;
311
- getSnapshot(runId: string): unknown | undefined;
312
294
  markActiveRunsSuspect(): void;
313
295
  hasActiveWorkspaceLease(workspaceId: string): boolean;
314
296
  reconcileRun(runId: string, active: boolean): void;
315
297
  private releaseLease;
316
- private recordWorkspaceActivity;
317
298
  private recordConversationItemChange;
318
299
  private projectApproval;
319
300
  private readConversationTurn;
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
  }