@bbigbang/core 0.1.0

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 (175) hide show
  1. package/dist/config.js +380 -0
  2. package/dist/execution/executionDispatcher.js +3810 -0
  3. package/dist/main.js +90 -0
  4. package/dist/nodeEventHistory.js +206 -0
  5. package/dist/scheduler/dreamLogic.js +50 -0
  6. package/dist/scheduler/dreamScheduler.js +65 -0
  7. package/dist/services/agentFileAccessService.js +1913 -0
  8. package/dist/services/agentRuntimeCleanupBroker.js +62 -0
  9. package/dist/services/agentSkillsBroker.js +118 -0
  10. package/dist/services/agentSkillsService.js +83 -0
  11. package/dist/services/agentWorkspaceBroker.js +937 -0
  12. package/dist/services/agentWorkspaceService.js +70 -0
  13. package/dist/services/appVersion.js +14 -0
  14. package/dist/services/auth.js +586 -0
  15. package/dist/services/claudeControlBroker.js +154 -0
  16. package/dist/services/claudeTranscriptBroker.js +100 -0
  17. package/dist/services/claudeTranscriptService.js +359 -0
  18. package/dist/services/codexAppServerBroker.js +155 -0
  19. package/dist/services/codexTranscriptBroker.js +98 -0
  20. package/dist/services/codexTranscriptService.js +961 -0
  21. package/dist/services/droidMissionBroker.js +124 -0
  22. package/dist/services/droidMissionImporter.js +630 -0
  23. package/dist/services/droidModelOptions.js +165 -0
  24. package/dist/services/hubServerRegistrationService.js +268 -0
  25. package/dist/services/libraryManifest.js +43 -0
  26. package/dist/services/libraryScaffold.js +26 -0
  27. package/dist/services/libraryService.js +2263 -0
  28. package/dist/services/memoryService.js +386 -0
  29. package/dist/services/missionEvidence.js +377 -0
  30. package/dist/services/missionService.js +2361 -0
  31. package/dist/services/missionTrace.js +158 -0
  32. package/dist/services/nativeMissionBriefParser.js +120 -0
  33. package/dist/services/nativeMissionOrchestrator.js +2045 -0
  34. package/dist/services/nativeMissionReportGenerator.js +227 -0
  35. package/dist/services/nativeMissionValidationRunner.js +452 -0
  36. package/dist/services/nativeMissionWorkerBroker.js +190 -0
  37. package/dist/services/nodeRegistry.js +34 -0
  38. package/dist/services/nodeStateReconciler.js +97 -0
  39. package/dist/services/panelMediaScanner.js +119 -0
  40. package/dist/services/persistentRuntimeJsonlClient.js +153 -0
  41. package/dist/services/platformAgentPolicy.js +180 -0
  42. package/dist/services/platformAgentService.js +2041 -0
  43. package/dist/services/projectAccessResolver.js +93 -0
  44. package/dist/services/projectService.js +392 -0
  45. package/dist/services/resourceSpaceService.js +140 -0
  46. package/dist/services/scenarioRuntimeService.js +1130 -0
  47. package/dist/services/suggestedPlannerService.js +868 -0
  48. package/dist/services/workbenchGitBroker.js +161 -0
  49. package/dist/services/workbenchGitService.js +69 -0
  50. package/dist/services/workbenchInspectBroker.js +65 -0
  51. package/dist/services/workbenchNodePathService.js +79 -0
  52. package/dist/services/workbenchRegistryService.js +240 -0
  53. package/dist/services/workbenchRootService.js +181 -0
  54. package/dist/services/workbenchTerminalBroker.js +378 -0
  55. package/dist/services/workspaceRunOwnership.js +60 -0
  56. package/dist/services/workspaceScaffold.js +105 -0
  57. package/dist/services/workspaceSessionRuntimeService.js +576 -0
  58. package/dist/services/workspaceSessionService.js +245 -0
  59. package/dist/services/workspaceToolActionRunner.js +1582 -0
  60. package/dist/services/workspaceToolErrors.js +10 -0
  61. package/dist/services/workspaceToolExecutionUtils.js +895 -0
  62. package/dist/services/workspaceToolLatestStateProjector.js +91 -0
  63. package/dist/services/workspaceToolManifest.js +572 -0
  64. package/dist/services/workspaceToolMutationQueue.js +43 -0
  65. package/dist/services/workspaceToolPanelProjection.js +460 -0
  66. package/dist/services/workspaceToolPromotion.js +255 -0
  67. package/dist/services/workspaceToolPromotionState.js +224 -0
  68. package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
  69. package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
  70. package/dist/services/workspaceToolReadModel.js +378 -0
  71. package/dist/services/workspaceToolRunLedger.js +239 -0
  72. package/dist/services/workspaceToolService.js +3067 -0
  73. package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
  74. package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
  75. package/dist/services/workspaceToolTypes.js +1 -0
  76. package/dist/services/workspaceToolUploadMaterializer.js +228 -0
  77. package/dist/web/actionCardRoutes.js +129 -0
  78. package/dist/web/actionCards.js +469 -0
  79. package/dist/web/activationContext.js +684 -0
  80. package/dist/web/agentChannelGuards.js +48 -0
  81. package/dist/web/agentMentionCooldowns.js +32 -0
  82. package/dist/web/agentReminders.js +1668 -0
  83. package/dist/web/agentRuntimePresence.js +197 -0
  84. package/dist/web/agentSelfState.js +494 -0
  85. package/dist/web/agentTaskLinks.js +26 -0
  86. package/dist/web/agentVisibility.js +79 -0
  87. package/dist/web/assets.js +95 -0
  88. package/dist/web/channelActivationPrompt.js +395 -0
  89. package/dist/web/channelMemoryNotes.js +127 -0
  90. package/dist/web/channelMentions.js +10 -0
  91. package/dist/web/channelMessageSequences.js +19 -0
  92. package/dist/web/channelSubscriptions.js +26 -0
  93. package/dist/web/clearedTaskRoots.js +10 -0
  94. package/dist/web/collaborationPromptGuidance.js +36 -0
  95. package/dist/web/collaborationSurfaceState.js +140 -0
  96. package/dist/web/contextBundleRanking.js +154 -0
  97. package/dist/web/contextBundleResolver.js +488 -0
  98. package/dist/web/conversationBuiltinSkillRoots.js +50 -0
  99. package/dist/web/conversationControls.js +232 -0
  100. package/dist/web/conversationHandoffs.js +612 -0
  101. package/dist/web/conversationManager.js +2511 -0
  102. package/dist/web/conversationSummaries.js +876 -0
  103. package/dist/web/conversationSurfaceKinds.js +17 -0
  104. package/dist/web/conversationTargets.js +173 -0
  105. package/dist/web/directActivationPrompt.js +122 -0
  106. package/dist/web/directReplyTargets.js +69 -0
  107. package/dist/web/directThreadResolver.js +129 -0
  108. package/dist/web/dmTaskHandoffPrompt.js +120 -0
  109. package/dist/web/dmTaskThreadStatusProjection.js +229 -0
  110. package/dist/web/ftsQuery.js +33 -0
  111. package/dist/web/internalAgentRouter.js +11341 -0
  112. package/dist/web/libraryCuratorScheduler.js +58 -0
  113. package/dist/web/libraryDocumentPromptGuidance.js +8 -0
  114. package/dist/web/messageCheckpoints.js +19 -0
  115. package/dist/web/nodeWsHandler.js +2495 -0
  116. package/dist/web/notificationRounds.js +1061 -0
  117. package/dist/web/panelActionMessages.js +108 -0
  118. package/dist/web/panelActivationPrompt.js +18 -0
  119. package/dist/web/panelAudit.js +273 -0
  120. package/dist/web/panelLifecycle.js +222 -0
  121. package/dist/web/panelMediaPolicy.js +43 -0
  122. package/dist/web/panelPathPolicy.js +63 -0
  123. package/dist/web/panelPreviews.js +175 -0
  124. package/dist/web/panelQueryHandles.js +2749 -0
  125. package/dist/web/panelRoutes.js +2147 -0
  126. package/dist/web/panels.js +904 -0
  127. package/dist/web/peerInboxAggregates.js +1247 -0
  128. package/dist/web/planApprovalState.js +92 -0
  129. package/dist/web/platformAgentScheduler.js +66 -0
  130. package/dist/web/proactiveOpportunities.js +452 -0
  131. package/dist/web/promptContextSections.js +242 -0
  132. package/dist/web/promptHistorySanitizer.js +26 -0
  133. package/dist/web/promptSlashCommands.js +158 -0
  134. package/dist/web/rollingConversationSummary.js +453 -0
  135. package/dist/web/routeHelpers.js +11 -0
  136. package/dist/web/routes/handoff.js +288 -0
  137. package/dist/web/routes/history.js +345 -0
  138. package/dist/web/routes/memory.js +258 -0
  139. package/dist/web/routes/selfState.js +171 -0
  140. package/dist/web/routes/workspace.js +154 -0
  141. package/dist/web/runSurfaceWatermarks.js +431 -0
  142. package/dist/web/runtimeCapabilities.js +48 -0
  143. package/dist/web/sameAgentHandoffs.js +494 -0
  144. package/dist/web/server.js +15567 -0
  145. package/dist/web/sharedCollaborationCapsules.js +163 -0
  146. package/dist/web/soloSessionRelay.js +42 -0
  147. package/dist/web/soloWsHandler.js +138 -0
  148. package/dist/web/suggestedPlannerScheduler.js +56 -0
  149. package/dist/web/surfaceActivationPolicy.js +108 -0
  150. package/dist/web/surfaceCollaborators.js +61 -0
  151. package/dist/web/surfaceSystemStatus.js +263 -0
  152. package/dist/web/targetParticipants.js +77 -0
  153. package/dist/web/taskEvents.js +49 -0
  154. package/dist/web/taskLifecycleMessages.js +165 -0
  155. package/dist/web/taskLoops.js +732 -0
  156. package/dist/web/taskMemoryNotes.js +224 -0
  157. package/dist/web/taskNumbers.js +16 -0
  158. package/dist/web/taskOwnerGuards.js +49 -0
  159. package/dist/web/taskParticipantResolver.js +42 -0
  160. package/dist/web/taskParticipants.js +97 -0
  161. package/dist/web/taskSourceDetails.js +20 -0
  162. package/dist/web/taskStateViews.js +210 -0
  163. package/dist/web/taskStatusTransitions.js +9 -0
  164. package/dist/web/taskThreadFollowups.js +599 -0
  165. package/dist/web/taskThreadRuntimeClosure.js +685 -0
  166. package/dist/web/taskUpdateDelivery.js +104 -0
  167. package/dist/web/threadReplyContentHeuristics.js +30 -0
  168. package/dist/web/threadRoots.js +61 -0
  169. package/dist/web/threadTaskBindings.js +365 -0
  170. package/dist/web/uiPanelPromptGuidance.js +27 -0
  171. package/dist/web/workspaceMemoryHints.js +143 -0
  172. package/dist/web/workspaceToolPromptGuidance.js +30 -0
  173. package/dist/web/wsHandler.js +397 -0
  174. package/dist/web/wsSink.js +116 -0
  175. package/package.json +54 -0
