@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,93 @@
1
+ export class ProjectAccessResolverError extends Error {
2
+ statusCode;
3
+ constructor(statusCode, message) {
4
+ super(message);
5
+ this.statusCode = statusCode;
6
+ }
7
+ }
8
+ export class ProjectAccessResolver {
9
+ projectService;
10
+ nodeRegistry;
11
+ workspaceBroker;
12
+ preferredNodeByProjectId = new Map();
13
+ constructor(params) {
14
+ this.projectService = params.projectService;
15
+ this.nodeRegistry = params.nodeRegistry;
16
+ this.workspaceBroker = params.workspaceBroker;
17
+ }
18
+ getCandidateNodeIds(projectId) {
19
+ const project = this.projectService.getProject(projectId);
20
+ if (!project)
21
+ throw new ProjectAccessResolverError(404, 'Project not found.');
22
+ if (project.backendType === 'local_path') {
23
+ if (!project.nodeId) {
24
+ throw new ProjectAccessResolverError(409, 'Project is missing its node binding.');
25
+ }
26
+ return [project.nodeId];
27
+ }
28
+ const onlineNodeIds = this.projectService
29
+ .getProjectCandidateNodeIds(projectId)
30
+ .filter((nodeId) => !!this.nodeRegistry.getNode(nodeId));
31
+ const stickyNodeId = this.preferredNodeByProjectId.get(projectId) ?? null;
32
+ const preferredNodeIds = stickyNodeId && onlineNodeIds.includes(stickyNodeId)
33
+ ? [stickyNodeId]
34
+ : [];
35
+ const fallbackNodeIds = onlineNodeIds.filter((nodeId) => nodeId !== stickyNodeId);
36
+ const candidateNodeIds = [...preferredNodeIds, ...fallbackNodeIds];
37
+ if (candidateNodeIds.length === 0) {
38
+ throw new ProjectAccessResolverError(409, 'No online node can currently access this shared project.');
39
+ }
40
+ return candidateNodeIds;
41
+ }
42
+ getTerminalCandidateNodeIds(projectId) {
43
+ return this.getCandidateNodeIds(projectId)
44
+ .filter((nodeId) => Boolean(this.nodeRegistry.getNode(nodeId)?.terminalBackendAvailable));
45
+ }
46
+ async verifySharedBinding(projectId, nodeId) {
47
+ const project = this.projectService.getProject(projectId);
48
+ if (!project)
49
+ throw new ProjectAccessResolverError(404, 'Project not found.');
50
+ try {
51
+ await this.workspaceBroker.listDirectory(nodeId, project.rootPath, '', { scaffold: false });
52
+ this.preferredNodeByProjectId.set(projectId, nodeId);
53
+ }
54
+ catch (error) {
55
+ throw new ProjectAccessResolverError(409, `Node ${nodeId} cannot currently access shared project ${project.rootPath}: ${String(error?.message ?? error)}`);
56
+ }
57
+ }
58
+ async withResolvedNode(projectId, operation, options) {
59
+ const project = this.projectService.getProject(projectId);
60
+ if (!project)
61
+ throw new ProjectAccessResolverError(404, 'Project not found.');
62
+ const candidateNodeIds = this.getCandidateNodeIds(projectId);
63
+ let lastError = null;
64
+ const attempts = [];
65
+ for (const nodeId of candidateNodeIds) {
66
+ try {
67
+ const result = await operation(nodeId);
68
+ if (project.backendType === 'shared_mount') {
69
+ this.preferredNodeByProjectId.set(projectId, nodeId);
70
+ }
71
+ return result;
72
+ }
73
+ catch (error) {
74
+ lastError = error;
75
+ attempts.push(`${nodeId} -> ${String(error?.message ?? error)}`);
76
+ if (project.backendType === 'shared_mount' && shouldRetryOnNextNode(error, options?.retryOnTransportOnly ?? true)) {
77
+ continue;
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ throw new ProjectAccessResolverError(409, attempts.length > 0 ? `Unable to access shared project. Attempts: ${attempts.join(' | ')}` : 'Project access failed.');
83
+ }
84
+ }
85
+ function shouldRetryOnNextNode(error, retryOnTransportOnly) {
86
+ if (!retryOnTransportOnly)
87
+ return true;
88
+ const message = String(error?.message ?? error);
89
+ return (message === 'Agent node is offline.'
90
+ || message === 'Workspace request timed out.'
91
+ || message.startsWith('io_error:')
92
+ || message.startsWith('Agent node disconnected:'));
93
+ }
@@ -0,0 +1,392 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getWorkbenchPathBasename, isAbsoluteWorkbenchRootPath } from './workbenchRootService.js';
3
+ export class ProjectServiceError extends Error {
4
+ statusCode;
5
+ constructor(statusCode, message) {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ }
9
+ }
10
+ export class ProjectService {
11
+ db;
12
+ workspaceBroker;
13
+ getAgentById;
14
+ constructor(params) {
15
+ this.db = params.db;
16
+ this.workspaceBroker = params.workspaceBroker;
17
+ this.getAgentById = params.getAgentById;
18
+ }
19
+ listProjects() {
20
+ const rows = this.db.prepare(`SELECT project_id as projectId,
21
+ name,
22
+ backend_type as backendType,
23
+ root_path as rootPath,
24
+ node_id as nodeId,
25
+ description,
26
+ created_at as createdAt,
27
+ updated_at as updatedAt,
28
+ archived_at as archivedAt
29
+ FROM projects
30
+ WHERE archived_at IS NULL
31
+ ORDER BY updated_at DESC, name COLLATE NOCASE ASC`).all();
32
+ return rows.map((row) => this.rowToProjectInfo(row));
33
+ }
34
+ getProject(projectId) {
35
+ const row = this.getProjectRow(projectId);
36
+ return row ? this.rowToProjectInfo(row) : null;
37
+ }
38
+ getProjectBindingsForAgent(agentId) {
39
+ const rows = this.db.prepare(`SELECT p.project_id as projectId,
40
+ p.name,
41
+ p.backend_type as backendType,
42
+ p.root_path as rootPath,
43
+ p.node_id as nodeId,
44
+ p.description as description,
45
+ b.is_default as isDefault
46
+ FROM agent_project_bindings b
47
+ JOIN projects p ON p.project_id = b.project_id
48
+ WHERE b.agent_id = ?
49
+ AND p.archived_at IS NULL
50
+ ORDER BY b.is_default DESC, p.updated_at DESC, p.name COLLATE NOCASE ASC`).all(agentId);
51
+ return rows.map((row) => ({
52
+ projectId: row.projectId,
53
+ name: row.name,
54
+ backendType: row.backendType,
55
+ rootPath: row.rootPath,
56
+ nodeId: row.nodeId,
57
+ description: row.description,
58
+ isDefault: row.isDefault !== 0,
59
+ }));
60
+ }
61
+ createProject(req) {
62
+ const name = req.name.trim();
63
+ const rootPath = req.rootPath.trim();
64
+ const backendType = req.backendType;
65
+ const nodeId = normalizeNullableString(req.nodeId);
66
+ const description = normalizeNullableString(req.description);
67
+ if (!name)
68
+ throw new ProjectServiceError(400, 'Project name is required.');
69
+ if (!rootPath || !isAbsoluteWorkbenchRootPath(rootPath)) {
70
+ throw new ProjectServiceError(400, 'Project rootPath must be an absolute path.');
71
+ }
72
+ this.assertBackendConfiguration(backendType, nodeId);
73
+ const projectId = randomUUID();
74
+ const now = Date.now();
75
+ this.db.prepare(`INSERT INTO projects(
76
+ project_id,
77
+ name,
78
+ backend_type,
79
+ root_path,
80
+ node_id,
81
+ description,
82
+ created_at,
83
+ updated_at,
84
+ archived_at
85
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(projectId, name, backendType, rootPath, nodeId, description, now, now);
86
+ return this.getProject(projectId);
87
+ }
88
+ updateProject(projectId, req) {
89
+ const existing = this.getProjectRow(projectId);
90
+ if (!existing)
91
+ throw new ProjectServiceError(404, 'Project not found.');
92
+ const name = 'name' in req ? req.name?.trim() || existing.name : existing.name;
93
+ const backendType = req.backendType ?? existing.backendType;
94
+ const rootPath = 'rootPath' in req ? req.rootPath?.trim() || existing.rootPath : existing.rootPath;
95
+ const nodeId = 'nodeId' in req ? normalizeNullableString(req.nodeId) : existing.nodeId;
96
+ const description = 'description' in req ? normalizeNullableString(req.description) : existing.description;
97
+ if (!name)
98
+ throw new ProjectServiceError(400, 'Project name is required.');
99
+ if (!rootPath || !isAbsoluteWorkbenchRootPath(rootPath)) {
100
+ throw new ProjectServiceError(400, 'Project rootPath must be an absolute path.');
101
+ }
102
+ this.assertBackendConfiguration(backendType, nodeId);
103
+ if (backendType === 'local_path') {
104
+ const boundAgents = this.listBoundAgents(projectId);
105
+ const invalid = boundAgents.find((agent) => (agent.nodeId ?? null) !== nodeId);
106
+ if (invalid) {
107
+ throw new ProjectServiceError(409, `Agent ${invalid.name} is bound to a different node and cannot stay attached to this local-path project.`);
108
+ }
109
+ }
110
+ this.db.prepare(`UPDATE projects
111
+ SET name = ?,
112
+ backend_type = ?,
113
+ root_path = ?,
114
+ node_id = ?,
115
+ description = ?,
116
+ updated_at = ?,
117
+ archived_at = NULL
118
+ WHERE project_id = ?`).run(name, backendType, rootPath, nodeId, description, Date.now(), projectId);
119
+ return this.getProject(projectId);
120
+ }
121
+ archiveProject(projectId) {
122
+ const existing = this.getProjectRow(projectId);
123
+ if (!existing)
124
+ throw new ProjectServiceError(404, 'Project not found.');
125
+ const now = Date.now();
126
+ this.db.prepare(`UPDATE projects
127
+ SET archived_at = ?, updated_at = ?
128
+ WHERE project_id = ?`).run(now, now, projectId);
129
+ this.db.prepare(`DELETE FROM agent_project_bindings WHERE project_id = ?`).run(projectId);
130
+ this.db.prepare(`UPDATE conversations
131
+ SET active_project_id = NULL, updated_at = ?
132
+ WHERE active_project_id = ?`).run(now, projectId);
133
+ }
134
+ async bindProjectToAgent(projectId, req) {
135
+ const project = this.getProjectRow(projectId);
136
+ if (!project)
137
+ throw new ProjectServiceError(404, 'Project not found.');
138
+ const agent = this.getAgentById(req.agentId);
139
+ if (!agent)
140
+ throw new ProjectServiceError(404, 'Agent not found.');
141
+ await this.assertBindingAllowed(agent, project);
142
+ const now = Date.now();
143
+ this.db.prepare(`INSERT INTO agent_project_bindings(
144
+ agent_id,
145
+ project_id,
146
+ is_default,
147
+ created_at,
148
+ updated_at
149
+ ) VALUES(?, ?, ?, ?, ?)
150
+ ON CONFLICT(agent_id, project_id) DO UPDATE SET
151
+ updated_at = excluded.updated_at,
152
+ is_default = CASE WHEN excluded.is_default != 0 THEN 1 ELSE agent_project_bindings.is_default END`).run(agent.agentId, projectId, req.isDefault ? 1 : 0, now, now);
153
+ if (req.isDefault) {
154
+ this.setAgentDefaultProject(agent.agentId, { projectId });
155
+ }
156
+ return this.getProject(projectId);
157
+ }
158
+ unbindProjectFromAgent(projectId, agentId) {
159
+ const binding = this.db.prepare(`SELECT 1
160
+ FROM agent_project_bindings
161
+ WHERE agent_id = ?
162
+ AND project_id = ?`).get(agentId, projectId);
163
+ if (!binding)
164
+ throw new ProjectServiceError(404, 'Project binding not found.');
165
+ this.db.prepare(`DELETE FROM agent_project_bindings
166
+ WHERE agent_id = ?
167
+ AND project_id = ?`).run(agentId, projectId);
168
+ this.db.prepare(`UPDATE conversations
169
+ SET active_project_id = NULL,
170
+ updated_at = ?
171
+ WHERE agent_id = ?
172
+ AND active_project_id = ?`).run(Date.now(), agentId, projectId);
173
+ this.archiveProjectIfUnbound(projectId);
174
+ }
175
+ setAgentDefaultProject(agentId, req) {
176
+ const projectId = normalizeNullableString(req.projectId);
177
+ const previousDefaultProjectId = this.db.prepare(`SELECT project_id as projectId
178
+ FROM agent_project_bindings
179
+ WHERE agent_id = ?
180
+ AND is_default != 0
181
+ LIMIT 1`).get(agentId);
182
+ if (projectId) {
183
+ const bindingExists = this.db.prepare(`SELECT 1
184
+ FROM agent_project_bindings
185
+ WHERE agent_id = ?
186
+ AND project_id = ?
187
+ LIMIT 1`).get(agentId, projectId);
188
+ if (!bindingExists)
189
+ throw new ProjectServiceError(404, 'Project binding not found.');
190
+ }
191
+ this.db.prepare(`UPDATE agent_project_bindings
192
+ SET is_default = 0,
193
+ updated_at = ?
194
+ WHERE agent_id = ?`).run(Date.now(), agentId);
195
+ if (projectId) {
196
+ this.db.prepare(`UPDATE agent_project_bindings
197
+ SET is_default = 1,
198
+ updated_at = ?
199
+ WHERE agent_id = ?
200
+ AND project_id = ?`).run(Date.now(), agentId, projectId);
201
+ }
202
+ const nextDefaultProjectId = projectId ?? null;
203
+ if ((previousDefaultProjectId?.projectId ?? null) !== nextDefaultProjectId) {
204
+ const now = Date.now();
205
+ if (nextDefaultProjectId) {
206
+ this.db.prepare(`UPDATE conversations
207
+ SET active_project_id = ?,
208
+ updated_at = ?
209
+ WHERE agent_id = ?
210
+ AND (
211
+ active_project_id IS NULL
212
+ OR active_project_id = ?
213
+ )`).run(nextDefaultProjectId, now, agentId, previousDefaultProjectId?.projectId ?? null);
214
+ }
215
+ else if (previousDefaultProjectId?.projectId) {
216
+ this.db.prepare(`UPDATE conversations
217
+ SET active_project_id = NULL,
218
+ updated_at = ?
219
+ WHERE agent_id = ?
220
+ AND active_project_id = ?`).run(now, agentId, previousDefaultProjectId.projectId);
221
+ }
222
+ }
223
+ return this.getProjectBindingsForAgent(agentId);
224
+ }
225
+ async replaceLegacyLocalProjectPath(agentId, previousProjectPath, nextProjectPath) {
226
+ const agent = this.getAgentById(agentId);
227
+ if (!agent?.nodeId)
228
+ return;
229
+ const previousPath = normalizeNullableString(previousProjectPath);
230
+ const nextPath = normalizeNullableString(nextProjectPath);
231
+ if (previousPath && (!nextPath || previousPath !== nextPath)) {
232
+ const legacyProject = this.findLegacyLocalProject(agent.nodeId, previousPath);
233
+ if (legacyProject) {
234
+ try {
235
+ this.unbindProjectFromAgent(legacyProject.projectId, agentId);
236
+ }
237
+ catch {
238
+ // Ignore stale legacy bindings during migration.
239
+ }
240
+ }
241
+ }
242
+ if (nextPath) {
243
+ const project = this.ensureLegacyLocalProject(agent, nextPath);
244
+ await this.bindProjectToAgent(project.projectId, { agentId, isDefault: true });
245
+ }
246
+ else {
247
+ this.setAgentDefaultProject(agentId, { projectId: null });
248
+ }
249
+ }
250
+ getProjectCandidateNodeIds(projectId) {
251
+ const project = this.getProjectRow(projectId);
252
+ if (!project)
253
+ return [];
254
+ if (project.backendType === 'local_path') {
255
+ return project.nodeId ? [project.nodeId] : [];
256
+ }
257
+ const nodeIds = new Set();
258
+ for (const agent of this.listBoundAgents(projectId)) {
259
+ if (agent.nodeId)
260
+ nodeIds.add(agent.nodeId);
261
+ }
262
+ return [...nodeIds];
263
+ }
264
+ isAgentBoundToProject(agentId, projectId) {
265
+ return !!this.db.prepare(`SELECT 1
266
+ FROM agent_project_bindings
267
+ WHERE agent_id = ?
268
+ AND project_id = ?
269
+ LIMIT 1`).get(agentId, projectId);
270
+ }
271
+ rowToProjectInfo(row) {
272
+ return {
273
+ projectId: row.projectId,
274
+ name: row.name,
275
+ backendType: row.backendType,
276
+ rootPath: row.rootPath,
277
+ nodeId: row.nodeId,
278
+ description: row.description,
279
+ boundAgents: this.listBoundAgents(row.projectId),
280
+ createdAt: row.createdAt,
281
+ updatedAt: row.updatedAt,
282
+ };
283
+ }
284
+ getProjectRow(projectId) {
285
+ const row = this.db.prepare(`SELECT project_id as projectId,
286
+ name,
287
+ backend_type as backendType,
288
+ root_path as rootPath,
289
+ node_id as nodeId,
290
+ description,
291
+ created_at as createdAt,
292
+ updated_at as updatedAt,
293
+ archived_at as archivedAt
294
+ FROM projects
295
+ WHERE project_id = ?
296
+ AND archived_at IS NULL`).get(projectId);
297
+ return row ?? null;
298
+ }
299
+ listBoundAgents(projectId) {
300
+ const rows = this.db.prepare(`SELECT a.agent_id as agentId,
301
+ a.name as name,
302
+ a.node_id as nodeId,
303
+ b.is_default as isDefault
304
+ FROM agent_project_bindings b
305
+ JOIN agents a ON a.agent_id = b.agent_id
306
+ WHERE b.project_id = ?
307
+ AND a.deleted_at IS NULL
308
+ ORDER BY b.is_default DESC, a.name COLLATE NOCASE ASC`).all(projectId);
309
+ return rows.map((row) => ({
310
+ agentId: row.agentId,
311
+ name: row.name,
312
+ nodeId: row.nodeId,
313
+ isDefault: row.isDefault !== 0,
314
+ }));
315
+ }
316
+ ensureLegacyLocalProject(agent, projectPath) {
317
+ const existing = this.findLegacyLocalProject(agent.nodeId ?? null, projectPath);
318
+ if (existing)
319
+ return existing;
320
+ if (!agent.nodeId) {
321
+ throw new ProjectServiceError(409, 'Agent must be assigned to a node before it can bind a local project.');
322
+ }
323
+ const now = Date.now();
324
+ const projectId = randomUUID();
325
+ this.db.prepare(`INSERT INTO projects(
326
+ project_id,
327
+ name,
328
+ backend_type,
329
+ root_path,
330
+ node_id,
331
+ description,
332
+ created_at,
333
+ updated_at,
334
+ archived_at
335
+ ) VALUES(?, ?, 'local_path', ?, ?, NULL, ?, ?, NULL)`).run(projectId, getWorkbenchPathBasename(projectPath) || projectPath, projectPath, agent.nodeId, now, now);
336
+ return this.getProject(projectId);
337
+ }
338
+ findLegacyLocalProject(nodeId, projectPath) {
339
+ if (!nodeId)
340
+ return null;
341
+ const row = this.db.prepare(`SELECT project_id as projectId
342
+ FROM projects
343
+ WHERE archived_at IS NULL
344
+ AND backend_type = 'local_path'
345
+ AND node_id = ?
346
+ AND root_path = ?
347
+ ORDER BY updated_at DESC
348
+ LIMIT 1`).get(nodeId, projectPath);
349
+ return row ? this.getProject(row.projectId) : null;
350
+ }
351
+ archiveProjectIfUnbound(projectId) {
352
+ const remaining = this.db.prepare(`SELECT 1
353
+ FROM agent_project_bindings
354
+ WHERE project_id = ?
355
+ LIMIT 1`).get(projectId);
356
+ if (remaining)
357
+ return;
358
+ this.db.prepare(`UPDATE projects
359
+ SET archived_at = ?,
360
+ updated_at = ?
361
+ WHERE project_id = ?`).run(Date.now(), Date.now(), projectId);
362
+ }
363
+ async assertBindingAllowed(agent, project) {
364
+ if (!agent.nodeId) {
365
+ throw new ProjectServiceError(409, 'Agent must be assigned to a node before binding a project.');
366
+ }
367
+ if (project.backendType === 'local_path') {
368
+ if (!project.nodeId) {
369
+ throw new ProjectServiceError(409, 'Local-path project is missing its node binding.');
370
+ }
371
+ if (project.nodeId !== agent.nodeId) {
372
+ throw new ProjectServiceError(409, 'Local-path projects can only bind agents on the same node.');
373
+ }
374
+ return;
375
+ }
376
+ try {
377
+ await this.workspaceBroker.listDirectory(agent.nodeId, project.rootPath, '', { scaffold: false });
378
+ }
379
+ catch (error) {
380
+ throw new ProjectServiceError(409, `Node ${agent.nodeId} cannot currently access shared project ${project.rootPath}: ${String(error?.message ?? error)}`);
381
+ }
382
+ }
383
+ assertBackendConfiguration(backendType, nodeId) {
384
+ if (backendType === 'local_path' && !nodeId) {
385
+ throw new ProjectServiceError(400, 'local_path projects require a nodeId.');
386
+ }
387
+ }
388
+ }
389
+ function normalizeNullableString(value) {
390
+ const trimmed = value?.trim() ?? '';
391
+ return trimmed ? trimmed : null;
392
+ }
@@ -0,0 +1,140 @@
1
+ export class ResourceSpaceServiceError extends Error {
2
+ statusCode;
3
+ constructor(statusCode, message) {
4
+ super(message);
5
+ this.statusCode = statusCode;
6
+ }
7
+ }
8
+ export class ResourceSpaceService {
9
+ getResourceSpaceById;
10
+ broker;
11
+ nodeRegistry;
12
+ preferredSharedMountNodes = new Map();
13
+ constructor(params) {
14
+ this.getResourceSpaceById = params.getResourceSpaceById;
15
+ this.broker = params.broker;
16
+ this.nodeRegistry = params.nodeRegistry;
17
+ }
18
+ async listTree(resourceSpaceId, relativePath, options = {}) {
19
+ const resourceSpace = this.getResourceSpaceById(resourceSpaceId);
20
+ if (!resourceSpace)
21
+ throw new ResourceSpaceServiceError(404, 'Resource space not found.');
22
+ try {
23
+ return await this.withResolvedNode(resourceSpace, async (nodeId) => this.broker.listDirectory(nodeId, resourceSpace.rootPath, relativePath, { scaffold: false, ...options }));
24
+ }
25
+ catch (error) {
26
+ throw mapResourceSpaceError(error);
27
+ }
28
+ }
29
+ async readFile(resourceSpaceId, relativePath) {
30
+ const resourceSpace = this.getResourceSpaceById(resourceSpaceId);
31
+ if (!resourceSpace)
32
+ throw new ResourceSpaceServiceError(404, 'Resource space not found.');
33
+ try {
34
+ return await this.withResolvedNode(resourceSpace, async (nodeId) => this.broker.readFile(nodeId, resourceSpace.rootPath, relativePath, { scaffold: false }));
35
+ }
36
+ catch (error) {
37
+ throw mapResourceSpaceError(error);
38
+ }
39
+ }
40
+ async withResolvedNode(resourceSpace, operation) {
41
+ const candidateNodeIds = this.resolveCandidateNodeIds(resourceSpace);
42
+ let lastError = null;
43
+ const attemptSummaries = [];
44
+ for (const nodeId of candidateNodeIds) {
45
+ try {
46
+ const result = await operation(nodeId);
47
+ if (resourceSpace.backendType === 'shared_mount') {
48
+ this.preferredSharedMountNodes.set(resourceSpace.resourceSpaceId, nodeId);
49
+ }
50
+ return result;
51
+ }
52
+ catch (error) {
53
+ lastError = error;
54
+ attemptSummaries.push(formatNodeAttemptSummary(nodeId, error));
55
+ if (resourceSpace.backendType === 'shared_mount' && canTryNextSharedNode(error)) {
56
+ continue;
57
+ }
58
+ if (resourceSpace.backendType === 'node_path') {
59
+ const mapped = mapResourceSpaceError(error);
60
+ throw new ResourceSpaceServiceError(mapped.statusCode, `Node ${nodeId}: ${mapped.message}`);
61
+ }
62
+ throw error;
63
+ }
64
+ }
65
+ if (resourceSpace.backendType === 'shared_mount' && attemptSummaries.length > 0) {
66
+ const mapped = mapResourceSpaceError(lastError);
67
+ throw new ResourceSpaceServiceError(mapped.statusCode, `Unable to read shared resource space. Attempts: ${attemptSummaries.join(' | ')}`);
68
+ }
69
+ throw lastError ?? new Error('No node could access this resource space.');
70
+ }
71
+ resolveCandidateNodeIds(resourceSpace) {
72
+ if (resourceSpace.backendType === 'node_path') {
73
+ if (!resourceSpace.nodeId) {
74
+ throw new ResourceSpaceServiceError(409, 'Resource space is missing its node binding.');
75
+ }
76
+ return [resourceSpace.nodeId];
77
+ }
78
+ const onlineNodes = this.nodeRegistry
79
+ .listNodes()
80
+ .sort((a, b) => b.lastSeen - a.lastSeen);
81
+ const stickyNodeId = this.preferredSharedMountNodes.get(resourceSpace.resourceSpaceId) ?? null;
82
+ const preferredNodeId = stickyNodeId ?? resourceSpace.nodeId ?? null;
83
+ const preferred = preferredNodeId
84
+ ? onlineNodes.filter((node) => node.nodeId === preferredNodeId).map((node) => node.nodeId)
85
+ : [];
86
+ const fallbacks = onlineNodes
87
+ .filter((node) => node.nodeId !== preferredNodeId)
88
+ .map((node) => node.nodeId);
89
+ const candidateNodeIds = [...preferred, ...fallbacks];
90
+ if (candidateNodeIds.length === 0) {
91
+ throw new ResourceSpaceServiceError(409, 'No online node can currently access this shared resource space.');
92
+ }
93
+ return candidateNodeIds;
94
+ }
95
+ }
96
+ function canTryNextSharedNode(error) {
97
+ // Only retry on transport-/availability-level errors. Deterministic results
98
+ // (not_found, not_file, binary_file, file_too_large …) point at the file
99
+ // itself, so retrying on every other node just slows the request down and
100
+ // produces noisy multi-node attempt logs.
101
+ const message = String(error?.message ?? error);
102
+ return (message === 'Agent node is offline.'
103
+ || message === 'Workspace request timed out.'
104
+ || message.startsWith('io_error:'));
105
+ }
106
+ function mapResourceSpaceError(error) {
107
+ if (error instanceof ResourceSpaceServiceError) {
108
+ return error;
109
+ }
110
+ const message = String(error?.message ?? error);
111
+ if (message === 'Agent node is offline.') {
112
+ return new ResourceSpaceServiceError(409, message);
113
+ }
114
+ if (message === 'Workspace request timed out.') {
115
+ return new ResourceSpaceServiceError(504, message);
116
+ }
117
+ if (message.startsWith('not_found:')) {
118
+ return new ResourceSpaceServiceError(404, message.slice('not_found:'.length));
119
+ }
120
+ if (message.startsWith('path_outside_workspace:')) {
121
+ return new ResourceSpaceServiceError(400, message.slice('path_outside_workspace:'.length));
122
+ }
123
+ if (message.startsWith('binary_file:')) {
124
+ return new ResourceSpaceServiceError(415, message.slice('binary_file:'.length));
125
+ }
126
+ if (message.startsWith('file_too_large:')) {
127
+ return new ResourceSpaceServiceError(413, message.slice('file_too_large:'.length));
128
+ }
129
+ if (message.startsWith('not_directory:') || message.startsWith('not_file:')) {
130
+ return new ResourceSpaceServiceError(400, message.slice(message.indexOf(':') + 1));
131
+ }
132
+ if (message.startsWith('io_error:')) {
133
+ return new ResourceSpaceServiceError(500, message.slice('io_error:'.length));
134
+ }
135
+ return new ResourceSpaceServiceError(500, message);
136
+ }
137
+ function formatNodeAttemptSummary(nodeId, error) {
138
+ const mapped = mapResourceSpaceError(error);
139
+ return `${nodeId} -> ${mapped.message}`;
140
+ }