@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.
- package/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- 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
|
+
}
|