@myagentroam/node 0.11.36 → 0.11.38

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 (145) hide show
  1. package/dist/application/execution-lifecycle.js +54 -0
  2. package/dist/application/node-lifecycle.js +104 -0
  3. package/dist/application/node-operation-admission.js +51 -0
  4. package/dist/application/node-operation-contracts.js +1 -0
  5. package/dist/application/runner-composition.js +22 -0
  6. package/dist/application/session-projection-composition.js +26 -0
  7. package/dist/application/workspace-application.js +61 -0
  8. package/dist/application/workspace-composition.js +21 -0
  9. package/dist/capabilities.js +2 -3
  10. package/dist/codex-app-server.js +1 -471
  11. package/dist/connection/authorization.js +26 -0
  12. package/dist/{service → connection}/direct-transfer-service.js +1 -1
  13. package/dist/connection/endpoints.js +32 -0
  14. package/dist/connector.js +145 -253
  15. package/dist/control-outbound-scheduler.js +3 -0
  16. package/dist/database.js +34 -14
  17. package/dist/git-askpass.js +1 -1
  18. package/dist/git-execution-helper.js +1 -1
  19. package/dist/hosting/hosted-conversation-file-identity.js +27 -0
  20. package/dist/hosting/hosted-conversation-file-operations.js +44 -0
  21. package/dist/hosting/hosted-conversation-node-service.js +974 -0
  22. package/dist/hosting/hosted-conversation-runs.js +599 -0
  23. package/dist/hosting/hosted-conversation-templates.js +280 -0
  24. package/dist/hosting/hosted-conversation-uploads.js +344 -0
  25. package/dist/hosting/hosted-conversation-watch.js +194 -0
  26. package/dist/hosting/runtime-contracts.js +1 -0
  27. package/dist/runner/abstract-runner.js +4 -13
  28. package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
  29. package/dist/runner/claude/conversation-parser.js +2 -1
  30. package/dist/runner/claude/managed-run-controller.js +6 -7
  31. package/dist/runner/claude/message-parsers.js +2 -1
  32. package/dist/runner/claude/native-transcript.js +2 -1
  33. package/dist/runner/claude/tool-normalizer.js +1 -1
  34. package/dist/runner/claude-code-runner.js +2 -2
  35. package/dist/runner/codex/codex-app-server.js +471 -0
  36. package/dist/runner/codex/conversation-parser.js +4 -2
  37. package/dist/runner/codex/managed-run-controller.js +7 -9
  38. package/dist/runner/codex/native-transcript.js +1 -1
  39. package/dist/runner/codex/tool-normalizer.js +1 -1
  40. package/dist/runner/codex-runner.js +9 -27
  41. package/dist/runner/contracts.js +1 -0
  42. package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
  43. package/dist/runner/mar-agent/history-projection.js +1 -1
  44. package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
  45. package/dist/runner/mar-agent/managed-run-controller.js +85 -61
  46. package/dist/runner/mar-agent/runtime-model-factory.js +0 -3
  47. package/dist/runner/mar-agent-runner.js +1 -1
  48. package/dist/runner/normalization.js +42 -0
  49. package/dist/runner/opencode/conversation-parser.js +1 -1
  50. package/dist/runner/opencode/managed-run-controller.js +6 -7
  51. package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
  52. package/dist/runner/opencode/tool-normalizer.js +1 -1
  53. package/dist/runner/opencode-runner.js +4 -5
  54. package/dist/runner/runner-registry.js +4 -1
  55. package/dist/runtime-state.js +48 -372
  56. package/dist/service/computer-use-enablement-service.js +1 -1
  57. package/dist/service/continuous-run-coordinator.js +52 -7
  58. package/dist/service/conversation-history-service.js +4 -4
  59. package/dist/service/conversation-hosting-capability-service.js +1 -1
  60. package/dist/service/conversation-hosting-enablement-service.js +1 -1
  61. package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
  62. package/dist/service/conversation-hosting-maintenance-service.js +1 -1
  63. package/dist/service/conversation-observation.js +1 -1
  64. package/dist/service/conversation-segment-service.js +1 -1
  65. package/dist/service/external-session-resume-service.js +47 -27
  66. package/dist/service/fake-managed-run-service.js +3 -7
  67. package/dist/service/git-operation-service.js +1 -1
  68. package/dist/service/goal-continuation-coordinator.js +14 -7
  69. package/dist/service/mar-agent-model-transport-test-service.js +1 -1
  70. package/dist/service/native-session-watch-service.js +1 -1
  71. package/dist/service/node-connection-lifecycle-service.js +6 -3
  72. package/dist/service/node-control-channel.js +48 -11
  73. package/dist/service/node-request-service.js +1 -1
  74. package/dist/service/node-terminal-channel.js +2 -1
  75. package/dist/service/node-workspace-coordinator.js +3 -2
  76. package/dist/service/notification-mcp-bridge.js +6 -2
  77. package/dist/service/recent-run-projection-service.js +1 -1
  78. package/dist/service/restart-session-restorer.js +1 -1
  79. package/dist/service/run-event-service.js +49 -42
  80. package/dist/service/run-workbench-service.js +3 -3
  81. package/dist/service/runner-interaction-service.js +6 -5
  82. package/dist/service/runner-service.js +1 -1
  83. package/dist/service/session-recent-activity.js +2 -1
  84. package/dist/service/skill-directory-service.js +1 -1
  85. package/dist/service/skill-install-service.js +2 -2
  86. package/dist/service/terminal-service.js +1 -1
  87. package/dist/service/workbench-event-projection.js +3 -2
  88. package/dist/service/workbench-event-service.js +1 -1
  89. package/dist/service/workspace-service.js +3 -2
  90. package/dist/session/attachments.js +110 -0
  91. package/dist/session/contracts.js +1 -0
  92. package/dist/{util → session}/conversation-identity.js +1 -1
  93. package/dist/session/input-validation.js +15 -0
  94. package/dist/{service → session}/native-session-projection-service.js +40 -11
  95. package/dist/session/pagination.js +77 -0
  96. package/dist/{util → session}/runner-native-session-parsers.js +2 -1
  97. package/dist/session/runtime/approvals.js +90 -0
  98. package/dist/session/runtime/conversation-projection.js +310 -0
  99. package/dist/session/runtime/type-guards.js +19 -0
  100. package/dist/{service → session}/session-catalog-service.js +5 -4
  101. package/dist/{service → session}/session-command-service.js +3 -3
  102. package/dist/{service → session}/session-identity-service.js +1 -1
  103. package/dist/{service → session}/session-lifecycle-service.js +18 -7
  104. package/dist/{service → session}/session-message-service.js +6 -5
  105. package/dist/{service → session}/session-presentation-service.js +1 -1
  106. package/dist/{service → session}/session-query-service.js +2 -2
  107. package/dist/{service → session}/session-schedule-service.js +2 -2
  108. package/dist/util/records.js +3 -0
  109. package/dist/workspace/contracts.js +1 -0
  110. package/dist/workspace/file-index.js +534 -0
  111. package/dist/workspace/files.js +466 -0
  112. package/dist/workspace/git.js +991 -0
  113. package/dist/workspace/path-access.js +184 -0
  114. package/dist/workspace/utf8.js +12 -0
  115. package/dist/{service → workspace}/workspace-change-service.js +45 -21
  116. package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
  117. package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
  118. package/dist/{service → workspace}/workspace-file-service.js +16 -16
  119. package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
  120. package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
  121. package/dist/{service → workspace}/workspace-session-service.js +45 -40
  122. package/dist/{service → workspace}/workspace-upload-service.js +28 -28
  123. package/dist/{service → workspace}/workspace-watch-service.js +29 -28
  124. package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
  125. package/package.json +4 -4
  126. package/dist/service/hosted-conversation-node-service.js +0 -2252
  127. package/dist/service/run-domain-state.js +0 -4
  128. package/dist/service/session-domain-state.js +0 -9
  129. package/dist/service/workspace-domain-state.js +0 -41
  130. package/dist/util/node-operation-parsers.js +0 -320
  131. package/dist/workspace.js +0 -2168
  132. /package/dist/{connector → application}/node-connector-options.js +0 -0
  133. /package/dist/{util → connection}/node-operation-data.js +0 -0
  134. /package/dist/{connector → connection}/node-operation-router.js +0 -0
  135. /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
  136. /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
  137. /package/dist/{util → mcp}/secret-environment.js +0 -0
  138. /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
  139. /package/dist/{util → runner}/personal-instructions.js +0 -0
  140. /package/dist/{util → runner}/runner-error.js +0 -0
  141. /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
  142. /package/dist/{service → session}/session-context-service.js +0 -0
  143. /package/dist/{util → workspace}/git-credential-client.js +0 -0
  144. /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
  145. /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
