@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,1582 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { log } from '@bbigbang/runtime-acp';
3
+ import { actionRequiresCommand, parseWorkspaceToolManifest, resolveWorkspaceToolActionMode, } from './workspaceToolManifest.js';
4
+ import { WorkspaceToolServiceError } from './workspaceToolErrors.js';
5
+ import { buildWorkspaceToolStartupCommand, buildWorkspaceToolRuntimeCommand, CLI_STOP_CLEANUP_TIMEOUT_MS, closeTerminalBestEffort, deriveOneShotRunCompletion, deriveTerminalRunCompletion, extractWorkspaceToolBusinessStatus, getPublishedWorkspaceToolBundleRoot, isJsonlEventEnvelope, normalizeActionParams, normalizeStateRow, parseStateRowFromJsonlEvent, parseStateRowFromOutput, resolveToolCwd, resolveToolPath, rewriteWorkspaceToolCommandPaths, serializeJson, sleep, validateStateRowForManifest, waitForTerminalExit, } from './workspaceToolExecutionUtils.js';
6
+ import { WorkspaceToolRunLedger } from './workspaceToolRunLedger.js';
7
+ import { PersistentRuntimeJsonlClient } from './persistentRuntimeJsonlClient.js';
8
+ import { WorkspaceToolTerminalLifecycle, defaultPersistentRuntimeIdleTtlSeconds, } from './workspaceToolTerminalLifecycle.js';
9
+ import { WorkspaceToolUploadMaterializer, } from './workspaceToolUploadMaterializer.js';
10
+ const PERSISTENT_STARTUP_OUTPUT_SETTLE_MS = 1000;
11
+ const PERSISTENT_STARTUP_OUTPUT_POLL_MS = 50;
12
+ const PERSISTENT_STARTUP_SNAPSHOT_FALLBACK_MS = 2000;
13
+ const PERSISTENT_STARTUP_DUPLICATE_IGNORE_IDLE_MS = 1000;
14
+ const PERSISTENT_RUNTIME_ACTION_TIMEOUT_MS = 120_000;
15
+ function workspaceToolErrorWithRunId(error, runId) {
16
+ if (error instanceof WorkspaceToolServiceError) {
17
+ if (!error.runId)
18
+ error.runId = runId;
19
+ return error;
20
+ }
21
+ const wrapped = new WorkspaceToolServiceError(String(error?.message ?? error), 500, { runId });
22
+ wrapped.cause = error;
23
+ return wrapped;
24
+ }
25
+ function loadNodeHostname(db, nodeId) {
26
+ const row = db.prepare(`SELECT hostname
27
+ FROM nodes
28
+ WHERE node_id = ?
29
+ LIMIT 1`).get(nodeId);
30
+ return row?.hostname ?? null;
31
+ }
32
+ function isMissingTerminalError(error) {
33
+ const message = String(error?.message ?? error).toLowerCase();
34
+ return message.includes('not_found:terminal not found')
35
+ || message.includes('terminal not found')
36
+ || message.includes('unknown terminal');
37
+ }
38
+ export class WorkspaceToolActionRunner {
39
+ deps;
40
+ /*
41
+ * Run completion contract:
42
+ * - completed: one-shot terminal exitCode=0, persistent_runtime result event,
43
+ * or a stop action whose optional cleanup command exits successfully.
44
+ * - failed: one-shot non-zero exit/validation error, persistent_runtime error
45
+ * event/read failure, missing terminal while sending/closing, or cleanup
46
+ * failure that is not preserving an earlier action failure.
47
+ * - timed_out: persistent runtime action exceeded the JSONL result/error
48
+ * deadline, maxRunSeconds, or idle TTL without a protocol terminal event.
49
+ * - cancelled: user stop/restart/delete closes a previously active runtime
50
+ * terminal; missing already-closed terminals are recorded with
51
+ * signal='missing_terminal'.
52
+ *
53
+ * Latest-state writes are flushed before returning tool info to callers. Run
54
+ * completion rows are ledger facts; status-only snapshot sync after terminal
55
+ * lifecycle completion is best-effort and must not strand a completed run.
56
+ */
57
+ ignoredRealtimeStartupLineTimeouts = new Map();
58
+ observedPersistentStartupOutput = new Set();
59
+ startupStateReplaySuppressedRunIds = new Set();
60
+ ignoredRealtimeStartupLines = new Map();
61
+ runLedger;
62
+ uploadMaterializer;
63
+ latestStateProjector;
64
+ jsonlClient;
65
+ terminalLifecycle;
66
+ constructor(deps) {
67
+ this.deps = deps;
68
+ this.runLedger = new WorkspaceToolRunLedger({
69
+ db: this.deps.db,
70
+ loadUsername: (userId) => this.deps.loadUsername(userId),
71
+ isAdminUser: (userId) => this.deps.isAdminUser(userId),
72
+ });
73
+ this.uploadMaterializer = new WorkspaceToolUploadMaterializer({
74
+ db: this.deps.db,
75
+ isAdminUser: (userId) => this.deps.isAdminUser(userId),
76
+ hasRunRow: (runId) => this.runLedger.hasRunRow(runId),
77
+ writeWorkspaceFile: (nodeId, workspaceRoot, relativePath, content, mode, options) => this.deps.writeWorkspaceFile(nodeId, workspaceRoot, relativePath, content, mode, options),
78
+ deleteWorkspaceFile: (nodeId, workspaceRoot, relativePath) => this.deps.deleteWorkspaceFile(nodeId, workspaceRoot, relativePath),
79
+ });
80
+ this.latestStateProjector = deps.latestStateProjector;
81
+ this.jsonlClient = new PersistentRuntimeJsonlClient({
82
+ readWorkspaceFile: (nodeId, workspaceRoot, relativePath, options) => this.deps.readWorkspaceFile(nodeId, workspaceRoot, relativePath, options),
83
+ getActiveTerminalId: (toolId) => this.loadActiveTerminalId(toolId),
84
+ });
85
+ const activeTerminalBuffers = new Map();
86
+ this.terminalLifecycle = new WorkspaceToolTerminalLifecycle({
87
+ db: this.deps.db,
88
+ runLedger: this.runLedger,
89
+ latestStateProjector: this.latestStateProjector,
90
+ terminalBroker: this.deps.terminalBroker,
91
+ enqueueToolMutation: (toolId, work) => this.deps.enqueueToolMutation(toolId, work),
92
+ onBeforeFinish: (terminalId, runId) => {
93
+ this.flushRealtimeStateBuffer(terminalId);
94
+ this.clearPersistentStartupOutputObserved(runId);
95
+ this.clearIgnoredRealtimeStartupLines(terminalId);
96
+ },
97
+ maxActivePersistentRunsPerAgent: this.deps.maxActivePersistentRunsPerAgent,
98
+ }, activeTerminalBuffers);
99
+ }
100
+ processTerminalOutput(terminalId, chunk) {
101
+ const ctx = this.terminalLifecycle.getContext(terminalId);
102
+ if (!ctx)
103
+ return;
104
+ this.terminalLifecycle.resetPersistentIdleTimeout(terminalId);
105
+ this.processRealtimeStateChunk(ctx, terminalId, chunk);
106
+ }
107
+ processRealtimeStateChunk(ctx, terminalId, chunk) {
108
+ chunk = this.prepareRealtimeChunkAfterSnapshotFallback(ctx, terminalId, chunk);
109
+ if (!chunk)
110
+ return;
111
+ ctx.lineBuffer += chunk;
112
+ const lines = ctx.lineBuffer.split('\n');
113
+ ctx.lineBuffer = lines.pop() ?? '';
114
+ for (const line of lines) {
115
+ this.processRealtimeStateLine(ctx, terminalId, line);
116
+ }
117
+ }
118
+ processRealtimeStateLine(ctx, terminalId, line, options = {}) {
119
+ const trimmed = line.trim();
120
+ if (!trimmed)
121
+ return false;
122
+ const candidate = extractWorkspaceToolJsonLineCandidate(trimmed);
123
+ if (options.consumeIgnoredRealtimeLine !== false && (this.consumeIgnoredRealtimeStartupLine(terminalId, trimmed)
124
+ || (candidate ? this.consumeIgnoredRealtimeStartupLine(terminalId, candidate) : false))) {
125
+ return true;
126
+ }
127
+ let parsed;
128
+ try {
129
+ parsed = JSON.parse(candidate ?? trimmed);
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ try {
135
+ const isEvent = isJsonlEventEnvelope(parsed);
136
+ const state = isEvent
137
+ ? parseStateRowFromJsonlEvent(parsed)
138
+ : normalizeStateRow(parsed, { rejectUnsupportedMediaKinds: true });
139
+ if (!state && isEvent) {
140
+ if (ctx.runId) {
141
+ const inserted = this.insertRunEventFromEnvelope(ctx.runId, ctx.toolId, parsed);
142
+ if (inserted) {
143
+ this.markPersistentStartupOutputObserved(ctx.runId);
144
+ }
145
+ return inserted;
146
+ }
147
+ }
148
+ if (!state)
149
+ return false;
150
+ validateStateRowForManifest(ctx.manifest, state);
151
+ if (options.allowStateUpdate === false)
152
+ return true;
153
+ this.scheduleRealtimeStateUpdate(ctx, state);
154
+ if (ctx.runId) {
155
+ this.markPersistentStartupOutputObserved(ctx.runId);
156
+ }
157
+ return true;
158
+ }
159
+ catch (error) {
160
+ log.warn('[workspace-tools] ignoring invalid realtime tool state line', {
161
+ toolId: ctx.toolId,
162
+ terminalId,
163
+ error: String(error?.message ?? error),
164
+ });
165
+ return false;
166
+ }
167
+ }
168
+ scheduleRealtimeStateUpdate(ctx, state) {
169
+ this.latestStateProjector.scheduleRealtimeStateUpdate(ctx, state);
170
+ }
171
+ flushPendingRealtimeStateForTool(toolId) {
172
+ this.latestStateProjector.flushPendingRealtimeStateForTool(toolId);
173
+ }
174
+ clearPendingRealtimeStateForTool(toolId) {
175
+ this.latestStateProjector.clearPendingRealtimeStateForTool(toolId);
176
+ }
177
+ flushRealtimeStateBuffer(terminalId) {
178
+ const ctx = this.terminalLifecycle.getContext(terminalId);
179
+ if (!ctx)
180
+ return;
181
+ if (ctx.lineBuffer.trim()) {
182
+ this.processRealtimeStateLine(ctx, terminalId, ctx.lineBuffer);
183
+ ctx.lineBuffer = '';
184
+ }
185
+ this.latestStateProjector.flushPendingRealtimeStateForTool(ctx.toolId);
186
+ }
187
+ getToolInfoForActionViewerAfterPendingRealtimeFlush(params) {
188
+ return this.latestStateProjector.getToolInfoForActionViewerAfterPendingRealtimeFlush({
189
+ viewerUserId: params.viewerUserId,
190
+ toolId: params.toolRow.toolId,
191
+ allowAdminBypass: params.allowAdminBypass,
192
+ });
193
+ }
194
+ async invokeAction(params) {
195
+ const manifest = parseWorkspaceToolManifest(params.toolRow.manifestJson);
196
+ const action = manifest.actions.find((item) => item.actionId === params.actionId);
197
+ if (!action) {
198
+ throw new WorkspaceToolServiceError('Tool action not found.', 404);
199
+ }
200
+ const actionMode = resolveWorkspaceToolActionMode(action);
201
+ const actionParams = normalizeActionParams(action, params.inputParams ?? {});
202
+ if (actionMode === 'notify_agent') {
203
+ return this.deps.notifyAgentForToolAction({
204
+ toolRow: params.toolRow,
205
+ action,
206
+ viewerUserId: params.viewerUserId,
207
+ allowAdminBypass: params.allowAdminBypass,
208
+ requestedByAgentId: params.requestedByAgentId,
209
+ actionParams,
210
+ });
211
+ }
212
+ const agent = this.deps.conversationManager.getAgent(params.toolRow.agentId);
213
+ if (!agent?.nodeId || !agent.workspacePath) {
214
+ throw new WorkspaceToolServiceError('Agent workspace is unavailable for tool execution.', 409);
215
+ }
216
+ const agentNodeId = agent.nodeId;
217
+ const workspaceRoot = agent.workspacePath;
218
+ const executionHostname = loadNodeHostname(this.deps.db, agentNodeId);
219
+ const publishedBundleRoot = getPublishedWorkspaceToolBundleRoot(params.toolRow.toolId, params.toolRow.revision);
220
+ const actionCwdAbsolute = resolveToolCwd(workspaceRoot, manifest, action, params.toolRow.bundleRoot, publishedBundleRoot);
221
+ const runId = randomUUID();
222
+ const runStartedAt = Date.now();
223
+ const usesPersistentRuntime = manifest.runtime?.mode === 'persistent_runtime';
224
+ const hasExecutionEntry = manifest.cli?.entry?.trim() || (usesPersistentRuntime && manifest.runtime?.entry?.trim());
225
+ if (actionRequiresCommand(action) && !action.command?.trim() && !hasExecutionEntry) {
226
+ throw new WorkspaceToolServiceError(`Action "${action.actionId}" requires a command or manifest cli.entry.`, 400);
227
+ }
228
+ let materializedRun = null;
229
+ const getMaterializedRun = async () => {
230
+ if (materializedRun)
231
+ return materializedRun;
232
+ const materializedActionParams = await this.uploadMaterializer.materializeFileActionParams({
233
+ action,
234
+ actionParams,
235
+ runId,
236
+ toolId: params.toolRow.toolId,
237
+ viewerUserId: params.viewerUserId,
238
+ agentNodeId,
239
+ workspaceRoot,
240
+ });
241
+ const requesterAgent = params.requestedByAgentId
242
+ ? this.deps.conversationManager.getAgent(params.requestedByAgentId)
243
+ : null;
244
+ const requestedByUsername = requesterAgent
245
+ ? requesterAgent.name
246
+ : this.deps.loadUsername(params.viewerUserId);
247
+ const runBase = {
248
+ runId,
249
+ toolId: params.toolRow.toolId,
250
+ actionId: action.actionId,
251
+ terminalId: null,
252
+ status: 'running',
253
+ paramsJson: serializeJson(materializedActionParams),
254
+ exitCode: null,
255
+ signal: null,
256
+ startedAt: runStartedAt,
257
+ endedAt: null,
258
+ requestedByUserId: requesterAgent ? null : params.viewerUserId,
259
+ requestedByUsername,
260
+ requestedByAgentId: requesterAgent ? params.requestedByAgentId : null,
261
+ requestedByAgentName: requesterAgent ? requesterAgent.name : null,
262
+ toolRevision: params.toolRow.revision,
263
+ sourceBundleRoot: params.toolRow.bundleRoot,
264
+ actionCwdAbsolute,
265
+ runtimeLogDir: usesPersistentRuntime ? this.jsonlClient.normalizeLogDir(params.toolRow.toolId, manifest) : null,
266
+ executionTargetKind: 'owner_agent',
267
+ executionAgentId: params.toolRow.agentId,
268
+ executionNodeId: agentNodeId,
269
+ executionHostname,
270
+ executionWorkspacePath: workspaceRoot,
271
+ };
272
+ materializedRun = {
273
+ actionParams: materializedActionParams,
274
+ run: runBase,
275
+ stagedUploadPaths: this.uploadMaterializer.collectMaterializedUploadPaths(materializedActionParams),
276
+ };
277
+ return materializedRun;
278
+ };
279
+ if (usesPersistentRuntime) {
280
+ const materialized = await getMaterializedRun();
281
+ const { actionParams: materializedActionParams, run: runBase } = materialized;
282
+ return this.uploadMaterializer.invokeWithMaterializedUploads({ runId: materialized.run.runId, agentNodeId, workspaceRoot, stagedUploadPaths: materialized.stagedUploadPaths }, () => this.runPersistentRuntimeAction({
283
+ toolRow: params.toolRow,
284
+ manifest,
285
+ action,
286
+ viewerUserId: params.viewerUserId,
287
+ allowAdminBypass: params.allowAdminBypass,
288
+ agentNodeId,
289
+ workspaceRoot,
290
+ sourceBundleRoot: params.toolRow.bundleRoot,
291
+ publishedBundleRoot,
292
+ actionParams: materializedActionParams,
293
+ run: runBase,
294
+ }));
295
+ }
296
+ if (action.kind === 'start') {
297
+ if (params.toolRow.activeTerminalId) {
298
+ throw new WorkspaceToolServiceError('Tool already has an active persistent terminal.', 409);
299
+ }
300
+ return this.deps.enqueueAgentPersistentStart(params.toolRow.agentId, async () => {
301
+ this.terminalLifecycle.assertAgentPersistentRunQuota(params.toolRow.agentId);
302
+ const materialized = await getMaterializedRun();
303
+ const { actionParams: materializedActionParams, run: runBase } = materialized;
304
+ return this.uploadMaterializer.invokeWithMaterializedUploads({ runId: materialized.run.runId, agentNodeId, workspaceRoot, stagedUploadPaths: materialized.stagedUploadPaths }, () => this.startPersistentAction({
305
+ toolRow: params.toolRow,
306
+ manifest,
307
+ action,
308
+ viewerUserId: params.viewerUserId,
309
+ allowAdminBypass: params.allowAdminBypass,
310
+ agentNodeId,
311
+ workspaceRoot,
312
+ sourceBundleRoot: params.toolRow.bundleRoot,
313
+ publishedBundleRoot,
314
+ actionParams: materializedActionParams,
315
+ run: runBase,
316
+ }));
317
+ });
318
+ }
319
+ if (action.kind === 'restart') {
320
+ const materialized = await getMaterializedRun();
321
+ const { actionParams: materializedActionParams, run: runBase } = materialized;
322
+ if (params.toolRow.activeTerminalId) {
323
+ await this.deps.terminalBroker.closeTerminal(agentNodeId, params.toolRow.activeTerminalId);
324
+ this.finishActivePersistentRun(params.toolRow.toolId, params.toolRow.activeTerminalId, { status: 'cancelled' });
325
+ }
326
+ return this.deps.enqueueAgentPersistentStart(params.toolRow.agentId, async () => {
327
+ this.terminalLifecycle.assertAgentPersistentRunQuota(params.toolRow.agentId);
328
+ return this.uploadMaterializer.invokeWithMaterializedUploads({ runId: materialized.run.runId, agentNodeId, workspaceRoot, stagedUploadPaths: materialized.stagedUploadPaths }, () => this.startPersistentAction({
329
+ toolRow: params.toolRow,
330
+ manifest,
331
+ action,
332
+ viewerUserId: params.viewerUserId,
333
+ allowAdminBypass: params.allowAdminBypass,
334
+ agentNodeId,
335
+ workspaceRoot,
336
+ sourceBundleRoot: params.toolRow.bundleRoot,
337
+ publishedBundleRoot,
338
+ actionParams: materializedActionParams,
339
+ run: runBase,
340
+ }));
341
+ });
342
+ }
343
+ if (action.kind === 'stop') {
344
+ if (!params.toolRow.activeTerminalId) {
345
+ throw new WorkspaceToolServiceError('Tool is not running.', 409);
346
+ }
347
+ const materialized = await getMaterializedRun();
348
+ const { actionParams: materializedActionParams, run: runBase } = materialized;
349
+ if (action.command?.trim()) {
350
+ return this.uploadMaterializer.invokeWithMaterializedUploads({ runId: materialized.run.runId, agentNodeId, workspaceRoot, stagedUploadPaths: materialized.stagedUploadPaths }, () => this.runOneShotAction({
351
+ toolRow: params.toolRow,
352
+ manifest,
353
+ action,
354
+ viewerUserId: params.viewerUserId,
355
+ allowAdminBypass: params.allowAdminBypass,
356
+ agentNodeId,
357
+ workspaceRoot,
358
+ sourceBundleRoot: params.toolRow.bundleRoot,
359
+ publishedBundleRoot,
360
+ actionParams: materializedActionParams,
361
+ run: runBase,
362
+ strictStatusJson: false,
363
+ }));
364
+ }
365
+ return this.uploadMaterializer.invokeWithMaterializedUploads({ runId: materialized.run.runId, agentNodeId, workspaceRoot, stagedUploadPaths: materialized.stagedUploadPaths }, async () => {
366
+ await this.deps.terminalBroker.closeTerminal(agentNodeId, params.toolRow.activeTerminalId);
367
+ this.finishActivePersistentRun(params.toolRow.toolId, params.toolRow.activeTerminalId, { status: 'cancelled' });
368
+ const runTerminalId = params.toolRow.activeTerminalId;
369
+ this.insertRun({
370
+ ...runBase,
371
+ terminalId: runTerminalId,
372
+ });
373
+ let cleanupResult = null;
374
+ if (manifest.cli?.entry?.trim()) {
375
+ cleanupResult = await this.runCliStopCleanupBestEffort({
376
+ toolRow: params.toolRow,
377
+ manifest,
378
+ action,
379
+ agentNodeId,
380
+ workspaceRoot,
381
+ sourceBundleRoot: params.toolRow.bundleRoot,
382
+ publishedBundleRoot,
383
+ actionParams: materializedActionParams,
384
+ runId: runBase.runId,
385
+ });
386
+ }
387
+ const completedAt = Date.now();
388
+ const cleanupCompletion = cleanupResult?.completion ?? {
389
+ status: 'completed',
390
+ exitCode: 0,
391
+ signal: null,
392
+ };
393
+ this.completeRun(runBase.runId, {
394
+ status: cleanupCompletion.status,
395
+ endedAt: completedAt,
396
+ exitCode: cleanupCompletion.exitCode,
397
+ signal: cleanupCompletion.signal,
398
+ });
399
+ await this.refreshLatestStateFromStatusActionSilently({
400
+ toolRow: params.toolRow,
401
+ manifest,
402
+ agentNodeId,
403
+ workspaceRoot,
404
+ sourceBundleRoot: params.toolRow.bundleRoot,
405
+ publishedBundleRoot,
406
+ });
407
+ const tool = this.deps.getToolInfoForActionViewer(params.viewerUserId, params.toolRow.toolId, params.allowAdminBypass);
408
+ const run = this.runInfoForActionViewer({
409
+ ...runBase,
410
+ terminalId: runTerminalId,
411
+ status: cleanupCompletion.status,
412
+ endedAt: completedAt,
413
+ exitCode: cleanupCompletion.exitCode,
414
+ signal: cleanupCompletion.signal,
415
+ }, action.kind, params.viewerUserId, params.toolRow.userId);
416
+ await this.deps.recordPlatformExecNotification({
417
+ toolRow: params.toolRow,
418
+ action,
419
+ run,
420
+ viewerUserId: params.viewerUserId,
421
+ actionParams: materializedActionParams,
422
+ outputText: null,
423
+ });
424
+ return {
425
+ mode: 'platform_exec',
426
+ tool,
427
+ run,
428
+ latestState: tool.latestState ?? null,
429
+ outputText: null,
430
+ };
431
+ });
432
+ }
433
+ const materialized = await getMaterializedRun();
434
+ const { actionParams: materializedActionParams, run: runBase } = materialized;
435
+ return this.uploadMaterializer.invokeWithMaterializedUploads({ runId: materialized.run.runId, agentNodeId, workspaceRoot, stagedUploadPaths: materialized.stagedUploadPaths }, () => this.runOneShotAction({
436
+ toolRow: params.toolRow,
437
+ manifest,
438
+ action,
439
+ viewerUserId: params.viewerUserId,
440
+ allowAdminBypass: params.allowAdminBypass,
441
+ agentNodeId,
442
+ workspaceRoot,
443
+ sourceBundleRoot: params.toolRow.bundleRoot,
444
+ publishedBundleRoot,
445
+ actionParams: materializedActionParams,
446
+ run: runBase,
447
+ strictStatusJson: action.kind === 'status',
448
+ }));
449
+ }
450
+ async runCliStopCleanupBestEffort(params) {
451
+ this.insertCleanupRunEvent(params.runId, params.toolRow.toolId, 'cleanup_started', {
452
+ summary: 'Cleanup started',
453
+ actionId: params.action.actionId,
454
+ timeoutMs: CLI_STOP_CLEANUP_TIMEOUT_MS,
455
+ });
456
+ const cwd = resolveToolCwd(params.workspaceRoot, params.manifest, params.action, params.sourceBundleRoot, params.publishedBundleRoot);
457
+ const command = rewriteWorkspaceToolCommandPaths(params.workspaceRoot, buildWorkspaceToolStartupCommand({
458
+ manifest: params.manifest,
459
+ action: params.action,
460
+ actionParams: params.actionParams,
461
+ workspaceRoot: params.workspaceRoot,
462
+ publishedBundleRoot: params.publishedBundleRoot,
463
+ }), params.sourceBundleRoot, params.publishedBundleRoot);
464
+ let terminalId = null;
465
+ let latestObservedOutputText = '';
466
+ try {
467
+ const terminal = await this.deps.terminalBroker.createTerminal(params.agentNodeId, {
468
+ workspaceRoot: params.workspaceRoot,
469
+ cwd,
470
+ name: `${params.toolRow.name}: ${params.action.label} (cleanup)`,
471
+ startupCommand: command,
472
+ closeOnStartupCommand: true,
473
+ });
474
+ terminalId = terminal.terminalId;
475
+ const snapshot = await waitForTerminalExit(this.deps.terminalBroker, params.agentNodeId, terminalId, {
476
+ timeoutMs: CLI_STOP_CLEANUP_TIMEOUT_MS,
477
+ timeoutMessage: 'Timed out waiting for workspace-tool CLI stop cleanup to finish.',
478
+ onSnapshot: (current) => {
479
+ latestObservedOutputText = current.buffer ?? '';
480
+ },
481
+ });
482
+ const outputText = snapshot.buffer ?? '';
483
+ this.recordRunEventsFromOutput(params.runId, params.toolRow.toolId, outputText);
484
+ const completion = deriveOneShotRunCompletion(snapshot);
485
+ if (completion.status === 'completed') {
486
+ this.insertCleanupRunEvent(params.runId, params.toolRow.toolId, 'cleanup_completed', {
487
+ summary: 'Cleanup completed',
488
+ actionId: params.action.actionId,
489
+ exitCode: completion.exitCode,
490
+ signal: completion.signal,
491
+ });
492
+ }
493
+ else {
494
+ this.insertCleanupRunEvent(params.runId, params.toolRow.toolId, 'cleanup_failed', {
495
+ summary: 'Cleanup failed',
496
+ actionId: params.action.actionId,
497
+ exitCode: completion.exitCode,
498
+ signal: completion.signal,
499
+ });
500
+ }
501
+ return {
502
+ outputText,
503
+ completion,
504
+ };
505
+ }
506
+ catch (error) {
507
+ let outputText = latestObservedOutputText;
508
+ if (terminalId) {
509
+ outputText = outputText || await this.captureTerminalOutputBestEffort(params.agentNodeId, terminalId);
510
+ this.recordRunEventsFromOutput(params.runId, params.toolRow.toolId, outputText);
511
+ }
512
+ log.warn('[workspace-tools] CLI stop cleanup failed', {
513
+ toolId: params.toolRow.toolId,
514
+ actionId: params.action.actionId,
515
+ error: String(error?.message ?? error),
516
+ });
517
+ const timedOut = error instanceof WorkspaceToolServiceError && error.statusCode === 504;
518
+ this.insertCleanupRunEvent(params.runId, params.toolRow.toolId, timedOut ? 'cleanup_timed_out' : 'cleanup_failed', {
519
+ summary: timedOut ? 'Cleanup timed out' : 'Cleanup failed',
520
+ actionId: params.action.actionId,
521
+ timeoutMs: timedOut ? CLI_STOP_CLEANUP_TIMEOUT_MS : undefined,
522
+ error: String(error?.message ?? error),
523
+ });
524
+ return {
525
+ outputText,
526
+ completion: {
527
+ status: 'failed',
528
+ exitCode: null,
529
+ signal: null,
530
+ },
531
+ };
532
+ }
533
+ finally {
534
+ if (terminalId) {
535
+ await closeTerminalBestEffort(this.deps.terminalBroker, params.agentNodeId, terminalId);
536
+ }
537
+ }
538
+ }
539
+ insertCleanupRunEvent(runId, toolId, type, payload) {
540
+ this.runLedger.insertCleanupRunEvent(runId, toolId, type, payload);
541
+ }
542
+ insertMissingTerminalRunEvent(runId, toolId, payload) {
543
+ this.runLedger.insertMissingTerminalRunEvent(runId, toolId, payload);
544
+ }
545
+ insertPersistentRuntimeActionTimeoutRunEvent(runId, toolId, payload) {
546
+ this.runLedger.insertPersistentRuntimeActionTimeoutRunEvent(runId, toolId, payload);
547
+ }
548
+ findPlatformStatusAction(manifest) {
549
+ const statusActions = manifest.actions.filter((candidate) => {
550
+ if (candidate.kind !== 'status')
551
+ return false;
552
+ if (resolveWorkspaceToolActionMode(candidate) !== 'platform_exec')
553
+ return false;
554
+ if (actionRequiresCommand(candidate) && !candidate.command?.trim() && !manifest.cli?.entry?.trim())
555
+ return false;
556
+ return true;
557
+ });
558
+ return statusActions.find((candidate) => candidate.actionId === 'status') ?? statusActions[0] ?? null;
559
+ }
560
+ async refreshLatestStateFromStatusActionSilently(params) {
561
+ const statusAction = this.findPlatformStatusAction(params.manifest);
562
+ if (!statusAction)
563
+ return null;
564
+ const cwd = resolveToolCwd(params.workspaceRoot, params.manifest, statusAction, params.sourceBundleRoot, params.publishedBundleRoot);
565
+ const command = rewriteWorkspaceToolCommandPaths(params.workspaceRoot, buildWorkspaceToolStartupCommand({
566
+ manifest: params.manifest,
567
+ action: statusAction,
568
+ actionParams: normalizeActionParams(statusAction, {}),
569
+ workspaceRoot: params.workspaceRoot,
570
+ publishedBundleRoot: params.publishedBundleRoot,
571
+ }), params.sourceBundleRoot, params.publishedBundleRoot);
572
+ let terminalId = null;
573
+ try {
574
+ const terminal = await this.deps.terminalBroker.createTerminal(params.agentNodeId, {
575
+ workspaceRoot: params.workspaceRoot,
576
+ cwd,
577
+ name: `${params.toolRow.name}: ${statusAction.label} (silent refresh)`,
578
+ startupCommand: command,
579
+ closeOnStartupCommand: true,
580
+ });
581
+ terminalId = terminal.terminalId;
582
+ const snapshot = await waitForTerminalExit(this.deps.terminalBroker, params.agentNodeId, terminalId);
583
+ const completion = deriveOneShotRunCompletion(snapshot);
584
+ if (completion.status !== 'completed')
585
+ return null;
586
+ const parsedState = parseStateRowFromOutput(snapshot.buffer ?? '', true);
587
+ if (!parsedState)
588
+ return null;
589
+ validateStateRowForManifest(params.manifest, parsedState);
590
+ this.latestStateProjector.updateLatestState({
591
+ toolId: params.toolRow.toolId,
592
+ userId: params.toolRow.userId,
593
+ agentId: params.toolRow.agentId,
594
+ maintenanceConversationId: params.toolRow.maintenanceConversationId,
595
+ state: parsedState,
596
+ });
597
+ return parsedState;
598
+ }
599
+ catch (error) {
600
+ log.warn('[workspace-tools] silent status refresh failed', {
601
+ toolId: params.toolRow.toolId,
602
+ actionId: statusAction.actionId,
603
+ error: String(error?.message ?? error),
604
+ });
605
+ return null;
606
+ }
607
+ finally {
608
+ if (terminalId) {
609
+ await closeTerminalBestEffort(this.deps.terminalBroker, params.agentNodeId, terminalId);
610
+ }
611
+ }
612
+ }
613
+ async refreshLatestStateAfterPersistentStart(params) {
614
+ const maxAttempts = 5;
615
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
616
+ const parsedState = await this.refreshLatestStateFromStatusActionSilently(params);
617
+ const businessStatus = extractWorkspaceToolBusinessStatus(parsedState);
618
+ if (!parsedState) {
619
+ return false;
620
+ }
621
+ if (businessStatus !== 'stopped') {
622
+ this.suppressPersistentStartupStateReplay(params.runId);
623
+ return true;
624
+ }
625
+ const latestRow = this.deps.db.prepare(`SELECT active_terminal_id as activeTerminalId
626
+ FROM workspace_tools
627
+ WHERE tool_id = ?
628
+ AND deleted_at IS NULL
629
+ LIMIT 1`).get(params.toolRow.toolId);
630
+ if (!latestRow?.activeTerminalId) {
631
+ this.suppressPersistentStartupStateReplay(params.runId);
632
+ return true;
633
+ }
634
+ if (attempt >= maxAttempts - 1) {
635
+ return true;
636
+ }
637
+ if (this.deps.silentStatusRefreshRetryDelayMs > 0) {
638
+ await sleep(this.deps.silentStatusRefreshRetryDelayMs);
639
+ }
640
+ }
641
+ return false;
642
+ }
643
+ async startPersistentAction(params) {
644
+ const cwd = resolveToolCwd(params.workspaceRoot, params.manifest, params.action, params.sourceBundleRoot, params.publishedBundleRoot);
645
+ const command = rewriteWorkspaceToolCommandPaths(params.workspaceRoot, buildWorkspaceToolStartupCommand({
646
+ manifest: params.manifest,
647
+ action: params.action,
648
+ actionParams: params.actionParams,
649
+ workspaceRoot: params.workspaceRoot,
650
+ publishedBundleRoot: params.publishedBundleRoot,
651
+ }), params.sourceBundleRoot, params.publishedBundleRoot);
652
+ let terminalId = null;
653
+ let runInserted = false;
654
+ try {
655
+ const terminal = await this.deps.terminalBroker.createTerminal(params.agentNodeId, {
656
+ workspaceRoot: params.workspaceRoot,
657
+ cwd,
658
+ name: `${params.toolRow.name}: ${params.action.label}`,
659
+ startupCommand: command,
660
+ bufferOutputUntilTaken: true,
661
+ });
662
+ terminalId = terminal.terminalId;
663
+ this.insertRun({ ...params.run, terminalId });
664
+ runInserted = true;
665
+ this.deps.db.prepare(`UPDATE workspace_tools
666
+ SET active_terminal_id = ?,
667
+ updated_at = ?
668
+ WHERE tool_id = ?`).run(terminalId, Date.now(), params.toolRow.toolId);
669
+ this.latestStateProjector.syncStatus(params.toolRow.toolId, 'running');
670
+ this.terminalLifecycle.setContext(terminalId, {
671
+ toolId: params.toolRow.toolId,
672
+ runId: params.run.runId,
673
+ userId: params.toolRow.userId,
674
+ agentId: params.toolRow.agentId,
675
+ agentNodeId: params.agentNodeId,
676
+ maintenanceConversationId: params.toolRow.maintenanceConversationId,
677
+ manifest: params.manifest,
678
+ lineBuffer: '',
679
+ idleTimeoutSeconds: params.action.idleTimeoutSeconds,
680
+ });
681
+ this.schedulePersistentRunTimeout({
682
+ toolId: params.toolRow.toolId,
683
+ agentNodeId: params.agentNodeId,
684
+ terminalId,
685
+ maxRunSeconds: params.action.maxRunSeconds,
686
+ });
687
+ const startedCtx = this.terminalLifecycle.getContext(terminalId);
688
+ if (startedCtx) {
689
+ this.terminalLifecycle.resetPersistentIdleTimeout(terminalId);
690
+ }
691
+ const realtimeCtx = this.terminalLifecycle.getContext(terminalId);
692
+ if (realtimeCtx) {
693
+ if (params.manifest.cli?.entry && !params.action.command) {
694
+ const shouldScheduleStartupFallback = await this.capturePersistentStartupOutput({
695
+ terminalId,
696
+ agentNodeId: params.agentNodeId,
697
+ runId: params.run.runId,
698
+ });
699
+ if (shouldScheduleStartupFallback) {
700
+ this.schedulePersistentStartupSnapshotFallback({
701
+ terminalId,
702
+ agentNodeId: params.agentNodeId,
703
+ runId: params.run.runId,
704
+ });
705
+ }
706
+ }
707
+ else {
708
+ const earlyOutput = this.deps.terminalBroker.takeBufferedOutput(terminalId);
709
+ if (earlyOutput) {
710
+ this.terminalLifecycle.resetPersistentIdleTimeout(terminalId);
711
+ this.processRealtimeStateChunk(realtimeCtx, terminalId, earlyOutput);
712
+ }
713
+ }
714
+ }
715
+ }
716
+ catch (error) {
717
+ if (runInserted) {
718
+ this.completeRun(params.run.runId, {
719
+ status: 'failed',
720
+ endedAt: Date.now(),
721
+ });
722
+ }
723
+ if (terminalId) {
724
+ try {
725
+ this.deps.db.prepare(`UPDATE workspace_tools
726
+ SET active_terminal_id = NULL,
727
+ updated_at = ?
728
+ WHERE tool_id = ?
729
+ AND active_terminal_id = ?`).run(Date.now(), params.toolRow.toolId, terminalId);
730
+ }
731
+ catch {
732
+ // Preserve the startup failure while still closing the leaked terminal.
733
+ }
734
+ this.terminalLifecycle.deleteContext(terminalId);
735
+ this.clearPendingRealtimeStateForTool(params.toolRow.toolId);
736
+ this.clearPersistentRunTimeout(terminalId);
737
+ this.clearPersistentIdleTimeout(terminalId);
738
+ this.clearPersistentStartupCaptureTimeout(terminalId);
739
+ this.clearPersistentStartupOutputObserved(params.run.runId);
740
+ this.clearIgnoredRealtimeStartupLines(terminalId);
741
+ await closeTerminalBestEffort(this.deps.terminalBroker, params.agentNodeId, terminalId);
742
+ }
743
+ throw workspaceToolErrorWithRunId(error, params.run.runId);
744
+ }
745
+ if (!terminalId) {
746
+ throw new WorkspaceToolServiceError('Persistent tool terminal was not created.', 500);
747
+ }
748
+ const didRefreshLatestState = await this.refreshLatestStateAfterPersistentStart({
749
+ runId: params.run.runId,
750
+ toolRow: params.toolRow,
751
+ manifest: params.manifest,
752
+ agentNodeId: params.agentNodeId,
753
+ workspaceRoot: params.workspaceRoot,
754
+ sourceBundleRoot: params.sourceBundleRoot,
755
+ publishedBundleRoot: params.publishedBundleRoot,
756
+ });
757
+ const tool = this.deps.getToolInfoForActionViewer(params.viewerUserId, params.toolRow.toolId, params.allowAdminBypass);
758
+ const run = this.requireRun(params.run.runId, params.action.kind, params.viewerUserId, params.toolRow.userId);
759
+ await this.deps.recordPlatformExecNotification({
760
+ toolRow: params.toolRow,
761
+ action: params.action,
762
+ run,
763
+ viewerUserId: params.viewerUserId,
764
+ actionParams: params.actionParams,
765
+ outputText: null,
766
+ });
767
+ return {
768
+ mode: 'platform_exec',
769
+ tool,
770
+ run,
771
+ latestState: tool.latestState ?? null,
772
+ outputText: null,
773
+ };
774
+ }
775
+ async capturePersistentStartupOutput(params) {
776
+ const drainBufferedOutput = () => {
777
+ const ctx = this.getActivePersistentStartupContext(params.terminalId, params.runId);
778
+ if (!ctx)
779
+ return null;
780
+ const output = this.deps.terminalBroker.takeBufferedOutput(params.terminalId);
781
+ if (!output)
782
+ return ctx;
783
+ this.terminalLifecycle.resetPersistentIdleTimeout(params.terminalId);
784
+ this.processRealtimeStateChunk(ctx, params.terminalId, output);
785
+ return ctx;
786
+ };
787
+ let ctx = drainBufferedOutput();
788
+ if (!ctx)
789
+ return false;
790
+ const deadline = Date.now() + PERSISTENT_STARTUP_OUTPUT_SETTLE_MS;
791
+ while (!this.hasObservedPersistentStartupOutput(ctx, params.runId) && Date.now() < deadline) {
792
+ await sleep(PERSISTENT_STARTUP_OUTPUT_POLL_MS);
793
+ ctx = drainBufferedOutput();
794
+ if (!ctx)
795
+ return false;
796
+ }
797
+ if (this.hasObservedPersistentStartupOutput(ctx, params.runId)) {
798
+ return false;
799
+ }
800
+ const snapshot = await this.captureTerminalSnapshotBestEffort(params.agentNodeId, params.terminalId);
801
+ if (!snapshot)
802
+ return true;
803
+ if (snapshot.exited)
804
+ return false;
805
+ if (!snapshot.buffer)
806
+ return true;
807
+ ctx = this.getActivePersistentStartupContext(params.terminalId, params.runId);
808
+ if (!ctx || this.hasObservedPersistentStartupOutput(ctx, params.runId)) {
809
+ return false;
810
+ }
811
+ this.terminalLifecycle.resetPersistentIdleTimeout(params.terminalId);
812
+ this.processRealtimeStateSnapshotFallback(ctx, params.terminalId, snapshot.buffer, {
813
+ allowStateUpdate: !this.isPersistentStartupStateReplaySuppressed(params.runId),
814
+ });
815
+ this.flushPendingRealtimeStateForTool(ctx.toolId);
816
+ return !this.hasObservedPersistentStartupOutput(ctx, params.runId);
817
+ }
818
+ schedulePersistentStartupSnapshotFallback(params) {
819
+ if (this.hasObservedPersistentStartupOutputForTerminal(params.terminalId, params.runId)) {
820
+ return;
821
+ }
822
+ this.terminalLifecycle.schedulePersistentStartupSnapshotFallback({
823
+ terminalId: params.terminalId,
824
+ delayMs: PERSISTENT_STARTUP_SNAPSHOT_FALLBACK_MS,
825
+ onFallback: () => this.capturePersistentStartupSnapshotFallback(params),
826
+ });
827
+ }
828
+ async capturePersistentStartupSnapshotFallback(params) {
829
+ const ctx = this.getActivePersistentStartupContext(params.terminalId, params.runId);
830
+ if (!ctx || this.hasObservedPersistentStartupOutput(ctx, params.runId)) {
831
+ return;
832
+ }
833
+ const snapshot = await this.captureTerminalSnapshotBestEffort(params.agentNodeId, params.terminalId);
834
+ if (!snapshot?.buffer || snapshot.exited)
835
+ return;
836
+ const latestCtx = this.getActivePersistentStartupContext(params.terminalId, params.runId);
837
+ if (!latestCtx || this.hasObservedPersistentStartupOutput(latestCtx, params.runId)) {
838
+ return;
839
+ }
840
+ this.terminalLifecycle.resetPersistentIdleTimeout(params.terminalId);
841
+ this.processRealtimeStateSnapshotFallback(latestCtx, params.terminalId, snapshot.buffer, {
842
+ allowStateUpdate: !this.isPersistentStartupStateReplaySuppressed(params.runId),
843
+ });
844
+ this.flushPendingRealtimeStateForTool(latestCtx.toolId);
845
+ }
846
+ processRealtimeStateSnapshotFallback(ctx, terminalId, outputText, options = {}) {
847
+ for (const line of outputText.split('\n')) {
848
+ const candidate = extractWorkspaceToolJsonLineCandidate(line);
849
+ if (!candidate)
850
+ continue;
851
+ const accepted = this.processRealtimeStateLine(ctx, terminalId, candidate, {
852
+ consumeIgnoredRealtimeLine: false,
853
+ allowStateUpdate: options.allowStateUpdate,
854
+ });
855
+ if (accepted) {
856
+ this.recordIgnoredRealtimeStartupLine(terminalId, candidate);
857
+ }
858
+ }
859
+ }
860
+ prepareRealtimeChunkAfterSnapshotFallback(ctx, terminalId, chunk) {
861
+ if (!ctx.lineBuffer || !chunk)
862
+ return chunk;
863
+ const ignored = this.ignoredRealtimeStartupLines.get(terminalId);
864
+ if (!ignored?.size)
865
+ return chunk;
866
+ const buffered = ctx.lineBuffer;
867
+ const bufferedTrimmed = buffered.trimStart();
868
+ const bufferedJsonFragment = extractWorkspaceToolJsonFragment(buffered);
869
+ const chunkJsonFragment = extractWorkspaceToolJsonFragment(chunk);
870
+ for (const line of ignored.keys()) {
871
+ const prefix = line.startsWith(buffered)
872
+ ? buffered
873
+ : bufferedTrimmed.length > 0 && line.startsWith(bufferedTrimmed)
874
+ ? bufferedTrimmed
875
+ : bufferedJsonFragment && line.startsWith(bufferedJsonFragment)
876
+ ? bufferedJsonFragment
877
+ : null;
878
+ if (prefix == null)
879
+ continue;
880
+ const tail = line.slice(prefix.length);
881
+ if (tail && chunk.startsWith(tail)) {
882
+ this.consumeIgnoredRealtimeStartupLine(terminalId, line);
883
+ ctx.lineBuffer = '';
884
+ this.scheduleIgnoredRealtimeStartupLinesExpiryIfIdle(ctx, terminalId);
885
+ return chunk.slice(tail.length);
886
+ }
887
+ if (chunkJsonFragment) {
888
+ this.consumeIgnoredRealtimeStartupLine(terminalId, line);
889
+ ctx.lineBuffer = '';
890
+ this.scheduleIgnoredRealtimeStartupLinesExpiryIfIdle(ctx, terminalId);
891
+ return chunk;
892
+ }
893
+ }
894
+ return chunk;
895
+ }
896
+ recordIgnoredRealtimeStartupLine(terminalId, line) {
897
+ const trimmed = line.trim();
898
+ if (!trimmed)
899
+ return;
900
+ const existing = this.ignoredRealtimeStartupLines.get(terminalId) ?? new Map();
901
+ existing.set(trimmed, (existing.get(trimmed) ?? 0) + 1);
902
+ this.ignoredRealtimeStartupLines.set(terminalId, existing);
903
+ this.scheduleIgnoredRealtimeStartupLinesExpiryIfIdle(this.terminalLifecycle.getContext(terminalId) ?? null, terminalId);
904
+ }
905
+ consumeIgnoredRealtimeStartupLine(terminalId, line) {
906
+ const lines = this.ignoredRealtimeStartupLines.get(terminalId);
907
+ const count = lines?.get(line) ?? 0;
908
+ if (!lines || count <= 0)
909
+ return false;
910
+ if (count === 1) {
911
+ lines.delete(line);
912
+ }
913
+ else {
914
+ lines.set(line, count - 1);
915
+ }
916
+ if (lines.size === 0) {
917
+ this.ignoredRealtimeStartupLines.delete(terminalId);
918
+ this.clearIgnoredRealtimeStartupLinesExpiry(terminalId);
919
+ }
920
+ return true;
921
+ }
922
+ hasObservedPersistentStartupOutputForTerminal(terminalId, runId) {
923
+ const ctx = this.terminalLifecycle.getContext(terminalId);
924
+ return ctx ? this.hasObservedPersistentStartupOutput(ctx, runId) : true;
925
+ }
926
+ suppressPersistentStartupStateReplay(runId) {
927
+ this.startupStateReplaySuppressedRunIds.add(runId);
928
+ }
929
+ isPersistentStartupStateReplaySuppressed(runId) {
930
+ return this.startupStateReplaySuppressedRunIds.has(runId);
931
+ }
932
+ scheduleIgnoredRealtimeStartupLinesExpiryIfIdle(ctx, terminalId) {
933
+ if (ctx?.lineBuffer)
934
+ return;
935
+ this.clearIgnoredRealtimeStartupLinesExpiry(terminalId);
936
+ const timer = setTimeout(() => {
937
+ this.ignoredRealtimeStartupLineTimeouts.delete(terminalId);
938
+ this.ignoredRealtimeStartupLines.delete(terminalId);
939
+ }, PERSISTENT_STARTUP_DUPLICATE_IGNORE_IDLE_MS);
940
+ timer.unref?.();
941
+ this.ignoredRealtimeStartupLineTimeouts.set(terminalId, timer);
942
+ }
943
+ clearIgnoredRealtimeStartupLines(terminalId) {
944
+ this.clearIgnoredRealtimeStartupLinesExpiry(terminalId);
945
+ this.ignoredRealtimeStartupLines.delete(terminalId);
946
+ }
947
+ clearIgnoredRealtimeStartupLinesExpiry(terminalId) {
948
+ const handle = this.ignoredRealtimeStartupLineTimeouts.get(terminalId);
949
+ if (handle) {
950
+ clearTimeout(handle);
951
+ this.ignoredRealtimeStartupLineTimeouts.delete(terminalId);
952
+ }
953
+ }
954
+ getActivePersistentStartupContext(terminalId, runId) {
955
+ if (this.terminalLifecycle.isTerminating(terminalId))
956
+ return null;
957
+ const ctx = this.terminalLifecycle.getContext(terminalId);
958
+ return ctx?.runId === runId ? ctx : null;
959
+ }
960
+ async runPersistentRuntimeAction(params) {
961
+ const currentActiveTerminalId = this.loadActiveTerminalId(params.toolRow.toolId);
962
+ if (params.action.kind === 'stop') {
963
+ if (!currentActiveTerminalId) {
964
+ throw new WorkspaceToolServiceError('Tool runtime is not running.', 409);
965
+ }
966
+ return this.stopPersistentRuntimeAction({ ...params, terminalId: currentActiveTerminalId });
967
+ }
968
+ let terminalId = currentActiveTerminalId;
969
+ let startedRuntime = false;
970
+ if (!terminalId) {
971
+ terminalId = await this.deps.enqueueAgentPersistentStart(params.toolRow.agentId, async () => {
972
+ const latestActiveTerminalId = this.loadActiveTerminalId(params.toolRow.toolId);
973
+ if (latestActiveTerminalId)
974
+ return latestActiveTerminalId;
975
+ this.terminalLifecycle.assertAgentPersistentRunQuota(params.toolRow.agentId);
976
+ return this.startPersistentRuntimeTerminal(params);
977
+ });
978
+ startedRuntime = true;
979
+ }
980
+ const ctx = this.terminalLifecycle.getContext(terminalId);
981
+ if (ctx) {
982
+ ctx.runId = null;
983
+ ctx.idleTimeoutSeconds = undefined;
984
+ ctx.lineBuffer = '';
985
+ this.clearPersistentIdleTimeout(terminalId);
986
+ }
987
+ const logDir = this.jsonlClient.normalizeLogDir(params.toolRow.toolId, params.manifest);
988
+ const logPath = this.jsonlClient.buildRunLogPath(logDir, params.run.runId);
989
+ this.insertRun({ ...params.run, terminalId });
990
+ try {
991
+ const request = this.jsonlClient.buildStdinRequest({
992
+ runId: params.run.runId,
993
+ actionId: params.action.actionId,
994
+ actionParams: params.actionParams,
995
+ toolId: params.toolRow.toolId,
996
+ revision: params.toolRow.revision,
997
+ });
998
+ try {
999
+ await this.deps.terminalBroker.sendInput(params.agentNodeId, terminalId, this.jsonlClient.stdinRequestToLine(request));
1000
+ }
1001
+ catch (error) {
1002
+ if (isMissingTerminalError(error)) {
1003
+ this.finishActivePersistentRun(params.toolRow.toolId, terminalId, { status: 'failed', signal: 'missing_terminal' });
1004
+ }
1005
+ throw error;
1006
+ }
1007
+ const completion = await this.jsonlClient.syncRunLog({
1008
+ runId: params.run.runId,
1009
+ toolId: params.toolRow.toolId,
1010
+ terminalId,
1011
+ agentNodeId: params.agentNodeId,
1012
+ workspaceRoot: params.workspaceRoot,
1013
+ logPath,
1014
+ actionId: params.action.actionId,
1015
+ manifest: params.manifest,
1016
+ timeoutMs: this.jsonlClient.actionTimeoutMs(params.action),
1017
+ onStateUpdate: (state) => this.latestStateProjector.updateLatestState({
1018
+ toolId: params.toolRow.toolId,
1019
+ userId: params.toolRow.userId,
1020
+ agentId: params.toolRow.agentId,
1021
+ maintenanceConversationId: params.toolRow.maintenanceConversationId,
1022
+ state,
1023
+ }),
1024
+ onRunEvent: (envelope) => this.runLedger.insertRunEventFromEnvelope(params.run.runId, params.toolRow.toolId, envelope),
1025
+ onTimeoutEvent: (timeoutPayload) => this.runLedger.insertPersistentRuntimeActionTimeoutRunEvent(params.run.runId, params.toolRow.toolId, timeoutPayload),
1026
+ });
1027
+ const endedAt = Date.now();
1028
+ this.completeRun(params.run.runId, {
1029
+ status: completion.status,
1030
+ endedAt,
1031
+ exitCode: completion.exitCode,
1032
+ signal: completion.signal,
1033
+ });
1034
+ this.resetPersistentRuntimeIdleTimeout(terminalId, params);
1035
+ }
1036
+ catch (error) {
1037
+ const timedOut = error instanceof WorkspaceToolServiceError && error.statusCode === 504;
1038
+ const missingTerminal = isMissingTerminalError(error);
1039
+ const completion = deriveTerminalRunCompletion(timedOut ? { kind: 'timeout' }
1040
+ : missingTerminal ? { kind: 'missing_terminal' }
1041
+ : { kind: 'error_event' });
1042
+ if (missingTerminal) {
1043
+ this.insertMissingTerminalRunEvent(params.run.runId, params.toolRow.toolId, {
1044
+ actionId: params.action.actionId,
1045
+ error: String(error?.message ?? error),
1046
+ });
1047
+ }
1048
+ this.completeRun(params.run.runId, {
1049
+ status: completion.status,
1050
+ endedAt: Date.now(),
1051
+ exitCode: completion.exitCode,
1052
+ signal: completion.signal,
1053
+ });
1054
+ if (completion.status === 'timed_out' || startedRuntime || missingTerminal) {
1055
+ await closeTerminalBestEffort(this.deps.terminalBroker, params.agentNodeId, terminalId);
1056
+ this.finishActivePersistentRun(params.toolRow.toolId, terminalId, completion);
1057
+ }
1058
+ else {
1059
+ this.resetPersistentRuntimeIdleTimeout(terminalId, params);
1060
+ }
1061
+ const failedRun = this.requireRun(params.run.runId, params.action.kind, params.viewerUserId, params.toolRow.userId);
1062
+ await this.deps.recordPlatformExecNotification({
1063
+ toolRow: params.toolRow,
1064
+ action: params.action,
1065
+ run: failedRun,
1066
+ viewerUserId: params.viewerUserId,
1067
+ actionParams: params.actionParams,
1068
+ outputText: null,
1069
+ });
1070
+ throw workspaceToolErrorWithRunId(error, params.run.runId);
1071
+ }
1072
+ const tool = this.getToolInfoForActionViewerAfterPendingRealtimeFlush({
1073
+ viewerUserId: params.viewerUserId,
1074
+ toolRow: params.toolRow,
1075
+ allowAdminBypass: params.allowAdminBypass,
1076
+ });
1077
+ const run = this.requireRun(params.run.runId, params.action.kind, params.viewerUserId, params.toolRow.userId);
1078
+ await this.deps.recordPlatformExecNotification({
1079
+ toolRow: params.toolRow,
1080
+ action: params.action,
1081
+ run,
1082
+ viewerUserId: params.viewerUserId,
1083
+ actionParams: params.actionParams,
1084
+ outputText: null,
1085
+ });
1086
+ return {
1087
+ mode: 'platform_exec',
1088
+ tool,
1089
+ run,
1090
+ latestState: tool.latestState ?? null,
1091
+ outputText: null,
1092
+ };
1093
+ }
1094
+ async stopPersistentRuntimeAction(params) {
1095
+ this.insertRun({ ...params.run, terminalId: params.terminalId });
1096
+ this.insertCleanupRunEvent(params.run.runId, params.toolRow.toolId, 'cleanup_started', {
1097
+ summary: 'Runtime stop requested',
1098
+ actionId: params.action.actionId,
1099
+ });
1100
+ let finalStatus = 'completed';
1101
+ let finalExitCode = 0;
1102
+ let finalSignal = null;
1103
+ let actionError = null;
1104
+ const shouldRequestRuntimeStop = typeof params.action.maxRunSeconds === 'number';
1105
+ if (shouldRequestRuntimeStop) {
1106
+ const logDir = this.jsonlClient.normalizeLogDir(params.toolRow.toolId, params.manifest);
1107
+ const logPath = this.jsonlClient.buildRunLogPath(logDir, params.run.runId);
1108
+ try {
1109
+ const request = this.jsonlClient.buildStdinRequest({
1110
+ runId: params.run.runId,
1111
+ actionId: params.action.actionId,
1112
+ actionParams: params.actionParams,
1113
+ toolId: params.toolRow.toolId,
1114
+ revision: params.toolRow.revision,
1115
+ });
1116
+ await this.deps.terminalBroker.sendInput(params.agentNodeId, params.terminalId, this.jsonlClient.stdinRequestToLine(request));
1117
+ const completion = await this.jsonlClient.syncRunLog({
1118
+ runId: params.run.runId,
1119
+ toolId: params.toolRow.toolId,
1120
+ terminalId: params.terminalId,
1121
+ agentNodeId: params.agentNodeId,
1122
+ workspaceRoot: params.workspaceRoot,
1123
+ logPath,
1124
+ actionId: params.action.actionId,
1125
+ manifest: params.manifest,
1126
+ timeoutMs: this.jsonlClient.actionTimeoutMs(params.action),
1127
+ onStateUpdate: (state) => this.latestStateProjector.updateLatestState({
1128
+ toolId: params.toolRow.toolId,
1129
+ userId: params.toolRow.userId,
1130
+ agentId: params.toolRow.agentId,
1131
+ maintenanceConversationId: params.toolRow.maintenanceConversationId,
1132
+ state,
1133
+ }),
1134
+ onRunEvent: (envelope) => this.runLedger.insertRunEventFromEnvelope(params.run.runId, params.toolRow.toolId, envelope),
1135
+ onTimeoutEvent: (timeoutPayload) => this.runLedger.insertPersistentRuntimeActionTimeoutRunEvent(params.run.runId, params.toolRow.toolId, timeoutPayload),
1136
+ });
1137
+ finalStatus = completion.status;
1138
+ finalExitCode = completion.exitCode;
1139
+ finalSignal = completion.signal;
1140
+ if (completion.status === 'failed') {
1141
+ actionError = new WorkspaceToolServiceError('Persistent runtime stop action reported an error event.', 500, {
1142
+ runId: params.run.runId,
1143
+ });
1144
+ }
1145
+ }
1146
+ catch (error) {
1147
+ const timedOut = error instanceof WorkspaceToolServiceError && error.statusCode === 504;
1148
+ const missingTerminal = isMissingTerminalError(error);
1149
+ const completion = deriveTerminalRunCompletion(timedOut ? { kind: 'timeout' }
1150
+ : missingTerminal ? { kind: 'missing_terminal' }
1151
+ : { kind: 'error_event' });
1152
+ finalStatus = completion.status;
1153
+ finalExitCode = completion.exitCode;
1154
+ finalSignal = completion.signal;
1155
+ if (missingTerminal) {
1156
+ this.insertMissingTerminalRunEvent(params.run.runId, params.toolRow.toolId, {
1157
+ actionId: params.action.actionId,
1158
+ error: String(error?.message ?? error),
1159
+ });
1160
+ }
1161
+ actionError = error;
1162
+ }
1163
+ }
1164
+ try {
1165
+ await this.deps.terminalBroker.closeTerminal(params.agentNodeId, params.terminalId);
1166
+ this.finishActivePersistentRun(params.toolRow.toolId, params.terminalId, { status: 'cancelled' });
1167
+ }
1168
+ catch (error) {
1169
+ if (isMissingTerminalError(error)) {
1170
+ this.finishActivePersistentRun(params.toolRow.toolId, params.terminalId, { status: 'cancelled', signal: 'missing_terminal' });
1171
+ this.insertCleanupRunEvent(params.run.runId, params.toolRow.toolId, 'cleanup_completed', {
1172
+ summary: 'Runtime terminal was already gone',
1173
+ actionId: params.action.actionId,
1174
+ exitCode: null,
1175
+ signal: 'missing_terminal',
1176
+ });
1177
+ const finalEndedAt = Date.now();
1178
+ this.completeRun(params.run.runId, {
1179
+ status: finalStatus,
1180
+ endedAt: finalEndedAt,
1181
+ exitCode: finalExitCode,
1182
+ signal: finalSignal ?? 'missing_terminal',
1183
+ });
1184
+ const tool = this.getToolInfoForActionViewerAfterPendingRealtimeFlush({
1185
+ viewerUserId: params.viewerUserId,
1186
+ toolRow: params.toolRow,
1187
+ allowAdminBypass: params.allowAdminBypass,
1188
+ });
1189
+ const run = this.requireRun(params.run.runId, params.action.kind, params.viewerUserId, params.toolRow.userId);
1190
+ await this.deps.recordPlatformExecNotification({
1191
+ toolRow: params.toolRow,
1192
+ action: params.action,
1193
+ run,
1194
+ viewerUserId: params.viewerUserId,
1195
+ actionParams: params.actionParams,
1196
+ outputText: null,
1197
+ });
1198
+ if (actionError && finalStatus !== 'completed') {
1199
+ throw workspaceToolErrorWithRunId(actionError, params.run.runId);
1200
+ }
1201
+ return {
1202
+ mode: 'platform_exec',
1203
+ tool,
1204
+ run,
1205
+ latestState: tool.latestState ?? null,
1206
+ outputText: null,
1207
+ };
1208
+ }
1209
+ this.insertCleanupRunEvent(params.run.runId, params.toolRow.toolId, 'cleanup_failed', {
1210
+ summary: 'Runtime terminal close failed',
1211
+ actionId: params.action.actionId,
1212
+ error: String(error?.message ?? error),
1213
+ });
1214
+ const preserveActionFailure = Boolean(actionError && finalStatus !== 'completed');
1215
+ this.completeRun(params.run.runId, {
1216
+ status: preserveActionFailure ? finalStatus : 'failed',
1217
+ endedAt: Date.now(),
1218
+ exitCode: preserveActionFailure ? finalExitCode : null,
1219
+ signal: preserveActionFailure ? finalSignal : null,
1220
+ });
1221
+ throw workspaceToolErrorWithRunId(preserveActionFailure ? actionError : error, params.run.runId);
1222
+ }
1223
+ this.insertCleanupRunEvent(params.run.runId, params.toolRow.toolId, 'cleanup_completed', {
1224
+ summary: 'Runtime terminal closed',
1225
+ actionId: params.action.actionId,
1226
+ exitCode: finalExitCode,
1227
+ signal: finalSignal,
1228
+ });
1229
+ this.completeRun(params.run.runId, {
1230
+ status: finalStatus,
1231
+ endedAt: Date.now(),
1232
+ exitCode: finalExitCode,
1233
+ signal: finalSignal,
1234
+ });
1235
+ const tool = this.getToolInfoForActionViewerAfterPendingRealtimeFlush({
1236
+ viewerUserId: params.viewerUserId,
1237
+ toolRow: params.toolRow,
1238
+ allowAdminBypass: params.allowAdminBypass,
1239
+ });
1240
+ const run = this.requireRun(params.run.runId, params.action.kind, params.viewerUserId, params.toolRow.userId);
1241
+ await this.deps.recordPlatformExecNotification({
1242
+ toolRow: params.toolRow,
1243
+ action: params.action,
1244
+ run,
1245
+ viewerUserId: params.viewerUserId,
1246
+ actionParams: params.actionParams,
1247
+ outputText: null,
1248
+ });
1249
+ if (actionError && finalStatus !== 'completed') {
1250
+ throw workspaceToolErrorWithRunId(actionError, params.run.runId);
1251
+ }
1252
+ return {
1253
+ mode: 'platform_exec',
1254
+ tool,
1255
+ run,
1256
+ latestState: tool.latestState ?? null,
1257
+ outputText: null,
1258
+ };
1259
+ }
1260
+ async startPersistentRuntimeTerminal(params) {
1261
+ const logDir = this.jsonlClient.normalizeLogDir(params.toolRow.toolId, params.manifest);
1262
+ const command = rewriteWorkspaceToolCommandPaths(params.workspaceRoot, buildWorkspaceToolRuntimeCommand({
1263
+ manifest: params.manifest,
1264
+ workspaceRoot: params.workspaceRoot,
1265
+ publishedBundleRoot: params.publishedBundleRoot,
1266
+ toolId: params.toolRow.toolId,
1267
+ revision: params.toolRow.revision,
1268
+ logDir,
1269
+ }), params.sourceBundleRoot, params.publishedBundleRoot);
1270
+ if (!command.trim()) {
1271
+ throw new WorkspaceToolServiceError('Persistent tool runtime requires runtime.entry or cli.entry.', 400);
1272
+ }
1273
+ let terminalId = null;
1274
+ try {
1275
+ const terminal = await this.deps.terminalBroker.createTerminal(params.agentNodeId, {
1276
+ workspaceRoot: params.workspaceRoot,
1277
+ cwd: resolveToolPath(params.workspaceRoot, params.publishedBundleRoot),
1278
+ name: `${params.toolRow.name}: runtime`,
1279
+ startupCommand: command,
1280
+ });
1281
+ terminalId = terminal.terminalId;
1282
+ this.deps.db.prepare(`UPDATE workspace_tools
1283
+ SET active_terminal_id = ?,
1284
+ updated_at = ?
1285
+ WHERE tool_id = ?`).run(terminal.terminalId, Date.now(), params.toolRow.toolId);
1286
+ this.latestStateProjector.syncStatus(params.toolRow.toolId, 'running');
1287
+ this.terminalLifecycle.setContext(terminal.terminalId, {
1288
+ toolId: params.toolRow.toolId,
1289
+ runId: null,
1290
+ userId: params.toolRow.userId,
1291
+ agentId: params.toolRow.agentId,
1292
+ agentNodeId: params.agentNodeId,
1293
+ maintenanceConversationId: params.toolRow.maintenanceConversationId,
1294
+ manifest: params.manifest,
1295
+ lineBuffer: '',
1296
+ });
1297
+ return terminal.terminalId;
1298
+ }
1299
+ catch (error) {
1300
+ if (terminalId) {
1301
+ try {
1302
+ this.deps.db.prepare(`UPDATE workspace_tools
1303
+ SET active_terminal_id = NULL,
1304
+ updated_at = ?
1305
+ WHERE tool_id = ?
1306
+ AND active_terminal_id = ?`).run(Date.now(), params.toolRow.toolId, terminalId);
1307
+ }
1308
+ catch {
1309
+ // Preserve the startup failure while still closing the leaked terminal.
1310
+ }
1311
+ this.terminalLifecycle.deleteContext(terminalId);
1312
+ this.clearPersistentRunTimeout(terminalId);
1313
+ this.clearPersistentIdleTimeout(terminalId);
1314
+ this.clearPersistentStartupCaptureTimeout(terminalId);
1315
+ this.clearIgnoredRealtimeStartupLines(terminalId);
1316
+ await closeTerminalBestEffort(this.deps.terminalBroker, params.agentNodeId, terminalId);
1317
+ }
1318
+ throw error;
1319
+ }
1320
+ }
1321
+ resetPersistentRuntimeIdleTimeout(terminalId, params) {
1322
+ const ctx = this.terminalLifecycle.getContext(terminalId);
1323
+ if (!ctx)
1324
+ return;
1325
+ ctx.runId = null;
1326
+ ctx.idleTimeoutSeconds = params.manifest.runtime?.idleTtlSeconds ?? defaultPersistentRuntimeIdleTtlSeconds();
1327
+ this.terminalLifecycle.resetPersistentIdleTimeout(terminalId);
1328
+ }
1329
+ loadActiveTerminalId(toolId) {
1330
+ const row = this.deps.db.prepare(`SELECT active_terminal_id as activeTerminalId
1331
+ FROM workspace_tools
1332
+ WHERE tool_id = ?
1333
+ AND deleted_at IS NULL`).get(toolId);
1334
+ return row?.activeTerminalId ?? null;
1335
+ }
1336
+ hasObservedPersistentStartupOutput(ctx, runId) {
1337
+ if (this.observedPersistentStartupOutput.has(runId))
1338
+ return true;
1339
+ if (this.latestStateProjector.hasPendingRealtimeStateUpdate(ctx.toolId))
1340
+ return true;
1341
+ const row = this.deps.db.prepare(`SELECT 1
1342
+ FROM workspace_tool_run_events
1343
+ WHERE run_id = ?
1344
+ LIMIT 1`).get(runId);
1345
+ return Boolean(row);
1346
+ }
1347
+ async runOneShotAction(params) {
1348
+ const cwd = resolveToolCwd(params.workspaceRoot, params.manifest, params.action, params.sourceBundleRoot, params.publishedBundleRoot);
1349
+ const command = rewriteWorkspaceToolCommandPaths(params.workspaceRoot, buildWorkspaceToolStartupCommand({
1350
+ manifest: params.manifest,
1351
+ action: params.action,
1352
+ actionParams: params.actionParams,
1353
+ workspaceRoot: params.workspaceRoot,
1354
+ publishedBundleRoot: params.publishedBundleRoot,
1355
+ }), params.sourceBundleRoot, params.publishedBundleRoot);
1356
+ let terminalId = null;
1357
+ try {
1358
+ const terminal = await this.deps.terminalBroker.createTerminal(params.agentNodeId, {
1359
+ workspaceRoot: params.workspaceRoot,
1360
+ cwd,
1361
+ name: `${params.toolRow.name}: ${params.action.label}`,
1362
+ startupCommand: command,
1363
+ closeOnStartupCommand: true,
1364
+ });
1365
+ terminalId = terminal.terminalId;
1366
+ this.insertRun({ ...params.run, terminalId });
1367
+ }
1368
+ catch (error) {
1369
+ if (terminalId) {
1370
+ await closeTerminalBestEffort(this.deps.terminalBroker, params.agentNodeId, terminalId);
1371
+ }
1372
+ throw error;
1373
+ }
1374
+ if (!terminalId) {
1375
+ throw new WorkspaceToolServiceError('Tool action terminal was not created.', 500);
1376
+ }
1377
+ let outputText = '';
1378
+ let latestObservedOutputText = '';
1379
+ let completion = null;
1380
+ try {
1381
+ const snapshot = await waitForTerminalExit(this.deps.terminalBroker, params.agentNodeId, terminalId, {
1382
+ onSnapshot: (current) => {
1383
+ latestObservedOutputText = current.buffer ?? '';
1384
+ },
1385
+ });
1386
+ const endedAt = Date.now();
1387
+ outputText = snapshot.buffer ?? '';
1388
+ completion = deriveOneShotRunCompletion(snapshot);
1389
+ this.recordRunEventsFromOutput(params.run.runId, params.toolRow.toolId, outputText);
1390
+ if (params.action.kind === 'status') {
1391
+ const parsedState = parseStateRowFromOutput(outputText, true);
1392
+ if (!parsedState) {
1393
+ throw new WorkspaceToolServiceError('Status action output must be a single JSON object.', 400);
1394
+ }
1395
+ validateStateRowForManifest(params.manifest, parsedState);
1396
+ this.latestStateProjector.updateLatestState({
1397
+ toolId: params.toolRow.toolId,
1398
+ userId: params.toolRow.userId,
1399
+ agentId: params.toolRow.agentId,
1400
+ maintenanceConversationId: params.toolRow.maintenanceConversationId,
1401
+ state: parsedState,
1402
+ });
1403
+ }
1404
+ else {
1405
+ const parsedState = parseStateRowFromOutput(outputText, false);
1406
+ if (parsedState) {
1407
+ validateStateRowForManifest(params.manifest, parsedState);
1408
+ this.latestStateProjector.updateLatestState({
1409
+ toolId: params.toolRow.toolId,
1410
+ userId: params.toolRow.userId,
1411
+ agentId: params.toolRow.agentId,
1412
+ maintenanceConversationId: params.toolRow.maintenanceConversationId,
1413
+ state: parsedState,
1414
+ });
1415
+ }
1416
+ }
1417
+ this.completeRun(params.run.runId, {
1418
+ status: completion.status,
1419
+ endedAt,
1420
+ exitCode: completion.exitCode,
1421
+ signal: completion.signal,
1422
+ });
1423
+ }
1424
+ catch (error) {
1425
+ if (!outputText) {
1426
+ outputText = latestObservedOutputText || await this.captureTerminalOutputBestEffort(params.agentNodeId, terminalId);
1427
+ this.recordRunEventsFromOutput(params.run.runId, params.toolRow.toolId, outputText);
1428
+ }
1429
+ const failedEndedAt = Date.now();
1430
+ const failedExitCode = completion?.exitCode ?? null;
1431
+ const failedSignal = completion?.signal ?? null;
1432
+ this.completeRun(params.run.runId, {
1433
+ status: 'failed',
1434
+ endedAt: failedEndedAt,
1435
+ exitCode: failedExitCode,
1436
+ signal: failedSignal,
1437
+ });
1438
+ const failedRun = this.runInfoForActionViewer({
1439
+ ...params.run,
1440
+ terminalId,
1441
+ status: 'failed',
1442
+ endedAt: failedEndedAt,
1443
+ exitCode: failedExitCode,
1444
+ signal: failedSignal,
1445
+ }, params.action.kind, params.viewerUserId, params.toolRow.userId);
1446
+ await this.deps.recordPlatformExecNotification({
1447
+ toolRow: params.toolRow,
1448
+ action: params.action,
1449
+ run: failedRun,
1450
+ viewerUserId: params.viewerUserId,
1451
+ actionParams: params.actionParams,
1452
+ outputText,
1453
+ });
1454
+ throw workspaceToolErrorWithRunId(error, params.run.runId);
1455
+ }
1456
+ finally {
1457
+ if (terminalId) {
1458
+ await closeTerminalBestEffort(this.deps.terminalBroker, params.agentNodeId, terminalId);
1459
+ }
1460
+ }
1461
+ if (params.action.kind !== 'status') {
1462
+ await this.refreshLatestStateFromStatusActionSilently({
1463
+ toolRow: params.toolRow,
1464
+ manifest: params.manifest,
1465
+ agentNodeId: params.agentNodeId,
1466
+ workspaceRoot: params.workspaceRoot,
1467
+ sourceBundleRoot: params.sourceBundleRoot,
1468
+ publishedBundleRoot: params.publishedBundleRoot,
1469
+ });
1470
+ }
1471
+ const tool = this.deps.getToolInfoForActionViewer(params.viewerUserId, params.toolRow.toolId, params.allowAdminBypass);
1472
+ const run = this.requireRun(params.run.runId, params.action.kind, params.viewerUserId, params.toolRow.userId);
1473
+ await this.deps.recordPlatformExecNotification({
1474
+ toolRow: params.toolRow,
1475
+ action: params.action,
1476
+ run,
1477
+ viewerUserId: params.viewerUserId,
1478
+ actionParams: params.actionParams,
1479
+ outputText,
1480
+ });
1481
+ return {
1482
+ mode: 'platform_exec',
1483
+ tool,
1484
+ run,
1485
+ latestState: tool.latestState ?? null,
1486
+ outputText,
1487
+ };
1488
+ }
1489
+ insertRun(run) {
1490
+ this.runLedger.insertRun(run);
1491
+ }
1492
+ runInfoForActionViewer(row, actionKind, viewerUserId, ownerUserId) {
1493
+ return this.runLedger.runInfoForActionViewer(row, actionKind, viewerUserId, ownerUserId);
1494
+ }
1495
+ recordRunEventsFromOutput(runId, toolId, outputText) {
1496
+ this.runLedger.recordRunEventsFromOutput(runId, toolId, outputText);
1497
+ }
1498
+ async captureTerminalOutputBestEffort(agentNodeId, terminalId) {
1499
+ const snapshot = await this.captureTerminalSnapshotBestEffort(agentNodeId, terminalId);
1500
+ return snapshot?.buffer ?? '';
1501
+ }
1502
+ async captureTerminalSnapshotBestEffort(agentNodeId, terminalId) {
1503
+ try {
1504
+ const snapshot = await this.deps.terminalBroker.snapshotTerminal(agentNodeId, terminalId);
1505
+ return {
1506
+ buffer: snapshot.buffer ?? '',
1507
+ exited: snapshot.terminal.exited,
1508
+ };
1509
+ }
1510
+ catch {
1511
+ return null;
1512
+ }
1513
+ }
1514
+ insertRunEventFromEnvelope(runId, toolId, value) {
1515
+ return this.runLedger.insertRunEventFromEnvelope(runId, toolId, value);
1516
+ }
1517
+ completeRun(runId, params) {
1518
+ this.runLedger.completeRun(runId, params);
1519
+ }
1520
+ requireRun(runId, actionKind, viewerUserId, ownerUserId) {
1521
+ return this.runLedger.requireRun(runId, actionKind, viewerUserId, ownerUserId);
1522
+ }
1523
+ finishActivePersistentRun(toolId, terminalId, completion) {
1524
+ return this.terminalLifecycle.finishActivePersistentRun(toolId, terminalId, completion);
1525
+ }
1526
+ reattachActivePersistentTerminal(row, nodeId) {
1527
+ return this.terminalLifecycle.reattachActivePersistentTerminal(row, nodeId);
1528
+ }
1529
+ async handleTerminalTermination(event) {
1530
+ return this.terminalLifecycle.handleTerminalTermination(event);
1531
+ }
1532
+ schedulePersistentRunTimeout(params) {
1533
+ this.terminalLifecycle.schedulePersistentRunTimeout(params);
1534
+ }
1535
+ schedulePersistentIdleTimeout(params) {
1536
+ this.terminalLifecycle.schedulePersistentIdleTimeout(params);
1537
+ }
1538
+ clearPersistentRunTimeout(terminalId) {
1539
+ this.terminalLifecycle.clearPersistentRunTimeout(terminalId);
1540
+ }
1541
+ clearPersistentIdleTimeout(terminalId) {
1542
+ this.terminalLifecycle.clearPersistentIdleTimeout(terminalId);
1543
+ }
1544
+ clearPersistentStartupCaptureTimeout(terminalId) {
1545
+ this.terminalLifecycle.clearPersistentStartupCaptureTimeout(terminalId);
1546
+ }
1547
+ markPersistentStartupOutputObserved(runId) {
1548
+ this.observedPersistentStartupOutput.add(runId);
1549
+ }
1550
+ clearPersistentStartupOutputObserved(runId) {
1551
+ if (runId) {
1552
+ this.observedPersistentStartupOutput.delete(runId);
1553
+ this.startupStateReplaySuppressedRunIds.delete(runId);
1554
+ }
1555
+ }
1556
+ }
1557
+ function extractWorkspaceToolJsonLineCandidate(line) {
1558
+ const trimmed = line.trim();
1559
+ if (!trimmed)
1560
+ return null;
1561
+ for (let index = trimmed.indexOf('{'); index >= 0; index = trimmed.indexOf('{', index + 1)) {
1562
+ const candidate = trimmed.slice(index);
1563
+ try {
1564
+ JSON.parse(candidate);
1565
+ return candidate;
1566
+ }
1567
+ catch {
1568
+ // Keep scanning; prompts or shell echoes can contain braces before the JSON object.
1569
+ }
1570
+ }
1571
+ return null;
1572
+ }
1573
+ function extractWorkspaceToolJsonFragment(line) {
1574
+ const trimmed = line.trim();
1575
+ if (!trimmed)
1576
+ return null;
1577
+ const quotedObjectIndex = trimmed.indexOf('{"');
1578
+ if (quotedObjectIndex >= 0) {
1579
+ return trimmed.slice(quotedObjectIndex);
1580
+ }
1581
+ return trimmed.startsWith('{') ? trimmed : null;
1582
+ }