@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,190 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export class NativeMissionWorkerBroker {
3
+ pending = new Map();
4
+ active = new Map();
5
+ nodeRegistry;
6
+ timeoutMs;
7
+ callbacks;
8
+ constructor(params) {
9
+ this.nodeRegistry = params.nodeRegistry;
10
+ this.timeoutMs = params.timeoutMs ?? 15_000;
11
+ this.callbacks = params.callbacks ?? {};
12
+ }
13
+ setCallbacks(callbacks) {
14
+ this.callbacks = callbacks;
15
+ }
16
+ runMission(nodeId, mission, options = {}) {
17
+ const missionDir = mission.missionDir;
18
+ if (!missionDir) {
19
+ return Promise.reject(new Error('Mission has no missionDir configured.'));
20
+ }
21
+ const requestId = options.requestId ?? randomUUID();
22
+ return new Promise((resolve, reject) => {
23
+ const timer = setTimeout(() => {
24
+ this.pending.delete(requestId);
25
+ reject(new Error('Native mission start request timed out.'));
26
+ }, this.timeoutMs);
27
+ this.pending.set(requestId, {
28
+ nodeId,
29
+ missionId: mission.missionId,
30
+ featureId: options?.feature?.featureId,
31
+ attemptId: options.attemptId,
32
+ resolve,
33
+ reject,
34
+ timer,
35
+ });
36
+ const message = {
37
+ type: 'mission.run.request',
38
+ requestId,
39
+ missionId: mission.missionId,
40
+ runtimeProvider: 'bigbang_native',
41
+ title: mission.title,
42
+ prompt: options.promptOverride ?? (mission.description || mission.title),
43
+ workspaceRoot: mission.workspacePath,
44
+ missionDir,
45
+ modelMode: mission.modelMode,
46
+ orchestratorModel: mission.orchestratorModel,
47
+ workerModel: mission.workerModel,
48
+ validatorModel: mission.validatorModel,
49
+ ...(options.feature ?? {}),
50
+ };
51
+ const sent = this.nodeRegistry.send(nodeId, message);
52
+ if (!sent) {
53
+ clearTimeout(timer);
54
+ this.pending.delete(requestId);
55
+ reject(new Error('Agent node is offline.'));
56
+ }
57
+ });
58
+ }
59
+ hasActiveMission(missionId) {
60
+ for (const pending of this.pending.values()) {
61
+ if (pending.missionId === missionId)
62
+ return true;
63
+ }
64
+ for (const attempt of this.active.values()) {
65
+ if (attempt.missionId === missionId)
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ handleAccepted(msg, nodeId) {
71
+ const pending = this.pending.get(msg.requestId);
72
+ if (!pending || pending.nodeId !== nodeId || pending.missionId !== msg.missionId)
73
+ return null;
74
+ this.pending.delete(msg.requestId);
75
+ clearTimeout(pending.timer);
76
+ const attempt = {
77
+ attemptId: pending.attemptId,
78
+ requestId: msg.requestId,
79
+ nodeId,
80
+ missionId: msg.missionId,
81
+ featureId: msg.featureId ?? pending.featureId ?? null,
82
+ missionDir: msg.missionDir,
83
+ };
84
+ this.active.set(msg.requestId, attempt);
85
+ pending.resolve(msg);
86
+ this.callbacks.onAccepted?.(attempt, msg);
87
+ return attempt;
88
+ }
89
+ verifyEvent(msg, nodeId) {
90
+ const attempt = this.active.get(msg.requestId);
91
+ if (!attempt || attempt.nodeId !== nodeId || attempt.missionId !== msg.missionId)
92
+ return null;
93
+ return attempt;
94
+ }
95
+ handleEvent(msg, nodeId) {
96
+ const attempt = this.verifyEvent(msg, nodeId);
97
+ if (!attempt)
98
+ return null;
99
+ this.callbacks.onEvent?.(attempt, msg);
100
+ return attempt;
101
+ }
102
+ handleEnd(msg, nodeId) {
103
+ const pending = this.pending.get(msg.requestId);
104
+ if (pending) {
105
+ this.pending.delete(msg.requestId);
106
+ clearTimeout(pending.timer);
107
+ pending.reject(new Error(msg.error ?? 'Native mission did not start.'));
108
+ return null;
109
+ }
110
+ const attempt = this.active.get(msg.requestId);
111
+ if (!attempt || attempt.nodeId !== nodeId || attempt.missionId !== msg.missionId)
112
+ return null;
113
+ this.active.delete(msg.requestId);
114
+ this.callbacks.onEnd?.(attempt, msg);
115
+ return attempt;
116
+ }
117
+ /**
118
+ * Send a cancellation signal to the node for an active feature attempt.
119
+ * Returns true if a matching active attempt was found and the cancel message
120
+ * was sent; false otherwise.
121
+ */
122
+ cancelFeature(requestId) {
123
+ const attempt = this.active.get(requestId);
124
+ if (!attempt)
125
+ return false;
126
+ const message = {
127
+ type: 'mission.run.cancel',
128
+ requestId: attempt.requestId,
129
+ missionId: attempt.missionId,
130
+ runtimeProvider: 'bigbang_native',
131
+ featureId: attempt.featureId ?? null,
132
+ };
133
+ return this.nodeRegistry.send(attempt.nodeId, message);
134
+ }
135
+ /**
136
+ * Cancel all active feature attempts for a mission. Returns true if at least
137
+ * one cancellation message was sent.
138
+ */
139
+ cancelMissionFeatures(missionId) {
140
+ let sent = false;
141
+ for (const attempt of this.active.values()) {
142
+ if (attempt.missionId !== missionId)
143
+ continue;
144
+ const message = {
145
+ type: 'mission.run.cancel',
146
+ requestId: attempt.requestId,
147
+ missionId: attempt.missionId,
148
+ runtimeProvider: 'bigbang_native',
149
+ featureId: attempt.featureId ?? null,
150
+ };
151
+ if (this.nodeRegistry.send(attempt.nodeId, message)) {
152
+ sent = true;
153
+ }
154
+ }
155
+ return sent;
156
+ }
157
+ /**
158
+ * Reject all pending and active attempts for a disconnected node. Pending
159
+ * requests are rejected directly. Active attempts are surfaced through the
160
+ * onEnd callback with a synthetic mission.run.end message so the orchestrator
161
+ * can mark the feature and mission as failed.
162
+ */
163
+ rejectNodeAttempts(nodeId, error) {
164
+ for (const [requestId, pending] of this.pending.entries()) {
165
+ if (pending.nodeId !== nodeId)
166
+ continue;
167
+ this.pending.delete(requestId);
168
+ clearTimeout(pending.timer);
169
+ pending.reject(error);
170
+ }
171
+ for (const [requestId, attempt] of this.active.entries()) {
172
+ if (attempt.nodeId !== nodeId)
173
+ continue;
174
+ this.active.delete(requestId);
175
+ const supersededAt = Date.now();
176
+ this.callbacks.onAttemptSuperseded?.(attempt, 'orphan_cleanup', supersededAt);
177
+ const endMsg = {
178
+ type: 'mission.run.end',
179
+ requestId: attempt.requestId,
180
+ missionId: attempt.missionId,
181
+ runtimeProvider: 'bigbang_native',
182
+ missionDir: attempt.missionDir,
183
+ featureId: attempt.featureId ?? undefined,
184
+ error: error.message,
185
+ reason: 'node_disconnected',
186
+ };
187
+ this.callbacks.onEnd?.(attempt, endMsg);
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,34 @@
1
+ export class NodeRegistry {
2
+ entries = new Map();
3
+ register(entry) {
4
+ this.entries.set(entry.nodeId, entry);
5
+ }
6
+ unregister(nodeId) {
7
+ this.entries.delete(nodeId);
8
+ }
9
+ getNode(nodeId) {
10
+ return this.entries.get(nodeId);
11
+ }
12
+ listNodes() {
13
+ return [...this.entries.values()].map(({ ws: _ws, localSend: _localSend, terminalBackendAvailable, ...rest }) => ({
14
+ ...rest,
15
+ terminalBackendAvailable: Boolean(terminalBackendAvailable),
16
+ }));
17
+ }
18
+ heartbeat(nodeId) {
19
+ const entry = this.entries.get(nodeId);
20
+ if (entry)
21
+ entry.lastSeen = Date.now();
22
+ }
23
+ send(nodeId, msg) {
24
+ const entry = this.entries.get(nodeId);
25
+ if (!entry)
26
+ return false;
27
+ if (entry.localSend)
28
+ return entry.localSend(msg);
29
+ if (!entry.ws || entry.ws.readyState !== 1 /* OPEN */)
30
+ return false;
31
+ entry.ws.send(JSON.stringify(msg));
32
+ return true;
33
+ }
34
+ }
@@ -0,0 +1,97 @@
1
+ import { finishRun, log } from '@bbigbang/runtime-acp';
2
+ import { releaseDreamRunLock } from './memoryService.js';
3
+ function parseDreamAgentId(raw) {
4
+ if (!raw)
5
+ return null;
6
+ try {
7
+ const parsed = JSON.parse(raw);
8
+ return typeof parsed.agentId === 'string' && parsed.agentId.trim() ? parsed.agentId.trim() : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function reconcileNodeStateOnStartup(db) {
15
+ const now = Date.now();
16
+ const offlineResult = db
17
+ .prepare(`UPDATE nodes SET status = 'offline', last_seen = ? WHERE status = 'online'`)
18
+ .run(now);
19
+ const failedResult = db
20
+ .prepare(`UPDATE conversations
21
+ SET status = 'failed', updated_at = ?
22
+ WHERE node_id IS NOT NULL
23
+ AND status IN ('active', 'recovering', 'awaiting_approval')`)
24
+ .run(now);
25
+ const failedWorkspaceSessionResult = db
26
+ .prepare(`UPDATE workspace_sessions
27
+ SET status = 'failed',
28
+ updated_at = ?,
29
+ last_active_at = ?
30
+ WHERE status IN ('active', 'queued', 'recovering', 'awaiting_approval')`)
31
+ .run(now, now);
32
+ const openRuns = db.prepare(`SELECT r.run_id as runId,
33
+ rdi.dispatch_mode as dispatchMode,
34
+ rdi.activation_metadata_json as activationMetadataJson,
35
+ c.agent_id as conversationAgentId
36
+ FROM runs r
37
+ LEFT JOIN run_debug_inputs rdi ON rdi.run_id = r.run_id
38
+ LEFT JOIN conversations c ON c.session_key = r.session_key
39
+ WHERE r.ended_at IS NULL`).all();
40
+ for (const run of openRuns) {
41
+ if (run.dispatchMode === 'dream') {
42
+ const agentId = parseDreamAgentId(run.activationMetadataJson) ?? run.conversationAgentId;
43
+ if (agentId)
44
+ releaseDreamRunLock(db, agentId);
45
+ }
46
+ finishRun(db, {
47
+ runId: run.runId,
48
+ error: 'Core restarted before run completed',
49
+ });
50
+ }
51
+ const backfilledAgentResult = db
52
+ .prepare(`UPDATE conversations
53
+ SET agent_id = (
54
+ SELECT a.agent_id
55
+ FROM agents a
56
+ WHERE a.agent_type = conversations.agent_type
57
+ AND a.channel_id = conversations.channel_id
58
+ AND (
59
+ (a.node_id IS NULL AND conversations.node_id IS NULL)
60
+ OR a.node_id = conversations.node_id
61
+ )
62
+ AND (
63
+ (a.workspace_path IS NULL AND conversations.workspace_path IS NULL)
64
+ OR a.workspace_path = conversations.workspace_path
65
+ )
66
+ )
67
+ WHERE agent_id IS NULL
68
+ AND 1 = (
69
+ SELECT COUNT(*)
70
+ FROM agents a
71
+ WHERE a.agent_type = conversations.agent_type
72
+ AND a.channel_id = conversations.channel_id
73
+ AND (
74
+ (a.node_id IS NULL AND conversations.node_id IS NULL)
75
+ OR a.node_id = conversations.node_id
76
+ )
77
+ AND (
78
+ (a.workspace_path IS NULL AND conversations.workspace_path IS NULL)
79
+ OR a.workspace_path = conversations.workspace_path
80
+ )
81
+ )`)
82
+ .run();
83
+ const summary = {
84
+ offlinedNodes: offlineResult.changes,
85
+ failedConversations: failedResult.changes,
86
+ failedWorkspaceSessions: failedWorkspaceSessionResult.changes,
87
+ backfilledConversationAgents: backfilledAgentResult.changes,
88
+ finishedRuns: openRuns.length,
89
+ };
90
+ if (summary.offlinedNodes > 0
91
+ || summary.failedConversations > 0
92
+ || summary.failedWorkspaceSessions > 0
93
+ || summary.backfilledConversationAgents > 0) {
94
+ log.warn('[startup-reconcile] reset stale runtime state on startup', summary);
95
+ }
96
+ return summary;
97
+ }
@@ -0,0 +1,119 @@
1
+ import { PANEL_MEDIA_FETCH_CONCURRENCY, PANEL_MEDIA_REMOTE_FETCH_TIMEOUT_MS, } from '@bbigbang/protocol';
2
+ import { panelMediaDisplayTypeLimitBytes, panelMediaMimeMatchesDisplayType, panelMediaSizeCapExceededError, } from '../web/panelMediaPolicy.js';
3
+ class Semaphore {
4
+ permits;
5
+ waiters = [];
6
+ constructor(permits) {
7
+ this.permits = permits;
8
+ }
9
+ async acquire() {
10
+ if (this.permits > 0) {
11
+ this.permits -= 1;
12
+ return () => this.release();
13
+ }
14
+ await new Promise((resolve) => this.waiters.push(resolve));
15
+ return () => this.release();
16
+ }
17
+ release() {
18
+ this.permits += 1;
19
+ const next = this.waiters.shift();
20
+ if (next) {
21
+ this.permits -= 1;
22
+ next();
23
+ }
24
+ }
25
+ }
26
+ function withTimeout(promise, ms, errorMessage) {
27
+ if (!Number.isFinite(ms) || ms <= 0)
28
+ return promise;
29
+ const timeout = new Promise((_, reject) => {
30
+ const timer = setTimeout(() => reject(new Error(errorMessage)), ms);
31
+ // Ensure the timer does not keep the process alive in tests.
32
+ timer.unref?.();
33
+ });
34
+ return Promise.race([promise, timeout]);
35
+ }
36
+ export function createPanelMediaScanner(workspaceBroker, options = {}) {
37
+ const concurrency = options.concurrency ?? PANEL_MEDIA_FETCH_CONCURRENCY;
38
+ const semaphore = new Semaphore(concurrency);
39
+ return {
40
+ async scan(params) {
41
+ const stats = { peakInFlight: 0, totalRequested: params.items.length };
42
+ let inFlight = 0;
43
+ async function scanOne(item) {
44
+ const release = await semaphore.acquire();
45
+ inFlight += 1;
46
+ stats.peakInFlight = Math.max(stats.peakInFlight, inFlight);
47
+ try {
48
+ const streamResult = await withTimeout(workspaceBroker.streamExactPath(item.nodeId, item.absolutePath, {
49
+ allowedRoot: item.allowedRoot,
50
+ }), params.timeoutMs ?? PANEL_MEDIA_REMOTE_FETCH_TIMEOUT_MS, 'Media scan request timed out.');
51
+ return validateStreamResult(item, streamResult);
52
+ }
53
+ catch (error) {
54
+ const message = String(error?.message ?? error);
55
+ if (message.includes('timed out')) {
56
+ return {
57
+ ok: false,
58
+ rowIndex: item.rowIndex,
59
+ slot: item.slot,
60
+ statusCode: 504,
61
+ error: 'Media source timed out',
62
+ };
63
+ }
64
+ return {
65
+ ok: false,
66
+ rowIndex: item.rowIndex,
67
+ slot: item.slot,
68
+ statusCode: 502,
69
+ error: message,
70
+ };
71
+ }
72
+ finally {
73
+ inFlight -= 1;
74
+ release();
75
+ }
76
+ }
77
+ const promises = params.items.map((item) => scanOne(item));
78
+ const settled = await Promise.all(promises);
79
+ for (const result of settled) {
80
+ if (!result.ok) {
81
+ return { result: { outcome: 'error', error: result }, stats };
82
+ }
83
+ }
84
+ return { result: { outcome: 'ok', results: settled }, stats };
85
+ },
86
+ };
87
+ }
88
+ function validateStreamResult(item, streamResult) {
89
+ if (!panelMediaMimeMatchesDisplayType(streamResult.mimeType, item.displayType)) {
90
+ return {
91
+ ok: false,
92
+ rowIndex: item.rowIndex,
93
+ slot: item.slot,
94
+ statusCode: 415,
95
+ error: item.displayType === 'img'
96
+ ? 'Non-image media not supported'
97
+ : `Media mime type does not match declared displayType ${item.displayType}`,
98
+ };
99
+ }
100
+ const limitBytes = panelMediaDisplayTypeLimitBytes(item.displayType);
101
+ if (streamResult.size > limitBytes) {
102
+ const capError = panelMediaSizeCapExceededError(item.displayType);
103
+ return {
104
+ ok: false,
105
+ rowIndex: item.rowIndex,
106
+ slot: item.slot,
107
+ statusCode: capError.statusCode,
108
+ error: capError.error,
109
+ };
110
+ }
111
+ return {
112
+ ok: true,
113
+ rowIndex: item.rowIndex,
114
+ slot: item.slot,
115
+ mimeType: streamResult.mimeType,
116
+ size: streamResult.size,
117
+ displayType: item.displayType,
118
+ };
119
+ }
@@ -0,0 +1,153 @@
1
+ import { WorkspaceToolServiceError } from './workspaceToolErrors.js';
2
+ import { deriveTerminalRunCompletion, isJsonlEventEnvelope, isWorkspaceFileNotFoundError, normalizeStateRow, normalizeWorkspaceToolRunEventEnvelope, parseStateRowFromJsonlEvent, sleep, validateStateRowForManifest, } from './workspaceToolExecutionUtils.js';
3
+ import { joinWorkspaceRelativePath } from './workspaceToolUploadMaterializer.js';
4
+ const PERSISTENT_RUNTIME_LOG_POLL_MS = 250;
5
+ const PERSISTENT_RUNTIME_ACTION_TIMEOUT_MS = 120_000;
6
+ const PERSISTENT_RUNTIME_LOG_READ_LIMIT = 64 * 1024;
7
+ export class PersistentRuntimeJsonlClient {
8
+ deps;
9
+ constructor(deps) {
10
+ this.deps = deps;
11
+ }
12
+ normalizeLogDir(toolId, manifest) {
13
+ const raw = manifest.runtime?.logDir?.trim() || `.agent-tools-runtime/${toolId}`;
14
+ const normalized = raw.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
15
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../') || normalized.startsWith('/')) {
16
+ throw new WorkspaceToolServiceError('Tool runtime.logDir must stay inside the agent workspace.', 400);
17
+ }
18
+ return normalized;
19
+ }
20
+ buildRunLogPath(logDir, runId) {
21
+ return joinWorkspaceRelativePath(logDir, 'runs', `${runId}.jsonl`);
22
+ }
23
+ buildStdinRequest(params) {
24
+ return {
25
+ type: 'action',
26
+ runId: params.runId,
27
+ actionId: params.actionId,
28
+ params: params.actionParams,
29
+ toolId: params.toolId,
30
+ revision: params.revision,
31
+ };
32
+ }
33
+ stdinRequestToLine(request) {
34
+ return `${JSON.stringify(request)}\n`;
35
+ }
36
+ actionTimeoutMs(action) {
37
+ return action.maxRunSeconds ? action.maxRunSeconds * 1000 : PERSISTENT_RUNTIME_ACTION_TIMEOUT_MS;
38
+ }
39
+ async syncRunLog(params) {
40
+ const deadline = Date.now() + params.timeoutMs;
41
+ let offset = 0;
42
+ let pending = '';
43
+ let completion = null;
44
+ let lastReadError = null;
45
+ while (Date.now() <= deadline) {
46
+ const current = this.deps.getActiveTerminalId(params.toolId);
47
+ if (current !== params.terminalId) {
48
+ throw new WorkspaceToolServiceError('Persistent tool runtime exited before the action completed.', 502, { runId: params.runId });
49
+ }
50
+ try {
51
+ const file = await this.deps.readWorkspaceFile(params.agentNodeId, params.workspaceRoot, params.logPath, {
52
+ scaffold: false,
53
+ offset,
54
+ limit: PERSISTENT_RUNTIME_LOG_READ_LIMIT,
55
+ });
56
+ lastReadError = null;
57
+ if (file.content) {
58
+ offset = file.nextOffset ?? Math.min(file.size, offset + file.content.length);
59
+ pending += file.content;
60
+ const lines = pending.split('\n');
61
+ pending = lines.pop() ?? '';
62
+ for (const line of lines) {
63
+ if (completion)
64
+ break;
65
+ const observed = this.processLogLine(params, line);
66
+ if (observed)
67
+ completion = observed;
68
+ }
69
+ }
70
+ if (!completion && !file.hasMore && pending.trim()) {
71
+ try {
72
+ JSON.parse(pending.trim());
73
+ const observed = this.processLogLine(params, pending);
74
+ if (observed)
75
+ completion = observed;
76
+ pending = '';
77
+ }
78
+ catch {
79
+ // The runtime may still be appending the final JSONL record.
80
+ }
81
+ }
82
+ if (!file.hasMore && file.nextOffset != null) {
83
+ offset = file.nextOffset;
84
+ }
85
+ }
86
+ catch (error) {
87
+ if (isWorkspaceFileNotFoundError(error)) {
88
+ lastReadError = null;
89
+ // The runtime may not have created the run file yet. Keep polling until the action deadline.
90
+ }
91
+ else {
92
+ lastReadError = error;
93
+ }
94
+ }
95
+ if (completion)
96
+ return completion;
97
+ await sleep(PERSISTENT_RUNTIME_LOG_POLL_MS);
98
+ }
99
+ if (lastReadError) {
100
+ throw new WorkspaceToolServiceError(`Persistent tool runtime action could not read the run log: ${String(lastReadError?.message ?? lastReadError)}`, 502, { runId: params.runId });
101
+ }
102
+ const timeoutError = 'Persistent tool runtime action timed out: the runtime did not write a result/error event per protocol.';
103
+ params.onTimeoutEvent({
104
+ actionId: params.actionId,
105
+ timeoutMs: params.timeoutMs,
106
+ expectedResultPath: params.logPath,
107
+ error: timeoutError,
108
+ });
109
+ throw new WorkspaceToolServiceError(timeoutError, 504, { runId: params.runId });
110
+ }
111
+ processLogLine(ctx, line) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed)
114
+ return null;
115
+ let parsed;
116
+ try {
117
+ parsed = JSON.parse(trimmed);
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ const isEvent = isJsonlEventEnvelope(parsed);
123
+ const state = isEvent
124
+ ? parseStateRowFromJsonlEvent(parsed)
125
+ : normalizeStateRow(parsed, { rejectUnsupportedMediaKinds: true });
126
+ if (state) {
127
+ validateStateRowForManifest(ctx.manifest, state);
128
+ ctx.onStateUpdate(state);
129
+ return null;
130
+ }
131
+ const event = normalizeWorkspaceToolRunEventEnvelope(parsed);
132
+ if (!event)
133
+ return null;
134
+ ctx.onRunEvent(parsed);
135
+ if (event.eventType === 'result') {
136
+ const completion = deriveTerminalRunCompletion({ kind: 'result_event' });
137
+ return { ...completion, summary: event.summary };
138
+ }
139
+ if (event.eventType === 'error') {
140
+ const completion = deriveTerminalRunCompletion({
141
+ kind: 'error_event',
142
+ exitCode: typeof parsed.exitCode === 'number'
143
+ ? parsed.exitCode
144
+ : undefined,
145
+ signal: typeof parsed.signal === 'string'
146
+ ? parsed.signal
147
+ : undefined,
148
+ });
149
+ return { ...completion, summary: event.summary };
150
+ }
151
+ return null;
152
+ }
153
+ }