package/dist/connector.js CHANGED
@@ -1,5 +1,12 @@
1
+ import { composeWorkspaceApplication } from './application/workspace-application.js';
2
+ import { composeSessionProjection } from './application/session-projection-composition.js';
3
+ import { composeWorkspaceResources } from './application/workspace-composition.js';
4
+ import { composeRunners } from './application/runner-composition.js';
5
+ import { ExecutionLifecycle } from './application/execution-lifecycle.js';
6
+ import { NodeOperationAdmission } from './application/node-operation-admission.js';
1
7
  import { normalizeMarAgentUsage } from './runner/mar-agent/conversation-normalizer.js';
2
- import { isImageAttachment } from './util/node-operation-parsers.js';
8
+ import { NodeLifecycle } from './application/node-lifecycle.js';
9
+ import { isImageAttachment } from './session/attachments.js';
3
10
  import { ModelUsageOutbox } from './service/model-usage-outbox.js';
4
11
  import { NodeModelUsageService } from './service/model-usage-service.js';
5
12
  import { dirname, resolve } from 'node:path';
@@ -7,7 +14,7 @@ import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
7
14
  import { loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
8
15
  import { NodeMetrics, nodeLog } from './operational.js';
9
16
  import { assertLocalSqlitePath } from './storage.js';
10
- import { resolveWorkspaceExecutionDirectory } from './workspace.js';
17
+ import { resolveWorkspaceExecutionDirectory } from './workspace/path-access.js';
11
18
  import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
12
19
  import { NodeDatabase } from './database.js';
13
20
  import { TerminalManager } from './terminal.js';
@@ -17,59 +24,43 @@ import { GoalRuntime } from './service/goal-runtime.js';
17
24
  import { GoalContinuationCoordinator } from './service/goal-continuation-coordinator.js';
18
25
  import { GoalMcpBridge } from './service/goal-mcp-bridge.js';
19
26
  import { RunnerUsageReader } from './runner-usage.js';
20
- import { ClaudeCodeRunner } from './runner/claude-code-runner.js';
21
27
  import { ClaudeManagedRunController } from './runner/claude/managed-run-controller.js';
22
- import { CodexRunner } from './runner/codex-runner.js';
23
28
  import { CodexManagedRunController } from './runner/codex/managed-run-controller.js';
24
- import { FakeTestRunner } from './runner/fake-test-runner.js';
25
- import { RunnerRegistry } from './runner/runner-registry.js';
26
- import { OpenCodeRunner } from './runner/opencode-runner.js';
27
- import { MarAgentRunner } from './runner/mar-agent-runner.js';
28
29
  import { MarAgentManagedRunController } from './runner/mar-agent/managed-run-controller.js';
29
30
  import { PyAutoGuiComputerUseProvider } from './runner/mar-agent/computer-use.js';
30
31
  import { ManagedComputerUseRuntime } from './computer-use-runtime.js';
31
32
  import { OpenCodeManagedRunController } from './runner/opencode/managed-run-controller.js';
32
- import { NodeOperationRouter } from './connector/node-operation-router.js';
33
+ import { NodeOperationRouter } from './connection/node-operation-router.js';
33
34
  import { SkillNodeOperationService } from './service/skill-node-operation-service.js';
34
35
  import { McpNodeOperationService } from './service/mcp-node-operation-service.js';
35
36
  import { RunnerService } from './service/runner-service.js';
36
37
  import { TerminalService } from './service/terminal-service.js';
37
38
  import { GitExecutionService } from './service/git-execution-service.js';
38
39
  import { GitOperationService } from './service/git-operation-service.js';
39
- import { WorkspaceService } from './service/workspace-service.js';
40
- import { WorkspaceWatchService } from './service/workspace-watch-service.js';
41
- import { WorkspaceFileService } from './service/workspace-file-service.js';
42
- import { WorkspaceUploadService } from './service/workspace-upload-service.js';
43
- import { DirectTransferService } from './service/direct-transfer-service.js';
44
- import { WorkspaceChangeService } from './service/workspace-change-service.js';
45
- import { WorkspaceSessionService } from './service/workspace-session-service.js';
40
+ import { DirectTransferService } from './connection/direct-transfer-service.js';
41
+ import { WorkspaceChangeService } from './workspace/workspace-change-service.js';
46
42
  import { WorkbenchEventService } from './service/workbench-event-service.js';
47
- import { RunDomainState } from './service/run-domain-state.js';
48
43
  import { RecentRunProjectionService } from './service/recent-run-projection-service.js';
49
44
  import { sessionGroupChangedSchema } from '@myagentroam/protocol';
50
- import { SessionDomainState } from './service/session-domain-state.js';
51
45
  import { RunEventService } from './service/run-event-service.js';
52
46
  import { RunAttachmentService } from './service/run-attachment-service.js';
53
47
  import { NodeTerminalChannel } from './service/node-terminal-channel.js';
54
- import { WorkspaceWorkbenchService } from './service/workspace-workbench-service.js';
55
48
  import { NativeSessionWatchService } from './service/native-session-watch-service.js';
56
49
  import { SessionWatchSlotCoordinator } from './service/session-watch-slot-coordinator.js';
57
- import { SessionContextService } from './service/session-context-service.js';
58
- import { WorkspaceDomainState } from './service/workspace-domain-state.js';
50
+ import { SessionContextService } from './session/session-context-service.js';
59
51
  import { RunWorkbenchService } from './service/run-workbench-service.js';
60
- import { SessionQueryService } from './service/session-query-service.js';
52
+ import { SessionQueryService } from './session/session-query-service.js';
61
53
  import { marctlSessionProgress } from './service/session-recent-activity.js';
62
- import { WorkspaceQueueWorkbenchService } from './service/workspace-queue-workbench-service.js';
54
+ import { WorkspaceQueueWorkbenchService } from './workspace/workspace-queue-workbench-service.js';
63
55
  import { ContinuousRunCoordinator } from './service/continuous-run-coordinator.js';
64
- import { SessionIdentityService } from './service/session-identity-service.js';
65
- import { NativeSessionProjectionService } from './service/native-session-projection-service.js';
56
+ import { SessionIdentityService } from './session/session-identity-service.js';
66
57
  import { ConversationHistoryService, hasManagedActiveRun } from './service/conversation-history-service.js';
67
58
  import { ConversationSegmentService } from './service/conversation-segment-service.js';
68
59
  import { RunnerInteractionService } from './service/runner-interaction-service.js';
69
- import { SessionLifecycleService } from './service/session-lifecycle-service.js';
60
+ import { SessionLifecycleService } from './session/session-lifecycle-service.js';
70
61
  import { ExternalSessionResumeService } from './service/external-session-resume-service.js';
71
- import { SessionMessageService } from './service/session-message-service.js';
72
- import { SessionScheduleService } from './service/session-schedule-service.js';
62
+ import { SessionMessageService } from './session/session-message-service.js';
63
+ import { SessionScheduleService } from './session/session-schedule-service.js';
73
64
  import { ScheduledSessionCoordinator } from './service/scheduled-session-coordinator.js';
74
65
  import { ScheduledSessionStore } from './service/scheduled-session-store.js';
75
66
  import { RunAuthorizationControl } from './service/run-authorization-control.js';
@@ -78,20 +69,20 @@ import { RestartSessionRestorer } from './service/restart-session-restorer.js';
78
69
  import { ScheduleMcpBridge } from './service/schedule-mcp-bridge.js';
79
70
  import { MailNotificationControl } from './service/mail-notification-control.js';
80
71
  import { NotificationMcpBridge, resolveNotificationCapability } from './service/notification-mcp-bridge.js';
81
- import { SessionCatalogService } from './service/session-catalog-service.js';
82
- import { SessionCommandService } from './service/session-command-service.js';
83
- import { SessionPresentationService, sessionGroup } from './service/session-presentation-service.js';
84
- import { isPlainRecord, isRunnerImageAttachment, parseRunnerImageAttachments } from './util/node-operation-parsers.js';
72
+ import { SessionCatalogService } from './session/session-catalog-service.js';
73
+ import { SessionCommandService } from './session/session-command-service.js';
74
+ import { sessionGroup } from './session/session-presentation-service.js';
75
+ import { isPlainRecord } from './util/records.js';
76
+ import { isRunnerImageAttachment, parseRunnerImageAttachments } from './session/attachments.js';
85
77
  import { WebToolService } from './service/web-tool-service.js';
86
78
  import { ModelResourcePublicationService } from './service/model-resource-publication-service.js';
87
- export { nodeConnectEndpoint, validatedCapabilities } from './util/node-operation-parsers.js';
79
+ export { nodeConnectEndpoint, validatedCapabilities } from './connection/endpoints.js';
88
80
  import { NodeControlChannel } from './service/node-control-channel.js';
89
81
  import { NodeControlMessageService } from './service/node-control-message-service.js';
90
82
  import { NodeRequestService } from './service/node-request-service.js';
91
83
  import { WorkbenchManifestService } from './service/workbench-manifest-service.js';
92
84
  import { NodeRunEventBridge } from './service/node-run-event-bridge.js';
93
85
  import { workbenchPayloadSessionId } from './service/workbench-event-projection.js';
94
- import { NodeWorkspaceCoordinator } from './service/node-workspace-coordinator.js';
95
86
  import { FakeManagedRunService } from './service/fake-managed-run-service.js';
96
87
  import { RunnerChannelMessageService } from './service/runner-channel-message-service.js';
97
88
  import { ManagedRunnerSessionService } from './service/managed-runner-session-service.js';
@@ -175,7 +166,8 @@ export class NodeConnector {
175
166
  scheduledRunAuthorization;
176
167
  restartAuthorization;
177
168
  restartHandoff;
178
- inFlightOperations = new Set();
169
+ operationAdmission;
170
+ executionLifecycle;
179
171
  pendingSessionGroups = new Map();
180
172
  mailNotificationControl = new MailNotificationControl((envelope) => this.sendEnvelope(envelope));
181
173
  scheduledSessions;
@@ -194,12 +186,25 @@ export class NodeConnector {
194
186
  throw new Error('NODE_CONFIG_UNAVAILABLE');
195
187
  return nodeDataDirectory(this.config, this.configPath);
196
188
  });
197
- /** @internal Existing contract tests inject their socket through this boundary. */
198
- set socket(value) {
199
- this.controlChannel.replaceSocketForTesting(value);
189
+ lifecycle = new NodeLifecycle(() => this.startResources(), () => {
190
+ this.continuousRunCoordinator.suspend();
191
+ this.goalCoordinator.freeze();
192
+ this.scheduledSessions.stop();
193
+ }, {
194
+ waitForRecovery: () => this.restartHandoff.wait(),
195
+ drainInput: async () => {
196
+ this.sessionMessageService.setAccepting(false);
197
+ await this.operationAdmission.drain();
198
+ await this.sessionMessageService.drain();
199
+ },
200
+ closeResume: () => this.externalSessionResumeService.close(),
201
+ capture: () => (this.config?.nodeId ? this.restartHandoff.capture() : undefined),
202
+ dispose: () => this.disposeRuntime(),
203
+ publish: (snapshot) => this.restartHandoff.publish(snapshot)
204
+ });
205
+ get stopped() {
206
+ return this.lifecycle.stopped;
200
207
  }
201
- stopped = false;
202
- stopping;
203
208
  config;
204
209
  capabilitiesProvided;
205
210
  capabilities;
@@ -224,10 +229,8 @@ export class NodeConnector {
224
229
  });
225
230
  fakeRunner;
226
231
  fakeScript;
227
- runState = new RunDomainState();
228
232
  runEventBridge;
229
233
  workspaceCoordinator;
230
- sessionState = new SessionDomainState();
231
234
  sessionIdentityService;
232
235
  nativeSessionProjectionService;
233
236
  conversationHistoryService;
@@ -311,30 +314,33 @@ export class NodeConnector {
311
314
  runnerUsageReader;
312
315
  terminalManager;
313
316
  upgradeCoordinator;
314
- workspaceState = new WorkspaceDomainState();
315
- workspaceWatchService = new WorkspaceWatchService(this.workspaceState, (workspaceId) => this.workspaceSessionService.ensureWatch(workspaceId), (workspaceId) => this.workspaceSessionService.stopWatch(workspaceId), (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId), (watch) => this.workspaceWorkbenchService?.cancelFilePrewarm(watch));
316
- workspaceSessionService = new WorkspaceSessionService(this.workspaceState, {
317
+ workspaceResources = composeWorkspaceResources({
317
318
  discover: (workspaceId, onRunnerDiscovered) => this.discoverWorkspaceSessions(workspaceId, onRunnerDiscovered),
318
319
  listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 30 }),
319
- active: (workspaceId) => this.workspaceWatchService.topicActive(workspaceId, 'sessions'),
320
- invalidate: (workspaceId) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions')
320
+ invalidateSessions: (workspaceId) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions'),
321
+ invalidateFiles: (workspaceId) => {
322
+ this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
323
+ this.workspaceCoordinator.invalidate(workspaceId);
324
+ },
325
+ releaseWatch: (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId),
326
+ removeWatch: (watch) => this.workspaceWorkbenchService?.cancelFilePrewarm(watch),
327
+ temporaryRoot: () => this.attachmentTemporaryRoot(),
328
+ attachmentResource: (id, path) => this.runAttachmentService.fileResource(id, path)
321
329
  });
322
- invalidateWorkspaceFiles = (workspaceId) => {
323
- this.workspaceFileService.dispose(workspaceId);
324
- this.workspaceWorkbenchService?.invalidateWorkspaceFiles(workspaceId);
325
- this.workspaceCoordinator.invalidate(workspaceId);
326
- };
327
- workspaceFileService = new WorkspaceFileService(this.workspaceState, this.invalidateWorkspaceFiles, {}, (workspaceId, resourcePath) => this.workspaceUploadService.fileResource(workspaceId, resourcePath) ??
328
- this.runAttachmentService.fileResource(workspaceId, resourcePath));
329
- workspaceUploadService = new WorkspaceUploadService(this.workspaceState, this.invalidateWorkspaceFiles, () => this.attachmentTemporaryRoot());
330
+ workspaceWatchService = this.workspaceResources.watch;
331
+ workspaceSessionService = this.workspaceResources.sessions;
332
+ workspaceFileService = this.workspaceResources.files;
333
+ workspaceUploadService = this.workspaceResources.uploads;
334
+ invalidateWorkspaceFiles = this.workspaceResources.invalidateFiles;
330
335
  directTransferService;
331
336
  attachmentTemporaryRoot() {
332
337
  if (this.config === undefined)
333
338
  throw new Error('NODE_CONFIG_UNAVAILABLE');
334
339
  return resolve(nodeDataDirectory(this.config, this.configPath), 'temp-files');
335
340
  }
336
- workspaceChangeService = new WorkspaceChangeService(this.workspaceState);
341
+ workspaceChangeService = new WorkspaceChangeService();
337
342
  workspaceWorkbenchService;
343
+ publishWorkspaceSession;
338
344
  database;
339
345
  recentRunProjectionService = new RecentRunProjectionService((message) => this.send('recent-runs', message));
340
346
  /** Runtime/session state is intentionally not persisted to Node SQLite. */
@@ -355,53 +361,23 @@ export class NodeConnector {
355
361
  runEventService = new RunEventService({
356
362
  runtime: this.runtime,
357
363
  metrics: this.metrics,
358
- state: this.runState,
359
364
  isBootstrapSession: (sessionId) => this.sessionIdentityService.bootstrapIds.has(sessionId),
360
365
  presentSession: (session) => this.sessionPresentationService.present(session),
361
366
  rejectBootstrap: (sessionId, code) => this.sessionIdentityService.reject(sessionId, code),
362
- emitWorkbench: (category, payload) => {
363
- if (category === 'conversation' && isPlainRecord(payload) && 'turn' in payload)
364
- this.emitConversationTurn(payload.turn);
365
- else
366
- this.publishWorkbenchEvent(category, payload);
367
- },
367
+ emitWorkbench: (category, payload) => this.publishWorkbenchEvent(category, payload),
368
368
  cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
369
369
  refreshAttachments: (sessionId) => this.runAttachmentService.refreshSession(sessionId),
370
370
  expireImages: (runId) => this.runAttachmentService.expire(runId),
371
371
  scheduleContextRefresh: (sessionId) => this.sessionContextService.schedule(sessionId),
372
372
  refreshWorkspaceSessions: (workspaceId) => this.workspaceWorkbenchService.ensureTopicSnapshot(workspaceId, 'sessions'),
373
- onTerminalRun: (run, status) => {
374
- if (this.database !== undefined)
375
- this.scheduledSessions.executionTerminated(run, status);
376
- this.continuousRunCoordinator.runTerminated(run.sessionId);
377
- },
378
- dispatchQueue: async (sessionId, delayMs = 0) => {
379
- if (this.upgradeCoordinator.pending())
380
- return;
381
- if (delayMs > 0) {
382
- this.continuousRunCoordinator.schedule(sessionId, delayMs);
383
- return;
384
- }
385
- await this.continuousRunCoordinator.dispatch(sessionId);
386
- },
373
+ onTerminalRun: (run, status) => this.executionLifecycle.terminal(run, status),
374
+ dispatchQueue: (sessionId, delayMs) => this.executionLifecycle.dispatchQueue(sessionId, delayMs),
387
375
  executionStarted: (executionId) => this.continuousRunCoordinator.executionStarted(executionId),
388
- keepRunActive: (sessionId) => this.goalCoordinator.keepRunActive(sessionId),
389
- executionTerminating: (executionId, status, permissionStop) => {
390
- this.gitOperationService.executionFinished(executionId);
391
- this.goalCoordinator.executionTerminating(executionId, status, permissionStop);
392
- },
393
- releaseExecution: (executionId) => {
394
- this.modelUsage.release(executionId);
395
- this.goalCoordinator.releaseExecution(executionId);
396
- this.revokeExecutionMcps(executionId);
397
- this.continuousRunCoordinator.remove(executionId);
398
- }
376
+ acceptExecutionTerminal: (input) => this.continuousRunCoordinator.acceptExecutionTerminal(input),
377
+ publishGoalUpdate: ({ session, goal }) => this.goalCoordinator.publishGoalState(session, goal),
378
+ publishQueueChange: (sessionId) => this.continuousRunCoordinator.publish(sessionId),
379
+ releaseExecution: (executionId) => this.executionLifecycle.release(executionId)
399
380
  });
400
- revokeExecutionMcps(executionId) {
401
- this.goalMcpBridge.revoke(executionId);
402
- this.scheduleMcpBridge.revoke(executionId);
403
- this.notificationMcpBridge.revoke(executionId);
404
- }
405
381
  sessionWatchSlots = new SessionWatchSlotCoordinator();
406
382
  nativeSessionWatchService = new NativeSessionWatchService({
407
383
  resolve: async (sessionId) => this.runtime.getAgentSession(sessionId) ??
@@ -483,7 +459,7 @@ export class NodeConnector {
483
459
  if (this.config === undefined)
484
460
  throw new Error('NODE_CONFIG_UNAVAILABLE');
485
461
  return nodeUpgradeRequestPath(nodeDatabasePath(this.config, this.configPath));
486
- }, () => this.runState.active.size, (status) => {
462
+ }, () => this.runtime.activeExecutionCount(), (status) => {
487
463
  this.capabilities = {
488
464
  ...this.capabilities,
489
465
  metadata: upgradeMetadata(this.capabilities.metadata, status)
@@ -506,8 +482,8 @@ export class NodeConnector {
506
482
  }));
507
483
  };
508
484
  this.reconnect = options.reconnect ?? true;
509
- this.fakeRunner =
510
- options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
485
+ const composedRunners = composeRunners(options);
486
+ this.fakeRunner = composedRunners.fake;
511
487
  this.fakeScript = options.fakeScript;
512
488
  this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session), (sessionId) => this.runtime.activeSessionRun(sessionId) !== undefined ||
513
489
  this.runtime.sessionQueue(sessionId).itemIds.length > 0, () => this.attachmentTemporaryRoot());
@@ -525,18 +501,17 @@ export class NodeConnector {
525
501
  return url.protocol === 'http:' || url.protocol === 'https:' ? url.origin : undefined;
526
502
  }
527
503
  });