@@ -0,0 +1,181 @@
1
+ import path from 'node:path';
2
+ export class WorkbenchRootService {
3
+ getAgentById;
4
+ getProjectById;
5
+ listProjects;
6
+ getResourceSpaceById;
7
+ listResourceSpaces;
8
+ nodeRegistry;
9
+ constructor(params) {
10
+ this.getAgentById = params.getAgentById;
11
+ this.getProjectById = params.getProjectById;
12
+ this.listProjects = params.listProjects;
13
+ this.getResourceSpaceById = params.getResourceSpaceById;
14
+ this.listResourceSpaces = params.listResourceSpaces;
15
+ this.nodeRegistry = params.nodeRegistry;
16
+ }
17
+ static buildAgentRootId(agentId) {
18
+ return `agent:${agentId}`;
19
+ }
20
+ static buildProjectRootId(projectId) {
21
+ return `project:${projectId}`;
22
+ }
23
+ static buildResourceRootId(resourceSpaceId) {
24
+ return `resource:${resourceSpaceId}`;
25
+ }
26
+ listRoots() {
27
+ const projectRoots = this.listProjectRoots();
28
+ const resourceRoots = this.listResourceSpaces()
29
+ .map((resourceSpace) => this.resourceSpaceToRoot(resourceSpace));
30
+ return [...projectRoots, ...resourceRoots];
31
+ }
32
+ getRoot(rootId) {
33
+ const parsed = parseWorkbenchRootId(rootId);
34
+ if (!parsed)
35
+ return null;
36
+ if (parsed.kind === 'agent_workspace') {
37
+ const agent = this.getAgentById(parsed.id);
38
+ const root = agent ? this.agentToMemoryRoot(agent) : null;
39
+ return root ? { ...root, rootPath: canonicalizeWorkbenchRootPath(agent.workspacePath) } : null;
40
+ }
41
+ if (parsed.kind === 'project_space') {
42
+ const project = this.getProjectById(parsed.id);
43
+ const root = project ? this.projectToRoot(project) : null;
44
+ return root ? { ...root, rootPath: canonicalizeWorkbenchRootPath(root.rootPath) } : null;
45
+ }
46
+ const resourceSpace = this.getResourceSpaceById(parsed.id);
47
+ const root = resourceSpace ? this.resourceSpaceToRoot(resourceSpace) : null;
48
+ return root ? { ...root, rootPath: canonicalizeWorkbenchRootPath(resourceSpace.rootPath) } : null;
49
+ }
50
+ listProjectRoots() {
51
+ return this.listProjects()
52
+ .map((project) => this.projectToRoot(project))
53
+ .filter((root) => !!root);
54
+ }
55
+ agentToMemoryRoot(agent) {
56
+ if (!agent.workspacePath)
57
+ return null;
58
+ const node = agent.nodeId ? this.nodeRegistry.getNode(agent.nodeId) : null;
59
+ const terminalSupported = Boolean(agent.nodeId && node?.terminalBackendAvailable);
60
+ return {
61
+ workbenchRootId: WorkbenchRootService.buildAgentRootId(agent.agentId),
62
+ kind: 'agent_workspace',
63
+ displayName: agent.name,
64
+ rootPath: canonicalizeWorkbenchRootPath(agent.workspacePath),
65
+ nodeId: agent.nodeId ?? null,
66
+ agentId: agent.agentId,
67
+ writable: true,
68
+ terminalSupported,
69
+ terminalDisabledReason: agent.nodeId
70
+ ? terminalSupported
71
+ ? null
72
+ : 'Persistent terminal backend is unavailable on the assigned node.'
73
+ : 'Agent is not assigned to a remote node.',
74
+ sourceLabel: 'Agent Workspace',
75
+ };
76
+ }
77
+ projectToRoot(project) {
78
+ const rootPath = project.rootPath?.trim() || null;
79
+ if (!rootPath || !isAbsoluteWorkbenchRootPath(rootPath))
80
+ return null;
81
+ const candidateNodeIds = project.backendType === 'local_path'
82
+ ? [project.nodeId].filter((value) => !!value)
83
+ : project.boundAgents
84
+ .map((agent) => agent.nodeId?.trim() || null)
85
+ .filter((value) => !!value);
86
+ const onlineNodeId = candidateNodeIds.find((nodeId) => !!this.nodeRegistry.getNode(nodeId)) ?? null;
87
+ const selectedNodeId = onlineNodeId ?? candidateNodeIds[0] ?? null;
88
+ const terminalSupported = candidateNodeIds.some((nodeId) => Boolean(this.nodeRegistry.getNode(nodeId)?.terminalBackendAvailable));
89
+ const agentIds = project.boundAgents.map((agent) => agent.agentId).sort((left, right) => left.localeCompare(right));
90
+ return {
91
+ workbenchRootId: WorkbenchRootService.buildProjectRootId(project.projectId),
92
+ kind: 'project_space',
93
+ displayName: project.name,
94
+ rootPath: canonicalizeWorkbenchRootPath(rootPath),
95
+ nodeId: selectedNodeId,
96
+ projectId: project.projectId,
97
+ projectBackendType: project.backendType,
98
+ agentIds,
99
+ writable: true,
100
+ terminalSupported,
101
+ terminalDisabledReason: terminalSupported
102
+ ? null
103
+ : project.backendType === 'shared_mount'
104
+ ? 'No bound online node currently exposes the persistent terminal backend for this shared project.'
105
+ : 'Persistent terminal backend is unavailable on the bound node.',
106
+ sourceLabel: project.backendType === 'shared_mount' ? 'Shared Project' : 'Project',
107
+ };
108
+ }
109
+ resourceSpaceToRoot(resourceSpace) {
110
+ const node = resourceSpace.nodeId ? this.nodeRegistry.getNode(resourceSpace.nodeId) : null;
111
+ const terminalSupported = resourceSpace.backendType === 'node_path'
112
+ && !!resourceSpace.nodeId
113
+ && Boolean(node?.terminalBackendAvailable);
114
+ const terminalDisabledReason = terminalSupported
115
+ ? null
116
+ : resourceSpace.backendType === 'shared_mount'
117
+ ? 'Shared-mount resources do not support persistent terminals.'
118
+ : resourceSpace.nodeId
119
+ ? 'Persistent terminal backend is unavailable on the bound node.'
120
+ : 'Resource space is missing its node binding.';
121
+ return {
122
+ workbenchRootId: WorkbenchRootService.buildResourceRootId(resourceSpace.resourceSpaceId),
123
+ kind: 'resource_space',
124
+ displayName: resourceSpace.name,
125
+ rootPath: canonicalizeWorkbenchRootPath(resourceSpace.rootPath),
126
+ nodeId: resourceSpace.nodeId ?? null,
127
+ resourceSpaceId: resourceSpace.resourceSpaceId,
128
+ resourceType: resourceSpace.resourceType,
129
+ backendType: resourceSpace.backendType,
130
+ writable: false,
131
+ terminalSupported,
132
+ terminalDisabledReason,
133
+ sourceLabel: 'Shared Resource',
134
+ };
135
+ }
136
+ }
137
+ function parseWorkbenchRootId(rootId) {
138
+ if (rootId.startsWith('agent:')) {
139
+ const id = rootId.slice('agent:'.length).trim();
140
+ return id ? { kind: 'agent_workspace', id } : null;
141
+ }
142
+ if (rootId.startsWith('project:')) {
143
+ const id = rootId.slice('project:'.length).trim();
144
+ return id ? { kind: 'project_space', id } : null;
145
+ }
146
+ if (rootId.startsWith('resource:')) {
147
+ const id = rootId.slice('resource:'.length).trim();
148
+ return id ? { kind: 'resource_space', id } : null;
149
+ }
150
+ return null;
151
+ }
152
+ export function canonicalizeWorkbenchRootPath(rootPath) {
153
+ const trimmed = rootPath.trim();
154
+ if (!trimmed)
155
+ return rootPath;
156
+ const pathModule = detectWorkbenchPathModule(trimmed);
157
+ if (!pathModule.isAbsolute(trimmed))
158
+ return trimmed;
159
+ const normalized = pathModule.normalize(trimmed);
160
+ const parsed = pathModule.parse(normalized);
161
+ if (normalized === parsed.root)
162
+ return normalized;
163
+ return normalized.replace(pathModule === path.win32 ? /[\\/]+$/ : /\/+$/, '');
164
+ }
165
+ export function isAbsoluteWorkbenchRootPath(rootPath) {
166
+ const trimmed = rootPath.trim();
167
+ if (!trimmed)
168
+ return false;
169
+ return path.posix.isAbsolute(trimmed) || path.win32.isAbsolute(trimmed);
170
+ }
171
+ export function getWorkbenchPathBasename(rootPath) {
172
+ const canonical = canonicalizeWorkbenchRootPath(rootPath);
173
+ const pathModule = detectWorkbenchPathModule(canonical);
174
+ return pathModule.basename(canonical);
175
+ }
176
+ function detectWorkbenchPathModule(rootPath) {
177
+ if (/^[a-zA-Z]:[\\/]/.test(rootPath) || rootPath.startsWith('\\\\') || rootPath.includes('\\')) {
178
+ return path.win32;
179
+ }
180
+ return path.posix;
181
+ }
@@ -0,0 +1,378 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export class WorkbenchTerminalBroker {
3
+ pending = new Map();
4
+ nodeRegistry;
5
+ timeoutMs;
6
+ routesByTerminalId = new Map();
7
+ socketsByTerminalId = new Map();
8
+ terminationListeners = new Set();
9
+ outputListeners = new Set();
10
+ bufferedOutputByTerminalId = new Map();
11
+ constructor(params) {
12
+ this.nodeRegistry = params.nodeRegistry;
13
+ this.timeoutMs = params.timeoutMs ?? 10_000;
14
+ }
15
+ listTerminals(nodeId, workspaceRoot) {
16
+ const requestId = randomUUID();
17
+ return new Promise((resolve, reject) => {
18
+ const timer = setTimeout(() => {
19
+ this.pending.delete(requestId);
20
+ reject(new Error('Terminal list request timed out.'));
21
+ }, this.timeoutMs);
22
+ this.pending.set(requestId, { nodeId, kind: 'list', resolve, reject, timer });
23
+ const sent = this.nodeRegistry.send(nodeId, {
24
+ type: 'terminal.list.request',
25
+ requestId,
26
+ workspaceRoot,
27
+ });
28
+ if (!sent) {
29
+ clearTimeout(timer);
30
+ this.pending.delete(requestId);
31
+ reject(new Error('Agent node is offline.'));
32
+ }
33
+ });
34
+ }
35
+ createTerminal(nodeId, params) {
36
+ const requestId = randomUUID();
37
+ return new Promise((resolve, reject) => {
38
+ const timer = setTimeout(() => {
39
+ this.pending.delete(requestId);
40
+ reject(new Error('Terminal create request timed out.'));
41
+ }, this.timeoutMs);
42
+ this.pending.set(requestId, {
43
+ nodeId,
44
+ kind: 'create',
45
+ bufferOutputUntilTaken: params.bufferOutputUntilTaken === true,
46
+ resolve,
47
+ reject,
48
+ timer,
49
+ });
50
+ const sent = this.nodeRegistry.send(nodeId, {
51
+ type: 'terminal.create.request',
52
+ requestId,
53
+ workspaceRoot: params.workspaceRoot,
54
+ cwd: params.cwd,
55
+ name: params.name,
56
+ startupCommand: params.startupCommand,
57
+ closeOnStartupCommand: params.closeOnStartupCommand,
58
+ cols: params.cols,
59
+ rows: params.rows,
60
+ });
61
+ if (!sent) {
62
+ clearTimeout(timer);
63
+ this.pending.delete(requestId);
64
+ reject(new Error('Agent node is offline.'));
65
+ }
66
+ });
67
+ }
68
+ snapshotTerminal(nodeId, terminalId) {
69
+ const requestId = randomUUID();
70
+ return new Promise((resolve, reject) => {
71
+ const timer = setTimeout(() => {
72
+ this.pending.delete(requestId);
73
+ reject(new Error('Terminal snapshot request timed out.'));
74
+ }, this.timeoutMs);
75
+ this.pending.set(requestId, { nodeId, kind: 'snapshot', resolve, reject, timer });
76
+ const sent = this.nodeRegistry.send(nodeId, {
77
+ type: 'terminal.snapshot.request',
78
+ requestId,
79
+ terminalId,
80
+ });
81
+ if (!sent) {
82
+ clearTimeout(timer);
83
+ this.pending.delete(requestId);
84
+ reject(new Error('Agent node is offline.'));
85
+ }
86
+ });
87
+ }
88
+ sendInput(nodeId, terminalId, data) {
89
+ return this.sendNoContentRequest(nodeId, 'input', {
90
+ type: 'terminal.input.request',
91
+ requestId: randomUUID(),
92
+ terminalId,
93
+ data,
94
+ });
95
+ }
96
+ resizeTerminal(nodeId, terminalId, cols, rows) {
97
+ return this.sendNoContentRequest(nodeId, 'resize', {
98
+ type: 'terminal.resize.request',
99
+ requestId: randomUUID(),
100
+ terminalId,
101
+ cols,
102
+ rows,
103
+ });
104
+ }
105
+ closeTerminal(nodeId, terminalId) {
106
+ const requestId = randomUUID();
107
+ return new Promise((resolve, reject) => {
108
+ const timer = setTimeout(() => {
109
+ this.pending.delete(requestId);
110
+ reject(new Error('Terminal close request timed out.'));
111
+ }, this.timeoutMs);
112
+ this.pending.set(requestId, { nodeId, kind: 'close', terminalId, resolve, reject, timer });
113
+ const sent = this.nodeRegistry.send(nodeId, {
114
+ type: 'terminal.close.request',
115
+ requestId,
116
+ terminalId,
117
+ });
118
+ if (!sent) {
119
+ clearTimeout(timer);
120
+ this.pending.delete(requestId);
121
+ reject(new Error('Agent node is offline.'));
122
+ }
123
+ });
124
+ }
125
+ getTerminalRoute(terminalId) {
126
+ return this.routesByTerminalId.get(terminalId) ?? null;
127
+ }
128
+ subscribe(terminalId, socket) {
129
+ let sockets = this.socketsByTerminalId.get(terminalId);
130
+ if (!sockets) {
131
+ sockets = new Set();
132
+ this.socketsByTerminalId.set(terminalId, sockets);
133
+ }
134
+ sockets.add(socket);
135
+ }
136
+ unsubscribe(terminalId, socket) {
137
+ const sockets = this.socketsByTerminalId.get(terminalId);
138
+ if (!sockets)
139
+ return;
140
+ sockets.delete(socket);
141
+ if (sockets.size === 0) {
142
+ this.socketsByTerminalId.delete(terminalId);
143
+ }
144
+ }
145
+ onTerminalTermination(listener) {
146
+ this.terminationListeners.add(listener);
147
+ return () => {
148
+ this.terminationListeners.delete(listener);
149
+ };
150
+ }
151
+ onOutput(listener) {
152
+ this.outputListeners.add(listener);
153
+ return () => {
154
+ this.outputListeners.delete(listener);
155
+ };
156
+ }
157
+ takeBufferedOutput(terminalId) {
158
+ const buffered = this.bufferedOutputByTerminalId.get(terminalId) ?? '';
159
+ this.bufferedOutputByTerminalId.delete(terminalId);
160
+ return buffered;
161
+ }
162
+ handleListResponse(msg) {
163
+ const pending = this.pending.get(msg.requestId);
164
+ if (!pending || pending.kind !== 'list')
165
+ return;
166
+ this.pending.delete(msg.requestId);
167
+ clearTimeout(pending.timer);
168
+ if (msg.error || !msg.terminals) {
169
+ pending.reject(new Error(formatTerminalError(msg.errorCode, msg.error)));
170
+ return;
171
+ }
172
+ for (const terminal of msg.terminals) {
173
+ this.trackTerminal(terminal, pending.nodeId);
174
+ }
175
+ pending.resolve(msg.terminals);
176
+ }
177
+ handleCreateResponse(msg) {
178
+ const pending = this.pending.get(msg.requestId);
179
+ if (!pending || pending.kind !== 'create')
180
+ return;
181
+ this.pending.delete(msg.requestId);
182
+ clearTimeout(pending.timer);
183
+ if (msg.error || !msg.terminal) {
184
+ pending.reject(new Error(formatTerminalError(msg.errorCode, msg.error)));
185
+ return;
186
+ }
187
+ this.trackTerminal(msg.terminal, pending.nodeId);
188
+ if (pending.bufferOutputUntilTaken) {
189
+ if (!this.bufferedOutputByTerminalId.has(msg.terminal.terminalId)) {
190
+ this.bufferedOutputByTerminalId.set(msg.terminal.terminalId, '');
191
+ }
192
+ }
193
+ else {
194
+ this.bufferedOutputByTerminalId.delete(msg.terminal.terminalId);
195
+ }
196
+ pending.resolve(msg.terminal);
197
+ }
198
+ handleSnapshotResponse(msg) {
199
+ const pending = this.pending.get(msg.requestId);
200
+ if (!pending || pending.kind !== 'snapshot')
201
+ return;
202
+ this.pending.delete(msg.requestId);
203
+ clearTimeout(pending.timer);
204
+ if (msg.error || !msg.terminal || msg.buffer === undefined) {
205
+ pending.reject(new Error(formatTerminalError(msg.errorCode, msg.error)));
206
+ return;
207
+ }
208
+ this.trackTerminal(msg.terminal, pending.nodeId);
209
+ pending.resolve({
210
+ terminal: msg.terminal,
211
+ buffer: msg.buffer,
212
+ });
213
+ }
214
+ handleInputResponse(msg) {
215
+ this.handleAckResponse(msg.requestId, msg.errorCode, msg.error, 'input');
216
+ }
217
+ handleResizeResponse(msg) {
218
+ this.handleAckResponse(msg.requestId, msg.errorCode, msg.error, 'resize');
219
+ }
220
+ handleCloseResponse(msg) {
221
+ const pending = this.pending.get(msg.requestId);
222
+ if (!pending || pending.kind !== 'close')
223
+ return;
224
+ this.pending.delete(msg.requestId);
225
+ clearTimeout(pending.timer);
226
+ if (msg.error) {
227
+ pending.reject(new Error(formatTerminalError(msg.errorCode, msg.error)));
228
+ return;
229
+ }
230
+ this.disposeTerminal(pending.terminalId, 'Terminal closed.');
231
+ pending.resolve();
232
+ }
233
+ handleOutputEvent(msg) {
234
+ const existingBuffer = this.bufferedOutputByTerminalId.get(msg.terminalId);
235
+ if (existingBuffer !== undefined) {
236
+ this.bufferedOutputByTerminalId.set(msg.terminalId, `${existingBuffer}${msg.data}`);
237
+ }
238
+ else if (!this.routesByTerminalId.has(msg.terminalId) && this.hasPendingBufferedCreate()) {
239
+ this.bufferedOutputByTerminalId.set(msg.terminalId, msg.data);
240
+ }
241
+ this.broadcast(msg.terminalId, {
242
+ type: 'output',
243
+ terminalId: msg.terminalId,
244
+ data: msg.data,
245
+ });
246
+ this.emitOutput({ terminalId: msg.terminalId, data: msg.data });
247
+ }
248
+ handleExitEvent(msg) {
249
+ this.broadcast(msg.terminalId, {
250
+ type: 'exit',
251
+ terminalId: msg.terminalId,
252
+ exitCode: msg.exitCode,
253
+ signal: msg.signal,
254
+ });
255
+ this.emitTermination({
256
+ terminalId: msg.terminalId,
257
+ exitCode: msg.exitCode,
258
+ signal: msg.signal,
259
+ disposed: false,
260
+ });
261
+ }
262
+ rejectPendingForNode(nodeId) {
263
+ for (const [requestId, pending] of this.pending.entries()) {
264
+ if (pending.nodeId !== nodeId)
265
+ continue;
266
+ clearTimeout(pending.timer);
267
+ pending.reject(new Error(`Agent node disconnected: ${nodeId}`));
268
+ this.pending.delete(requestId);
269
+ }
270
+ }
271
+ handleNodeDisconnect(nodeId) {
272
+ this.rejectPendingForNode(nodeId);
273
+ for (const [terminalId, route] of this.routesByTerminalId.entries()) {
274
+ if (route.nodeId !== nodeId)
275
+ continue;
276
+ this.disposeTerminal(terminalId, `Agent node disconnected: ${nodeId}`);
277
+ }
278
+ }
279
+ sendNoContentRequest(nodeId, kind, msg) {
280
+ return new Promise((resolve, reject) => {
281
+ const timer = setTimeout(() => {
282
+ this.pending.delete(msg.requestId);
283
+ reject(new Error(`Terminal ${kind} request timed out.`));
284
+ }, this.timeoutMs);
285
+ this.pending.set(msg.requestId, { nodeId, kind, resolve, reject, timer });
286
+ const sent = this.nodeRegistry.send(nodeId, msg);
287
+ if (!sent) {
288
+ clearTimeout(timer);
289
+ this.pending.delete(msg.requestId);
290
+ reject(new Error('Agent node is offline.'));
291
+ }
292
+ });
293
+ }
294
+ handleAckResponse(requestId, errorCode, error, kind) {
295
+ const pending = this.pending.get(requestId);
296
+ if (!pending || pending.kind !== kind)
297
+ return;
298
+ this.pending.delete(requestId);
299
+ clearTimeout(pending.timer);
300
+ if (error) {
301
+ pending.reject(new Error(formatTerminalError(errorCode, error)));
302
+ return;
303
+ }
304
+ pending.resolve();
305
+ }
306
+ hasPendingBufferedCreate() {
307
+ for (const pending of this.pending.values()) {
308
+ if (pending.kind === 'create' && pending.bufferOutputUntilTaken) {
309
+ return true;
310
+ }
311
+ }
312
+ return false;
313
+ }
314
+ trackTerminal(terminal, nodeId) {
315
+ this.routesByTerminalId.set(terminal.terminalId, {
316
+ nodeId,
317
+ workspaceRoot: terminal.workspaceRoot,
318
+ });
319
+ }
320
+ disposeTerminal(terminalId, reason) {
321
+ this.broadcast(terminalId, {
322
+ type: 'error',
323
+ message: reason,
324
+ });
325
+ this.bufferedOutputByTerminalId.delete(terminalId);
326
+ this.routesByTerminalId.delete(terminalId);
327
+ this.emitTermination({
328
+ terminalId,
329
+ reason,
330
+ disposed: true,
331
+ });
332
+ const sockets = this.socketsByTerminalId.get(terminalId);
333
+ if (!sockets)
334
+ return;
335
+ this.socketsByTerminalId.delete(terminalId);
336
+ for (const socket of sockets) {
337
+ if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING) {
338
+ socket.close();
339
+ }
340
+ }
341
+ }
342
+ broadcast(terminalId, event) {
343
+ const sockets = this.socketsByTerminalId.get(terminalId);
344
+ if (!sockets)
345
+ return;
346
+ const payload = JSON.stringify(event);
347
+ for (const socket of sockets) {
348
+ if (socket.readyState === socket.OPEN) {
349
+ socket.send(payload);
350
+ }
351
+ }
352
+ }
353
+ emitTermination(event) {
354
+ for (const listener of this.terminationListeners) {
355
+ try {
356
+ listener(event);
357
+ }
358
+ catch {
359
+ // Listener failures must not break terminal routing.
360
+ }
361
+ }
362
+ }
363
+ emitOutput(event) {
364
+ for (const listener of this.outputListeners) {
365
+ try {
366
+ listener(event);
367
+ }
368
+ catch {
369
+ // Listener failures must not break terminal routing.
370
+ }
371
+ }
372
+ }
373
+ }
374
+ function formatTerminalError(errorCode, error) {
375
+ if (errorCode)
376
+ return `${errorCode}:${error ?? 'terminal request failed'}`;
377
+ return error ?? 'terminal request failed';
378
+ }
@@ -0,0 +1,60 @@
1
+ export const WORKSPACE_RUN_DISPATCH_METHOD = 'workspace/run.dispatch';
2
+ const WORKSPACE_RUN_DISPATCH_SEQ = -1;
3
+ export function persistWorkspaceRunDispatchContext(db, runId, context) {
4
+ db.prepare(`INSERT OR REPLACE INTO events(run_id, seq, method, payload_json, created_at)
5
+ VALUES(?, ?, ?, ?, ?)`).run(runId, WORKSPACE_RUN_DISPATCH_SEQ, WORKSPACE_RUN_DISPATCH_METHOD, JSON.stringify(context), Date.now());
6
+ }
7
+ export function getWorkspaceRunDispatchContext(db, runId) {
8
+ const row = db.prepare(`SELECT payload_json as payloadJson
9
+ FROM events
10
+ WHERE run_id = ?
11
+ AND method = ?
12
+ LIMIT 1`).get(runId, WORKSPACE_RUN_DISPATCH_METHOD);
13
+ if (!row)
14
+ return null;
15
+ try {
16
+ const parsed = JSON.parse(row.payloadJson);
17
+ if (typeof parsed.nodeId !== 'string'
18
+ || !parsed.nodeId.trim()
19
+ || typeof parsed.workspaceSessionId !== 'string'
20
+ || typeof parsed.agentId !== 'string'
21
+ || typeof parsed.workspaceRootId !== 'string') {
22
+ return null;
23
+ }
24
+ return {
25
+ nodeId: parsed.nodeId,
26
+ workspaceSessionId: parsed.workspaceSessionId,
27
+ agentId: parsed.agentId,
28
+ workspaceRootId: parsed.workspaceRootId,
29
+ };
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function getWorkspaceRunDispatchNodeId(db, runId) {
36
+ return getWorkspaceRunDispatchContext(db, runId)?.nodeId ?? null;
37
+ }
38
+ export function getWorkspaceSessionOwnerNodeId(db, sessionKey) {
39
+ const row = db.prepare(`SELECT owner.payload_json as payloadJson
40
+ FROM runs r
41
+ JOIN events owner ON owner.run_id = r.run_id
42
+ WHERE r.session_key = ?
43
+ AND owner.method = ?
44
+ AND json_valid(owner.payload_json)
45
+ AND COALESCE(json_extract(owner.payload_json, '$.nodeId'), '') != ''
46
+ AND (r.ended_at IS NULL OR r.error IS NULL)
47
+ ORDER BY r.started_at DESC, owner.created_at DESC, r.run_id DESC
48
+ LIMIT 1`).get(sessionKey, WORKSPACE_RUN_DISPATCH_METHOD);
49
+ if (!row)
50
+ return null;
51
+ try {
52
+ const parsed = JSON.parse(row.payloadJson);
53
+ return typeof parsed.nodeId === 'string' && parsed.nodeId.trim()
54
+ ? parsed.nodeId
55
+ : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }