@myagentroam/node 0.9.0 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/connector.d.ts +2 -0
  2. package/dist/connector.js +50 -25
  3. package/dist/control-outbound-scheduler.d.ts +4 -1
  4. package/dist/control-outbound-scheduler.js +39 -6
  5. package/dist/database.d.ts +0 -19
  6. package/dist/database.js +1 -62
  7. package/dist/event-buffer.d.ts +2 -5
  8. package/dist/event-buffer.js +3 -12
  9. package/dist/native-session-history.js +105 -26
  10. package/dist/runner/abstract-runner.d.ts +1 -1
  11. package/dist/runner/abstract-runner.js +2 -2
  12. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  13. package/dist/runner/claude/managed-run-controller.js +32 -17
  14. package/dist/runner/claude-code-runner.js +3 -4
  15. package/dist/runner/codex/conversation-parser.d.ts +3 -3
  16. package/dist/runner/codex/conversation-parser.js +33 -12
  17. package/dist/runner/codex/managed-run-controller.d.ts +2 -2
  18. package/dist/runner/codex/managed-run-controller.js +26 -10
  19. package/dist/runner/codex-runner.d.ts +3 -1
  20. package/dist/runner/codex-runner.js +40 -10
  21. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  22. package/dist/runner/opencode/conversation-parser.js +16 -16
  23. package/dist/runner/opencode/managed-run-controller.d.ts +8 -2
  24. package/dist/runner/opencode/managed-run-controller.js +94 -22
  25. package/dist/runner/opencode-runner.d.ts +3 -0
  26. package/dist/runner/opencode-runner.js +25 -0
  27. package/dist/runner-command-engine.js +1 -1
  28. package/dist/runner-profiles.js +29 -17
  29. package/dist/runner-usage.js +5 -5
  30. package/dist/runtime-state.d.ts +5 -12
  31. package/dist/runtime-state.js +32 -46
  32. package/dist/service/attachment-domain-state.d.ts +2 -0
  33. package/dist/service/attachment-domain-state.js +2 -0
  34. package/dist/service/conversation-history-service.js +55 -24
  35. package/dist/service/conversation-segment-service.d.ts +20 -0
  36. package/dist/service/conversation-segment-service.js +156 -0
  37. package/dist/service/external-session-resume-service.js +9 -0
  38. package/dist/service/fake-managed-run-service.d.ts +0 -1
  39. package/dist/service/fake-managed-run-service.js +0 -2
  40. package/dist/service/mcp-installation-verifier.js +1 -1
  41. package/dist/service/native-session-projection-service.js +5 -0
  42. package/dist/service/native-session-watch-service.d.ts +1 -0
  43. package/dist/service/native-session-watch-service.js +21 -4
  44. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  45. package/dist/service/node-connection-lifecycle-service.js +0 -1
  46. package/dist/service/node-control-channel.js +1 -1
  47. package/dist/service/node-control-message-service.d.ts +0 -1
  48. package/dist/service/node-control-message-service.js +9 -6
  49. package/dist/service/node-request-service.js +2 -1
  50. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  51. package/dist/service/node-workspace-coordinator.js +2 -12
  52. package/dist/service/run-attachment-service.d.ts +3 -2
  53. package/dist/service/run-attachment-service.js +51 -17
  54. package/dist/service/run-event-service.d.ts +2 -0
  55. package/dist/service/run-event-service.js +77 -2
  56. package/dist/service/run-workbench-service.d.ts +1 -1
  57. package/dist/service/run-workbench-service.js +1 -5
  58. package/dist/service/runner-interaction-service.d.ts +1 -2
  59. package/dist/service/runner-interaction-service.js +1 -4
  60. package/dist/service/session-catalog-service.js +4 -4
  61. package/dist/service/session-context-service.d.ts +1 -0
  62. package/dist/service/session-context-service.js +14 -0
  63. package/dist/service/session-domain-state.d.ts +1 -0
  64. package/dist/service/session-domain-state.js +1 -0
  65. package/dist/service/session-message-service.js +5 -10
  66. package/dist/service/session-presentation-service.d.ts +1 -0
  67. package/dist/service/session-presentation-service.js +8 -1
  68. package/dist/service/session-query-service.d.ts +4 -0
  69. package/dist/service/session-query-service.js +6 -1
  70. package/dist/service/skill-directory-service.d.ts +3 -1
  71. package/dist/service/skill-directory-service.js +31 -30
  72. package/dist/service/skill-install-service.js +4 -74
  73. package/dist/service/skill-node-operation-service.d.ts +1 -1
  74. package/dist/service/skill-node-operation-service.js +6 -3
  75. package/dist/service/workbench-event-service.d.ts +1 -13
  76. package/dist/service/workbench-event-service.js +2 -36
  77. package/dist/service/workbench-manifest-service.d.ts +0 -1
  78. package/dist/service/workbench-manifest-service.js +0 -1
  79. package/dist/service/workspace-domain-state.d.ts +1 -0
  80. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  81. package/dist/service/workspace-git-exclude-service.js +119 -0
  82. package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
  83. package/dist/service/workspace-queue-workbench-service.js +4 -2
  84. package/dist/service/workspace-service.js +2 -0
  85. package/dist/service/workspace-upload-service.d.ts +2 -0
  86. package/dist/service/workspace-upload-service.js +28 -6
  87. package/dist/service/workspace-workbench-service.d.ts +0 -3
  88. package/dist/service/workspace-workbench-service.js +0 -17
  89. package/dist/supervisor.js +2 -1
  90. package/dist/terminal.js +1 -1
  91. package/dist/util/node-operation-parsers.js +2 -2
  92. package/dist/util/runner-error.d.ts +6 -0
  93. package/dist/util/runner-error.js +52 -0
  94. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  95. package/dist/util/runner-native-session-parsers.js +48 -20
  96. package/dist/util/safe-error.d.ts +2 -0
  97. package/dist/util/safe-error.js +5 -0
  98. package/package.json +2 -2
@@ -27,6 +27,7 @@ export declare class NodeConnector {
27
27
  private readonly sessionIdentityService;
28
28
  private readonly nativeSessionProjectionService;
29
29
  private readonly conversationHistoryService;
30
+ private readonly conversationSegmentService;
30
31
  private readonly runnerInteractionService;
31
32
  private readonly runnerChannelMessages;
32
33
  private readonly managedRunnerSessions;
@@ -78,6 +79,7 @@ export declare class NodeConnector {
78
79
  private readonly handleMessage;
79
80
  private readonly emitRunEvent;
80
81
  private readonly emitConversationItem;
82
+ private readonly emitConversationTurn;
81
83
  private readonly emitWorkbenchEvent;
82
84
  private readonly discoverWorkspaceSessions;
83
85
  private readonly invalidateWorkspaceWatchTopics;
package/dist/connector.js CHANGED
@@ -44,6 +44,7 @@ import { WorkspaceQueueWorkbenchService } from './service/workspace-queue-workbe
44
44
  import { SessionIdentityService } from './service/session-identity-service.js';
45
45
  import { NativeSessionProjectionService } from './service/native-session-projection-service.js';
46
46
  import { ConversationHistoryService, hasManagedActiveRun } from './service/conversation-history-service.js';
47
+ import { ConversationSegmentService, conversationSegmentIdentityForItem, conversationSegments } from './service/conversation-segment-service.js';
47
48
  import { RunnerInteractionService } from './service/runner-interaction-service.js';
48
49
  import { SessionLifecycleService } from './service/session-lifecycle-service.js';
49
50
  import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
@@ -51,7 +52,7 @@ import { SessionMessageService } from './service/session-message-service.js';
51
52
  import { SessionCatalogService } from './service/session-catalog-service.js';
52
53
  import { SessionCommandService } from './service/session-command-service.js';
53
54
  import { SessionPresentationService } from './service/session-presentation-service.js';
54
- import { isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
55
+ import { isPlainRecord, isRunnerImageAttachment, parseRunnerImageAttachments } from './runner/codex/conversation-parser.js';
55
56
  export { nodeConnectEndpoint, validatedCapabilities } from './runner/codex/conversation-parser.js';
56
57
  import { NodeControlChannel } from './service/node-control-channel.js';
57
58
  import { NodeControlMessageService } from './service/node-control-message-service.js';
@@ -114,6 +115,7 @@ export class NodeConnector {
114
115
  sessionIdentityService;
115
116
  nativeSessionProjectionService;
116
117
  conversationHistoryService;
118
+ conversationSegmentService;
117
119
  runnerInteractionService;
118
120
  runnerChannelMessages;
119
121
  managedRunnerSessions;
@@ -173,7 +175,12 @@ export class NodeConnector {
173
175
  isBootstrapSession: (sessionId) => this.sessionIdentityService.bootstrapIds.has(sessionId),
174
176
  presentSession: (session) => this.sessionPresentationService.present(session),
175
177
  rejectBootstrap: (sessionId, code) => this.sessionIdentityService.reject(sessionId, code),
176
- emitWorkbench: (category, payload) => this.emitWorkbenchEvent(category, payload),
178
+ emitWorkbench: (category, payload) => {
179
+ if (category === 'conversation' && isPlainRecord(payload) && 'turn' in payload)
180
+ this.emitConversationTurn(payload.turn);
181
+ else
182
+ this.emitWorkbenchEvent(category, payload);
183
+ },
177
184
  cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
178
185
  expireImages: (runId) => this.runAttachmentService.expire(runId),
179
186
  scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
@@ -190,8 +197,9 @@ export class NodeConnector {
190
197
  refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
191
198
  present: (session) => this.sessionPresentationService.present(session),
192
199
  readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
200
+ projectInitialPage: (session, page, _unit, limit) => this.conversationSegmentService.projectInitial(session, page, limit),
193
201
  emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
194
- emitTurn: (turn) => this.emitWorkbenchEvent('conversation', { turn })
202
+ emitTurn: (turn) => this.emitConversationTurn(turn)
195
203
  });
196
204
  constructor(options = {}) {
197
205
  this.config = options.config;
@@ -230,7 +238,7 @@ export class NodeConnector {
230
238
  this.fakeRunner =
231
239
  options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
232
240
  this.fakeScript = options.fakeScript;
233
- this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoot(session));
241
+ this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session));
234
242
  this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory);
235
243
  this.codexRunController = new CodexManagedRunController(this.codexClient, {
236
244
  available: () => this.capabilities.codex.available,
@@ -245,16 +253,15 @@ export class NodeConnector {
245
253
  interrupted: (runId) => this.runState.interruptRequested.has(runId),
246
254
  parseAttachments: parseRunnerImageAttachments,
247
255
  materialize: (runId, attachments) => this.runAttachmentService.materializeCodex(runId, attachments),
248
- completeCancelled: (runId, cwd) => this.runnerInteractionService.completeCancelledRun(runId, cwd),
256
+ completeCancelled: (runId) => this.runnerInteractionService.completeCancelledRun(runId),
249
257
  promoteSession: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
250
258
  send: (type, payload) => this.send(type, payload),
251
259
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
252
260
  cancel: (runId, threadId, turnId, cwd, status) => this.runnerInteractionService.cancelCodexTurn(runId, threadId, turnId, cwd, status),
253
261
  createApproval: (input) => this.runtime.createApproval(input),
254
262
  publishApproval: (input) => this.runEventBridge.publishApproval(input),
255
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
263
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
256
264
  appendImages: (runId, images) => this.runAttachmentService.append(runId, images.filter(isRunnerImageAttachment)),
257
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
258
265
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
259
266
  commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
260
267
  setCommandState: (sessionId, state) => this.commandStates.set(sessionId, state),
@@ -273,13 +280,20 @@ export class NodeConnector {
273
280
  intercepted: () => this.fakeRunner !== undefined,
274
281
  active: this.runState.active,
275
282
  parseAttachments: parseRunnerImageAttachments,
283
+ persistAttachments: (cwd, runId, attachments) => this.runAttachmentService.persistFiles(cwd, `${runId}-opencode-images`, attachments),
276
284
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
277
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
285
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
278
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),
279
294
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
280
295
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
281
296
  send: (type, payload) => this.send(type, payload),
282
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
283
297
  createApproval: (input) => this.runtime.createApproval(input),
284
298
  expireApproval: (approvalId) => {
285
299
  this.runtime.expireApproval(approvalId);
@@ -299,14 +313,13 @@ export class NodeConnector {
299
313
  run: (runId) => this.runtime.getRun(runId),
300
314
  hasActiveSessionLease: (sessionId) => this.runtime.hasActiveSessionLease(sessionId),
301
315
  session: (sessionId) => this.runtime.getAgentSession(sessionId),
302
- completeCancelled: (runId, cwd) => this.runnerInteractionService.completeCancelledRun(runId, cwd),
303
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
316
+ completeCancelled: (runId) => this.runnerInteractionService.completeCancelledRun(runId),
304
317
  requestUserInput: (runId, input, toolUseId) => this.runnerInteractionService.requestClaudeUserInput(runId, input, toolUseId),
305
318
  requestApproval: (runId, toolName, input) => this.runnerInteractionService.requestClaudeApproval(runId, toolName, input),
306
319
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
307
320
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
308
321
  send: (type, payload) => this.send(type, payload),
309
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
322
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
310
323
  syncTitle: (sessionId, nativeSessionId, cwd) => this.managedRunnerSessions.refreshTitle(sessionId, nativeSessionId, cwd),
311
324
  setChannelToken: (sessionId, token) => this.runtime.setChannelToken(sessionId, token),
312
325
  commandState: (sessionId, commandId) => this.commandStates.get(sessionId, commandId),
@@ -330,8 +343,7 @@ export class NodeConnector {
330
343
  claude: () => this.claudeClient,
331
344
  runners: this.runners,
332
345
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
333
- emitItem: (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item),
334
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
346
+ emitItem: (runId, item) => this.emitConversationItem(runId, item),
335
347
  approvalTimeoutMs: this.claudeApprovalTimeoutMs,
336
348
  publishApproval: (input) => this.runEventBridge.publishApproval(input)
337
349
  });
@@ -368,6 +380,7 @@ export class NodeConnector {
368
380
  hasManagedActiveRun: (session) => hasManagedActiveRun(this.runtime, session),
369
381
  setExternalActivity: (sessionId, activity) => this.sessionState.externalActivity.set(sessionId, activity)
370
382
  });
383
+ this.conversationSegmentService = new ConversationSegmentService((session, input) => this.conversationHistoryService.read(session, input));
371
384
  this.sessionContextService = new SessionContextService({
372
385
  runners: this.runners,
373
386
  session: (sessionId) => this.runtime.getAgentSession(sessionId),
@@ -381,6 +394,7 @@ export class NodeConnector {
381
394
  respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
382
395
  deleteQueuedStart: (runId) => this.workspaceQueueWorkbenchService.remove(runId),
383
396
  clearImages: (runId) => this.runAttachmentService.clear(runId),
397
+ cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
384
398
  emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
385
399
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
386
400
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
@@ -392,6 +406,7 @@ export class NodeConnector {
392
406
  resolve: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId),
393
407
  present: (session) => this.sessionPresentationService.present(session),
394
408
  history: (session, input) => this.conversationHistoryService.read(session, input),
409
+ segments: (session, input) => this.conversationSegmentService.read(session, input),
395
410
  context: this.sessionContextService,
396
411
  discover: (runner, workspaceId) => this.nativeSessionProjectionService.discover(runner, workspaceId),
397
412
  workspaceIdForCwd: (cwd) => this.database?.listWorkspaces().find((candidate) => candidate.path === cwd)?.id
@@ -400,8 +415,9 @@ export class NodeConnector {
400
415
  runtime: this.runtime,
401
416
  emit: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
402
417
  clearImages: (runId) => this.runAttachmentService.clear(runId),
418
+ cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
403
419
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
404
- emitConversation: (turn) => this.emitWorkbenchEvent('conversation', { turn }),
420
+ emitConversation: (turn) => this.emitConversationTurn(turn),
405
421
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
406
422
  stopped: () => this.stopped,
407
423
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
@@ -428,7 +444,7 @@ export class NodeConnector {
428
444
  emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
429
445
  emitRun: (run) => this.emitWorkbenchEvent('run', { run }),
430
446
  emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
431
- emitTurn: (turn) => this.emitWorkbenchEvent('conversation', { turn })
447
+ emitTurn: (turn) => this.emitConversationTurn(turn)
432
448
  });
433
449
  this.sessionLifecycleService = new SessionLifecycleService({
434
450
  database: () => {
@@ -554,7 +570,6 @@ export class NodeConnector {
554
570
  state: this.runState,
555
571
  emitRun: (runId, type, payload, status) => this.runEventBridge.emitRun(runId, type, payload, status),
556
572
  publishApproval: (input) => this.runEventBridge.publishApproval(input),
557
- captureSnapshot: (runId, cwd) => this.workspaceCoordinator.captureRunSnapshot(runId, cwd),
558
573
  disconnect: () => this.controlChannel.close(4010, 'FAKE_RUNNER_DISCONNECT')
559
574
  });
560
575
  this.fakeRunner?.bindControl({
@@ -596,7 +611,7 @@ export class NodeConnector {
596
611
  this.terminalChannel.disconnect();
597
612
  if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
598
613
  nodeLog('node.connection.replaced', {
599
- message: '当前 Node 已被同一身份的新实例接管,请检查是否启动了重复实例。'
614
+ message: 'This Node connection was replaced by a newer instance with the same identity. Check for duplicate processes.'
600
615
  });
601
616
  if (code === 4003 || code === 1001)
602
617
  void this.terminalManager.shutdown();
@@ -609,14 +624,13 @@ export class NodeConnector {
609
624
  close: (code, reason) => this.controlChannel.close(code, reason),
610
625
  inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
611
626
  handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
612
- acknowledgeWorkbenchEvent: (replyTo) => this.workbenchEventService.acknowledge(replyTo),
613
627
  reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
614
628
  runners: this.runners,
615
629
  deliverSessionChannelMessage: (envelope) => this.runnerChannelMessages.handle(envelope),
616
630
  completeUncontrolledCancellation: (runId) => {
617
631
  const run = this.runtime.getRun(runId);
618
632
  if (run?.status === 'CANCELLING')
619
- this.runnerInteractionService.completeCancelledRun(runId, undefined);
633
+ this.runnerInteractionService.completeCancelledRun(runId);
620
634
  },
621
635
  send: (type, payload, replyTo) => this.send(type, payload, replyTo)
622
636
  });
@@ -629,7 +643,6 @@ export class NodeConnector {
629
643
  this.workspaceCoordinator = new NodeWorkspaceCoordinator({
630
644
  config: () => this.config,
631
645
  database: () => this.database,
632
- runtime: this.runtime,
633
646
  watches: this.workspaceWatchService,
634
647
  sessions: this.workspaceSessionService,
635
648
  changes: this.workspaceChangeService,
@@ -661,7 +674,6 @@ export class NodeConnector {
661
674
  }),
662
675
  control: this.controlChannel,
663
676
  terminal: this.terminalChannel,
664
- events: this.workbenchEventService,
665
677
  metrics: this.metrics,
666
678
  heartbeatMs: HEARTBEAT_MS,
667
679
  refreshMs: CAPABILITY_REFRESH_MS
@@ -690,7 +702,6 @@ export class NodeConnector {
690
702
  });
691
703
  this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
692
704
  requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
693
- runtime: this.runtime,
694
705
  watch: this.workspaceWatchService,
695
706
  files: this.workspaceFileService,
696
707
  uploads: this.workspaceUploadService,
@@ -704,7 +715,6 @@ export class NodeConnector {
704
715
  this.operationRouter.registerAll(this.workspaceWorkbenchService.operations());
705
716
  this.operationRouter.registerAll(this.runWorkbenchService.operations());
706
717
  this.operationRouter.registerAll(this.nativeSessionWatchService.operations());
707
- this.operationRouter.registerAll(this.workbenchEventService.operations());
708
718
  this.operationRouter.registerAll(this.sessionQueryService.operations());
709
719
  this.operationRouter.registerAll(this.sessionLifecycleService.operations());
710
720
  this.operationRouter.registerAll(this.sessionMessageService.operations());
@@ -781,7 +791,22 @@ export class NodeConnector {
781
791
  /** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
782
792
  handleMessage = (raw) => this.controlMessages.handle(raw);
783
793
  emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
784
- emitConversationItem = (runId, item) => this.runEventBridge.emitRun(runId, 'conversation.item', item);
794
+ emitConversationItem = (runId, item) => {
795
+ this.runEventService.flushPendingText(runId);
796
+ const itemId = isPlainRecord(item) && typeof item.itemId === 'string' ? item.itemId : undefined;
797
+ const itemKind = isPlainRecord(item) && typeof item.kind === 'string' ? item.kind : undefined;
798
+ const turn = this.runtime.conversationTurnForRun(runId);
799
+ const segment = itemId === undefined || turn === undefined
800
+ ? undefined
801
+ : conversationSegmentIdentityForItem(turn, itemId, itemKind);
802
+ this.runEventBridge.emitRun(runId, 'conversation.item', segment === undefined || !isPlainRecord(item)
803
+ ? item
804
+ : { ...item, segmentId: segment.id, segmentIndex: segment.index });
805
+ };
806
+ emitConversationTurn = (turn) => {
807
+ const segments = isPlainRecord(turn) && Array.isArray(turn.items) ? conversationSegments(turn) : [];
808
+ this.emitWorkbenchEvent('conversation', { turn, segments });
809
+ };
785
810
  emitWorkbenchEvent = (category, payload) => this.runEventBridge.emitWorkbench(category, payload);
786
811
  discoverWorkspaceSessions = (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId);
787
812
  invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
@@ -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;