528
- this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory, options.codexRunClientFactory);
504
+ this.codexClient = composedRunners.codex;
529
505
  this.codexRunController = new CodexManagedRunController(this.codexClient, {
530
506
  available: () => this.capabilities.codex.available,
531
507
  intercepted: () => this.fakeRunner !== undefined,
532
- active: this.runState.active,
533
- addActive: (runId) => this.runState.active.add(runId),
534
- removeActive: (runId) => this.runState.active.delete(runId),
508
+ isExecutionActive: (runId) => this.runtime.isExecutionActive(runId),
509
+ executionAccepted: (runId) => this.runtime.executionAccepted(runId),
535
510
  adopt: (input) => this.runtime.adoptRun(input),
536
511
  session: (sessionId) => this.runtime.getAgentSession(sessionId),
537
512
  runStatus: (runId) => this.runtime.getRun(runId)?.status,
538
513
  run: (runId) => this.runtime.getRun(runId),
539
- interrupted: (runId) => this.runState.interruptRequested.has(runId),
514
+ interrupted: (runId) => this.runtime.isExecutionInterruptRequested(runId),
540
515
  parseAttachments: parseRunnerImageAttachments,
541
516
  materialize: (runId, attachments) => this.runAttachmentService.materializeCodex(runId, attachments),
542
517
  completeCancelled: (runId) => this.runnerInteractionService.completeCancelledRun(runId),
@@ -562,9 +537,9 @@ export class NodeConnector {
562
537
  })
563
538
  });
564
539
  this.codexClient.onNotification((notification) => this.codexRunController.handleNotification(notification));
565
- this.claudeClient = new ClaudeCodeRunner(options.claudeClient);
566
- this.openCodeClient = new OpenCodeRunner(options.openCodeClient);
567
- this.marAgentClient = new MarAgentRunner();
540
+ this.claudeClient = composedRunners.claude;
541
+ this.openCodeClient = composedRunners.openCode;
542
+ this.marAgentClient = composedRunners.marAgent;
568
543
  this.marAgentClient.setGeneratedImageRegistrar((session, images) => this.runAttachmentService.registerNative(session, images));
569
544
  this.computerUseProvider = new PyAutoGuiComputerUseProvider(() => {
570
545
  if (this.config?.computerUse?.enabled !== true)
@@ -588,7 +563,8 @@ export class NodeConnector {
588
563
  this.marAgentRunController = new MarAgentManagedRunController(this.marAgentClient, {
589
564
  createApproval: (input) => this.runtime.createApproval(input),
590
565
  publishApproval: (input) => this.runnerInteractionService.publishApproval(input),
591
- active: this.runState.active,
566
+ isExecutionActive: (runId) => this.runtime.isExecutionActive(runId),
567
+ executionAccepted: (runId) => this.runtime.executionAccepted(runId),
592
568
  adopt: (input) => this.runtime.adoptRun(input),
593
569
  promote: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
594
570
  customTitle: (sessionId) => this.runtime.getAgentSession(sessionId)?.customTitle,
@@ -632,7 +608,8 @@ export class NodeConnector {
632
608
  this.marAgentClient.setSessionResourceReleaser((sessionId) => this.marAgentRunController.releaseSession(sessionId));
633
609
  this.openCodeRunController = new OpenCodeManagedRunController(this.openCodeClient, {
634
610
  intercepted: () => this.fakeRunner !== undefined,
635
- active: this.runState.active,
611
+ isExecutionActive: (runId) => this.runtime.isExecutionActive(runId),
612
+ executionAccepted: (runId) => this.runtime.executionAccepted(runId),
636
613
  parseAttachments: parseRunnerImageAttachments,
637
614
  persistAttachments: async (_cwd, runId, attachments) => {
638
615
  const run = this.runtime.getRun(runId);
@@ -668,7 +645,8 @@ export class NodeConnector {
668
645
  }, this.claudeApprovalTimeoutMs);
669
646
  this.claudeRunController = new ClaudeManagedRunController(this.claudeClient, {
670
647
  intercepted: () => this.fakeRunner !== undefined,
671
- active: this.runState.active,
648
+ isExecutionActive: (runId) => this.runtime.isExecutionActive(runId),
649
+ executionAccepted: (runId) => this.runtime.executionAccepted(runId),
672
650
  parseAttachments: parseRunnerImageAttachments,
673
651
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
674
652
  adopt: (input) => this.runtime.adoptRun(input),
@@ -689,19 +667,12 @@ export class NodeConnector {
689
667
  clearCommandState: (sessionId, commandId) => this.commandStates.clear(sessionId, commandId),
690
668
  commandStates: (sessionId) => this.commandStates.list(sessionId)
691
669
  });
692
- this.runners = new RunnerRegistry([
693
- this.openCodeClient,
694
- this.marAgentClient,
695
- this.codexClient,
696
- this.claudeClient,
697
- ...(this.fakeRunner === undefined ? [] : [this.fakeRunner])
698
- ]);
670
+ this.runners = composedRunners.registry;
699
671
  this.managedRunnerSessions = new ManagedRunnerSessionService(this.runtime, this.runners, (session) => this.emitSessionWorkbenchEvent({
700
672
  session: this.sessionPresentationService.present(session)
701
673
  }));
702
674
  this.runnerInteractionService = new RunnerInteractionService({
703
675
  runtime: this.runtime,
704
- state: this.runState,
705
676
  codex: () => this.codexClient,
706
677
  settleCodexUsage: (runId, turnId) => this.codexRunController.settleUsage(runId, turnId),
707
678
  claude: () => this.claudeClient,
@@ -715,26 +686,17 @@ export class NodeConnector {
715
686
  interactions: this.runnerInteractionService,
716
687
  send: (type, payload, replyTo) => this.send(type, payload, replyTo)
717
688
  });
718
- this.sessionPresentationService = new SessionPresentationService({
719
- database: () => this.database,
720
- runtime: this.runtime,
721
- commands: this.commandStates,
722
- runners: this.runners,
723
- capabilities: () => this.capabilities,
724
- hasManagedActiveRun: (session) => hasManagedActiveRun(this.runtime, session),
725
- projectionForNative: (session) => this.nativeSessionProjectionService.projectionForNative(session)
726
- });
727
- this.nativeSessionProjectionService = new NativeSessionProjectionService({
689
+ const sessionProjection = composeSessionProjection({
728
690
  database: () => this.database,
729
691
  nodeId: () => this.config?.nodeId ?? 'recorded-session',
730
692
  runtime: this.runtime,
731
693
  runners: this.runners,
732
694
  capabilities: () => this.capabilities,
733
- state: this.sessionState,
734
695
  commands: this.commandStates,
735
- releaseResumeState: (directIds, projectionIds) => this.externalSessionResumeService.forgetSessions(directIds, projectionIds),
736
- present: (session) => this.sessionPresentationService.present(session)
696
+ releaseResumeState: (directIds, projectionIds) => this.externalSessionResumeService.forgetSessions(directIds, projectionIds)
737
697
  });
698
+ this.sessionPresentationService = sessionProjection.presentation;
699
+ this.nativeSessionProjectionService = sessionProjection.native;
738
700
  this.conversationHistoryService = new ConversationHistoryService({
739
701
  runtime: this.runtime,
740
702
  runners: this.runners,
@@ -765,7 +727,7 @@ export class NodeConnector {
765
727
  },
766
728
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
767
729
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
768
- markInterrupted: (runId) => this.runState.interruptRequested.add(runId)
730
+ markInterrupted: (runId) => this.runtime.requestExecutionInterrupt(runId)
769
731
  });
770
732
  this.sessionQueryService = new SessionQueryService({
771
733
  queuedCount: (sessionId) => this.workspaceQueueWorkbenchService.queuedCount(sessionId),
@@ -843,7 +805,12 @@ export class NodeConnector {
843
805
  emitConversation: (turn) => this.emitConversationTurn(turn),
844
806
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
845
807
  stopped: () => this.stopped,
846
- markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
808
+ markInterrupted: (runId) => this.runtime.requestExecutionInterrupt(runId),
809
+ keepRunActive: (sessionId) => this.goalCoordinator.keepRunActive(sessionId),
810
+ executionTerminating: (executionId, status, permissionStop) => {
811
+ this.gitOperationService.executionFinished(executionId);
812
+ return this.goalCoordinator.executionTerminating(executionId, status, permissionStop);
813
+ },
847
814
  mcps: (workspaceId) => this.database?.effectiveMcpInstallations(workspaceId) ?? [],
848
815
  retainEnvironment: (environment) => this.gitExecution().retain(environment),
849
816
  goalCurrent: (input) => input.goal === undefined ||
@@ -860,7 +827,7 @@ export class NodeConnector {
860
827
  code: safeErrorCode(error, 'GOAL_EXECUTION_START_FAILED')
861
828
  });
862
829
  },
863
- revokeExecutionMcps: (executionId) => this.revokeExecutionMcps(executionId),
830
+ revokeExecutionMcps: (executionId) => this.executionLifecycle.revokeMcps(executionId),
864
831
  goalMcp: async (input) => {
865
832
  if (input.goal === undefined)
866
833
  throw new Error('GOAL_NOT_ACTIVE');
@@ -979,7 +946,6 @@ export class NodeConnector {
979
946
  resolveExecutionDirectory,
980
947
  runtime: this.runtime,
981
948
  runners: this.runners,
982
- state: this.sessionState,
983
949
  available: (runner) => this.runners.require(runner).available(this.capabilities),
984
950
  projectionFor: (session) => this.nativeSessionProjectionService.projectionForNative(session),
985
951
  managedAliasFor: (session) => this.nativeSessionProjectionService.managedForNative(session),
@@ -1078,7 +1044,7 @@ export class NodeConnector {
1078
1044
  runners: this.runners,
1079
1045
  runnerAvailable: (runner) => this.runners.require(runner).available(this.capabilities),
1080
1046
  projection: this.nativeSessionProjectionService,
1081
- cachedNative: () => [...this.sessionState.directNative.values()],
1047
+ cachedNative: () => this.nativeSessionProjectionService.cachedNative(),
1082
1048
  cachedWorkspace: (workspaceId) => this.workspaceCoordinator.cachedSessions(workspaceId),
1083
1049
  discoveryPending: (workspaceId) => this.workspaceSessionService.discoveryPending(workspaceId),
1084
1050
  discoverWorkspace: (workspaceId, cleanup) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, undefined, cleanup),
@@ -1090,7 +1056,7 @@ export class NodeConnector {
1090
1056
  void this.runAttachmentService.releaseSession(session.id);
1091
1057
  },
1092
1058
  refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
1093
- externalActivity: (sessionId) => this.sessionState.externalActivity.get(sessionId),
1059
+ externalActivity: (sessionId) => this.externalSessionResumeService.activity(sessionId),
1094
1060
  isWatching: (sessionId) => this.nativeSessionWatchService.isWatching(sessionId),
1095
1061
  isRecovering: (sessionId) => this.restartHandoff.blocks(sessionId),
1096
1062
  history: (session, input) => this.conversationHistoryService.read(session, input),
@@ -1154,7 +1120,6 @@ export class NodeConnector {
1154
1120
  script: this.fakeScript,
1155
1121
  runners: this.runners,
1156
1122
  runtime: this.runtime,
1157
- state: this.runState,
1158
1123
  emitRun: (runId, type, payload, status) => this.runEventBridge.emitRun(runId, type, payload, status),
1159
1124
  publishApproval: (input) => this.runnerInteractionService.publishApproval(input),
1160
1125
  disconnect: () => this.controlChannel.close(4010, 'FAKE_RUNNER_DISCONNECT')
@@ -1249,9 +1214,9 @@ export class NodeConnector {
1249
1214
  },
1250
1215
  connected: (payload) => {
1251
1216
  this.usageOutbox?.connected(payload);
1217
+ this.connectionLifecycle.connected();
1252
1218
  for (const change of this.pendingSessionGroups.values())
1253
1219
  this.send('session.group.changed', change);
1254
- this.connectionLifecycle.connected();
1255
1220
  this.mailNotificationControl.setFeatures(payload);
1256
1221
  },
1257
1222
  close: (code, reason) => this.controlChannel.close(code, reason),
@@ -1278,15 +1243,29 @@ export class NodeConnector {
1278
1243
  execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
1279
1244
  send: (type, payload, replyTo) => this.send(type, payload, replyTo)
1280
1245
  });
1281
- this.workspaceCoordinator = new NodeWorkspaceCoordinator({
1246
+ const workspaceApplication = composeWorkspaceApplication({
1282
1247
  config: () => this.config,
1283
1248
  database: () => this.database,
1284
- watches: this.workspaceWatchService,
1285
- sessions: this.workspaceSessionService,
1249
+ runtime: this.runtime,
1250
+ terminals: this.terminalManager,
1251
+ resources: this.workspaceResources,
1286
1252
  changes: this.workspaceChangeService,
1287
- emit: (workspaceId, topic) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, topic),
1288
- send: (type, payload, replyTo) => this.send(type, payload, replyTo)
1253
+ listSessions: (workspaceId, sessionGroup) => this.executeNodeOperation('session.list', {
1254
+ workspaceId,
1255
+ limit: 30,
1256
+ ...(sessionGroup === undefined ? {} : { sessionGroup })
1257
+ }),
1258
+ invalidatePrompt: (id) => this.promptCompletion?.invalidateWorkspace(id),
1259
+ nodeId: () => this.config?.nodeId ?? 'runtime-node',
1260
+ nodeGeneration: () => this.nodeGeneration,
1261
+ send: (type, payload, replyTo) => this.send(type, payload, replyTo),
1262
+ emitWatchEvent: (payload) => this.send('watch.event', payload),
1263
+ publishSession: (payload) => this.emitWorkbenchEvent('session', payload)
1289
1264
  });
1265
+ this.workspaceCoordinator = workspaceApplication.coordinator;
1266
+ this.workspaceService = workspaceApplication.workspace;
1267
+ this.workspaceWorkbenchService = workspaceApplication.workbench;
1268
+ this.publishWorkspaceSession = workspaceApplication.publishSession;
1290
1269
  this.runEventBridge = new NodeRunEventBridge({
1291
1270
  bindModelUsage: (runId) => {
1292
1271
  const run = this.runtime.getRun(runId);
@@ -1401,43 +1380,12 @@ export class NodeConnector {
1401
1380
  terminals: this.terminalManager
1402
1381
  });
1403
1382
  this.terminalService = new TerminalService(requireDatabase, this.terminalManager, (environment) => this.gitExecution().retain(environment));
1404
- this.workspaceService = new WorkspaceService(requireDatabase, () => {
1405
- if (this.config === undefined)
1406
- throw new Error('NODE_CONFIG_UNAVAILABLE');
1407
- return this.config;
1408
- }, this.runtime, (workspaceId) => {
1409
- this.terminalManager.removeWorkspaceAssociation(workspaceId);
1410
- this.workspaceFileService.dispose(workspaceId);
1411
- this.workspaceCoordinator.invalidateChanges(workspaceId);
1412
- this.workspaceSessionService.invalidate(workspaceId);
1413
- this.workspaceCoordinator.clearWatches(workspaceId);
1414
- this.workspaceWorkbenchService.clearWorkspace(workspaceId);
1415
- this.promptCompletion?.invalidateWorkspace(workspaceId);
1416
- }, (workspaceId) => this.workspaceCoordinator.gitPolicyChanged(workspaceId));
1417
- this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
1418
- requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
1419
- inspectWorkspace: (workspace) => this.workspaceCoordinator.inspectMetadata(workspace),
1420
- commitWorkspace: (workspace, inspection) => this.workspaceCoordinator.commitMetadata(workspace, inspection),
1421
- watch: this.workspaceWatchService,
1422
- files: this.workspaceFileService,
1423
- uploads: this.workspaceUploadService,
1424
- changes: this.workspaceChangeService,
1425
- listSessions: (workspaceId, sessionGroup) => this.executeNodeOperation('session.list', {
1426
- workspaceId,
1427
- limit: 30,
1428
- ...(sessionGroup === undefined ? {} : { sessionGroup })
1429
- }),
1430
- invalidate: this.invalidateWorkspaceFiles,
1431
- nodeId: () => this.config?.nodeId ?? 'runtime-node',
1432
- nodeGeneration: () => this.nodeGeneration,
1433
- emitWatchEvent: (payload) => this.send('watch.event', payload)
1434
- });
1435
1383
  const completionWorkspace = new PromptCompletionWorkspaceSource({
1436
1384
  requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
1437
1385
  observesWorkspace: (workspaceId, actor, connectionId) => this.workspaceWatchService.observes(workspaceId, actor.sessionHash, connectionId),
1438
1386
  observesSession: (workspaceId, sessionId, actor, connectionId) => this.nativeSessionWatchService.observes(actor.sessionHash, connectionId, workspaceId, sessionId),
1439
1387
  resolveSession: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId),
1440
- cachedSessions: (workspaceId) => this.workspaceState.sessionCache.get(workspaceId)?.sessions ?? [],
1388
+ cachedSessions: (workspaceId) => this.workspaceSessionService.cachedSnapshot(workspaceId),
1441
1389
  readHistory: (session, limit) => this.conversationHistoryService.read(session, { limit })
1442
1390
  });
1443
1391
  this.promptCompletion = new PromptCompletionNodeService({
@@ -1555,7 +1503,7 @@ export class NodeConnector {
1555
1503
  this.operationRouter.register('node.metrics', () => ({
1556
1504
  metrics: {
1557
1505
  ...this.metrics.snapshot(),
1558
- activeRuns: this.runState.active.size,
1506
+ activeRuns: this.runtime.activeExecutionCount(),
1559
1507
  workspaceLeases: this.runtime.activeWorkspaceLeaseCount()
1560
1508
  }
1561
1509
  }));
@@ -1565,6 +1513,8 @@ export class NodeConnector {
1565
1513
  manifest: await this.workbenchManifestService.read(),
1566
1514
  binaryTerminal: true
1567
1515
  }));
1516
+ this.executionLifecycle = new ExecutionLifecycle(() => this.database !== undefined, this.scheduledSessions, this.continuousRunCoordinator, this.goalCoordinator, this.modelUsage, this.goalMcpBridge, this.scheduleMcpBridge, this.notificationMcpBridge, this.runEventBridge, this.upgradeCoordinator);
1517
+ this.operationAdmission = new NodeOperationAdmission(() => this.stopped, () => this.database !== undefined, () => this.config?.nodeId, this.restartHandoff, this.upgradeCoordinator, this.gitOperationService, this.operationRouter);
1568
1518
  }
1569
1519
  projectComputerUseCapability(capabilities) {
1570
1520
  return {
@@ -1572,11 +1522,10 @@ export class NodeConnector {
1572
1522
  computerUse: this.computerUseEnablement.capability()
1573
1523
  };
1574
1524
  }
1575
- async start() {
1576
- if (this.stopping !== undefined)
1577
- await this.stopping;
1578
- this.stopping = undefined;
1579
- this.stopped = false;
1525
+ start() {
1526
+ return this.lifecycle.start();
1527
+ }
1528
+ async startResources() {
1580
1529
  this.continuousRunCoordinator.resumeScheduling();
1581
1530
  this.sessionMessageService.setAccepting(true);
1582
1531
  this.config ??= await loadNodeConfig(this.configPath);
@@ -1677,30 +1626,7 @@ export class NodeConnector {
1677
1626
  return this.directTransferService?.handleTerminal(command) ?? false;
1678
1627
  }
1679
1628
  stop(options = {}) {
1680
- if (this.stopping !== undefined)
1681
- return this.stopping;
1682
- this.stopped = true;
1683
- this.continuousRunCoordinator.suspend();
1684
- this.goalCoordinator.freeze();
1685
- this.scheduledSessions.stop();
1686
- this.stopping = (async () => {
1687
- let snapshot;
1688
- try {
1689
- // In-flight recovery must rebuild accepted inputs before preparation closes.
1690
- await this.restartHandoff.wait();
1691
- this.sessionMessageService.setAccepting(false);
1692
- await Promise.allSettled([...this.inFlightOperations]);
1693
- await this.sessionMessageService.drain();
1694
- if (options.handoff && this.config?.nodeId)
1695
- snapshot = this.restartHandoff.capture();
1696
- }
1697
- finally {
1698
- await this.disposeRuntime();
1699
- }
1700
- if (snapshot !== undefined)
1701
- await this.restartHandoff.publish(snapshot);
1702
- })();
1703
- return this.stopping;
1629
+ return this.lifecycle.stop(options.handoff === true);
1704
1630
  }
1705
1631
  disposeRuntime() {
1706
1632
  this.recentRunProjectionService.dispose();
@@ -1756,19 +1682,20 @@ export class NodeConnector {
1756
1682
  webToolDrain
1757
1683
  ]);
1758
1684
  return (async () => {
1759
- let failed;
1685
+ const hosting = await Promise.allSettled([conversationHostingDrain]);
1686
+ const results = await drains;
1687
+ this.database = undefined;
1688
+ const git = await Promise.allSettled([this.gitExecutionService?.close()]);
1689
+ this.usageOutbox?.disconnected();
1690
+ this.usageOutbox = undefined;
1691
+ this.modelUsage.clear();
1692
+ const failed = [...hosting, ...results, ...git].find((result) => result.status === 'rejected');
1760
1693
  try {
1761
- await conversationHostingDrain;
1762
- }
1763
- finally {
1764
- const results = await drains;
1765
- this.database = undefined;
1766
- await this.gitExecutionService?.close();
1767
- this.usageOutbox?.disconnected();
1768
- this.usageOutbox = undefined;
1769
- this.modelUsage.clear();
1770
1694
  database?.close();
1771
- failed = results.find((result) => result.status === 'rejected');
1695
+ }
1696
+ catch (error) {
1697
+ if (failed === undefined)
1698
+ throw error;
1772
1699
  }
1773
1700
  if (failed !== undefined)
1774
1701
  throw failed.reason;
@@ -1777,10 +1704,7 @@ export class NodeConnector {
1777
1704
  /** @internal Thin compatibility delegates; all behavior remains in the extracted services. */
1778
1705
  handleMessage = (raw) => this.controlMessages.handle(raw);
1779
1706
  emitRunEvent = (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status);
1780
- emitConversationItem = (runId, item) => {
1781
- this.goalCoordinator.executionProgress(runId, item);
1782
- this.runEventBridge.emitConversationItem(runId, item);
1783
- };
1707
+ emitConversationItem = (runId, item) => this.executionLifecycle.emitItem(runId, item);
1784
1708
  emitConversationTurn = (turn) => {
1785
1709
  this.emitWorkbenchEvent('conversation', { turn });
1786
1710
  };
@@ -1791,46 +1715,14 @@ export class NodeConnector {
1791
1715
  }
1792
1716
  this.emitWorkbenchEvent(category, payload);
1793
1717
  };
1794
- emitSessionWorkbenchEvent = (payload) => {
1795
- if (isPlainRecord(payload) && isPlainRecord(payload.session)) {
1796
- const workspaceId = payload.session.workspaceId;
1797
- if (typeof workspaceId === 'string') {
1798
- this.workspaceSessionService.markCacheStale(workspaceId);
1799
- void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions');
1800
- }
1801
- }
1802
- this.emitWorkbenchEvent('session', payload);
1803
- };
1718
+ emitSessionWorkbenchEvent = (payload) => this.publishWorkspaceSession(payload);
1804
1719
  emitWorkbenchEvent = (category, payload) => this.workbenchEventService.dispatch(category, payload);
1805
1720
  discoverWorkspaceSessions = (workspaceId, onRunnerDiscovered) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, onRunnerDiscovered);
1806
1721
  invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
1807
1722
  startCapabilityRefresh = () => this.connectionLifecycle.startCapabilityRefresh();
1808
1723
  refreshCapabilities = () => this.connectionLifecycle.refreshCapabilities();
1809
- async executeNodeOperation(operation, data) {
1810
- if (this.stopped)
1811
- throw new Error('NODE_STOPPED');
1812
- const input = isPlainRecord(data) ? data : {};
1813
- const sessionId = typeof input.sessionId === 'string' ? input.sessionId : undefined;
1814
- if (this.restartHandoff.blocksOperation(operation, input))
1815
- throw new Error('SESSION_RESTART_PENDING');
1816
- if (this.database === undefined)
1817
- throw new Error('NODE_DATABASE_UNAVAILABLE');
1818
- if (this.upgradeCoordinator.blocks(operation))
1819
- throw new Error('NODE_UPGRADE_PENDING');
1820
- nodeLog('node.operation', { nodeId: this.config?.nodeId, operation });
1821
- const task = this.gitOperationService.execute(operation, data, (input) => this.operationRouter.execute(operation, input));
1822
- this.inFlightOperations.add(task);
1823
- try {
1824
- const result = await task;
1825
- if ((operation === 'session.command.execute' || operation === 'session.message') &&
1826
- sessionId !== undefined &&
1827
- Number.isSafeInteger(input.__requestUserId))
1828
- this.restartHandoff.rememberUser(sessionId, input.__requestUserId);
1829
- return result;
1830
- }
1831
- finally {
1832
- this.inFlightOperations.delete(task);
1833
- }
1724
+ executeNodeOperation(operation, data) {
1725
+ return this.operationAdmission.execute(operation, data);
1834
1726
  }
1835
1727
  gitExecution() {
1836
1728
  this.gitExecutionService ??= new GitExecutionService({