@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,1130 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { log, resolveWorkspacePath, } from '@bbigbang/runtime-acp';
4
+ import { listRuntimeDrivers, } from '@bbigbang/protocol';
5
+ import { ensureLocalWorkspaceScaffold } from './workspaceScaffold.js';
6
+ const DEFAULT_SCENARIO_NODE_ID = 'scenario-node';
7
+ const DEFAULT_SCENARIO_HOSTNAME = 'scenario-node.local';
8
+ const DEFAULT_SCENARIO_VERSION = 'scenario-runtime@phase1';
9
+ const DEFAULT_SCENARIO_ID = 'default';
10
+ const DEFAULT_CHANNEL_SPECS = [
11
+ { name: 'scenario-main', description: 'Scenario main channel', collaborationMode: 'mention_only' },
12
+ { name: 'scenario-review', description: 'Scenario review channel', collaborationMode: 'mention_only' },
13
+ ];
14
+ const DEFAULT_AGENT_SPECS = [
15
+ { name: 'ScenarioAlice', agentType: 'codex_app_server', homeChannel: 'scenario-main' },
16
+ { name: 'ScenarioBob', agentType: 'codex_app_server', homeChannel: 'scenario-main' },
17
+ { name: 'ScenarioCarol', agentType: 'codex_app_server', homeChannel: 'scenario-review' },
18
+ ];
19
+ function normalizeScenarioLibraryRelativePath(rawPath) {
20
+ const trimmed = rawPath.trim().replace(/\\/g, '/');
21
+ const normalized = path.posix.normalize(trimmed).replace(/^\/+/, '').replace(/\/+$/, '');
22
+ if (!normalized
23
+ || normalized === '.'
24
+ || normalized === '..'
25
+ || normalized.startsWith('../')
26
+ || normalized.includes('/../')) {
27
+ throw new Error('Invalid library path');
28
+ }
29
+ return normalized;
30
+ }
31
+ export class ScenarioRuntimeService {
32
+ db;
33
+ conversationManager;
34
+ nodeRegistry;
35
+ workspaceBroker;
36
+ inspectBroker;
37
+ workspaceRoot;
38
+ internalAgentAuthToken;
39
+ scripts = new Map();
40
+ activeRuns = new Set();
41
+ app = null;
42
+ nodeId = DEFAULT_SCENARIO_NODE_ID;
43
+ hostname = DEFAULT_SCENARIO_HOSTNAME;
44
+ version = DEFAULT_SCENARIO_VERSION;
45
+ constructor(params) {
46
+ this.db = params.db;
47
+ this.conversationManager = params.conversationManager;
48
+ this.nodeRegistry = params.nodeRegistry;
49
+ this.workspaceBroker = params.workspaceBroker;
50
+ this.inspectBroker = params.inspectBroker;
51
+ this.workspaceRoot = params.workspaceRoot;
52
+ this.internalAgentAuthToken = params.internalAgentAuthToken ?? null;
53
+ this.ensureRegisteredNode();
54
+ }
55
+ bindApp(app) {
56
+ this.app = app;
57
+ }
58
+ bootstrapScenario(request) {
59
+ const scenarioId = normalizeScenarioId(request?.scenarioId ?? DEFAULT_SCENARIO_ID);
60
+ this.nodeId = request?.nodeId?.trim() || DEFAULT_SCENARIO_NODE_ID;
61
+ this.hostname = request?.hostname?.trim() || DEFAULT_SCENARIO_HOSTNAME;
62
+ this.ensureRegisteredNode();
63
+ const channels = (request?.channels?.length ? request.channels : DEFAULT_CHANNEL_SPECS).map((channel) => ({
64
+ name: channel.name.trim(),
65
+ description: channel.description?.trim() || `Scenario channel: ${channel.name.trim()}`,
66
+ collaborationMode: channel.collaborationMode ?? 'mention_only',
67
+ }));
68
+ const createdChannels = channels.map((channel) => this.ensureChannel(channel));
69
+ const agents = (request?.agents?.length ? request.agents : DEFAULT_AGENT_SPECS).map((agent) => ({
70
+ ...agent,
71
+ name: agent.name.trim(),
72
+ }));
73
+ const createdAgents = agents.map((agent) => this.ensureAgent({
74
+ scenarioId,
75
+ agent,
76
+ fallbackChannelName: channels[0]?.name ?? DEFAULT_CHANNEL_SPECS[0].name,
77
+ }));
78
+ for (const spec of agents) {
79
+ const createdAgent = createdAgents.find((agent) => agent.name === spec.name);
80
+ if (!createdAgent)
81
+ continue;
82
+ this.setScript(createdAgent.agentId, spec.script);
83
+ }
84
+ this.refreshInventory();
85
+ return {
86
+ scenarioId,
87
+ nodeId: this.nodeId,
88
+ channels: createdChannels.map((channel) => ({
89
+ channelId: channel.channelId,
90
+ name: channel.name,
91
+ })),
92
+ agents: createdAgents.map((agent) => ({
93
+ agentId: agent.agentId,
94
+ name: agent.name,
95
+ workspacePath: agent.workspacePath ?? null,
96
+ })),
97
+ };
98
+ }
99
+ ensureRegisteredNode() {
100
+ const supportedAgentTypes = listRuntimeDrivers().map((driver) => driver.agentType);
101
+ this.nodeRegistry.register({
102
+ nodeId: this.nodeId,
103
+ hostname: this.hostname,
104
+ agentTypes: supportedAgentTypes,
105
+ version: this.version,
106
+ terminalBackendAvailable: false,
107
+ localSend: (msg) => this.handleCoreMessage(msg),
108
+ lastSeen: Date.now(),
109
+ });
110
+ const now = Date.now();
111
+ this.db.prepare(`INSERT INTO nodes(node_id, hostname, agent_types_json, version, status, last_seen, created_at, provisioned_at, display_name, env_var_keys)
112
+ VALUES(?, ?, ?, ?, 'online', ?, ?, ?, ?, '[]')
113
+ ON CONFLICT(node_id) DO UPDATE SET
114
+ hostname = excluded.hostname,
115
+ agent_types_json = excluded.agent_types_json,
116
+ version = excluded.version,
117
+ status = 'online',
118
+ last_seen = excluded.last_seen,
119
+ display_name = excluded.display_name`).run(this.nodeId, this.hostname, JSON.stringify(supportedAgentTypes), this.version, now, now, now, this.hostname);
120
+ persistScenarioRuntimeSnapshot(this.db, {
121
+ nodeId: this.nodeId,
122
+ snapshotAt: now,
123
+ runtime: {
124
+ workspaceRoot: this.workspaceRoot,
125
+ terminalBackendAvailable: false,
126
+ runtimeDrivers: listRuntimeDrivers().map((driver) => ({
127
+ agentType: driver.agentType,
128
+ command: driver.command,
129
+ args: [...driver.args],
130
+ supportsResume: driver.supportsResume,
131
+ supportsPushNotifications: driver.supportsPushNotifications,
132
+ capabilities: [...driver.capabilities],
133
+ nativeMemoryBackend: driver.nativeMemoryBackend,
134
+ supportsClaudeSessionControls: driver.agentType === 'claude_sdk',
135
+ })),
136
+ capabilities: {
137
+ workspaceRpc: true,
138
+ workspaceInspect: true,
139
+ workspaceGit: false,
140
+ terminals: false,
141
+ skills: false,
142
+ codexAppServerRpc: false,
143
+ codexTranscripts: false,
144
+ claudeTranscripts: false,
145
+ claudeControls: false,
146
+ agentSurfaceMode: 'bigbang',
147
+ bigbangCliAvailable: true,
148
+ },
149
+ },
150
+ });
151
+ }
152
+ handleCoreMessage(msg) {
153
+ this.nodeRegistry.heartbeat(this.nodeId);
154
+ this.db.prepare(`UPDATE nodes SET status = 'online', last_seen = ? WHERE node_id = ?`).run(Date.now(), this.nodeId);
155
+ switch (msg.type) {
156
+ case 'run.dispatch':
157
+ void this.handleRunDispatch(msg);
158
+ return true;
159
+ case 'run.cancel':
160
+ this.activeRuns.delete(msg.runId);
161
+ this.refreshInventory();
162
+ return true;
163
+ case 'workspace.list.request':
164
+ this.handleWorkspaceList(msg);
165
+ return true;
166
+ case 'workspace.read.request':
167
+ this.handleWorkspaceRead(msg);
168
+ return true;
169
+ case 'fs.read_path.request':
170
+ this.handleFsReadPath(msg);
171
+ return true;
172
+ case 'http.fetch_text.request':
173
+ this.workspaceBroker.handleHttpFetchTextResponse({
174
+ type: 'http.fetch_text.response',
175
+ requestId: msg.requestId,
176
+ url: msg.url,
177
+ error: 'Scenario runtime does not support panel api_jsonl HTTP fetch.',
178
+ errorCode: 'invalid_request',
179
+ });
180
+ return true;
181
+ case 'workspace.write.request':
182
+ this.handleWorkspaceWrite(msg);
183
+ return true;
184
+ case 'library.mirror.apply.request':
185
+ this.workspaceBroker.handleLibraryMirrorApplyResponse({
186
+ type: 'library.mirror.apply.response',
187
+ requestId: msg.requestId,
188
+ ok: true,
189
+ });
190
+ return true;
191
+ case 'library.mirror.snapshot.request':
192
+ this.workspaceBroker.handleLibraryMirrorSnapshotResponse({
193
+ type: 'library.mirror.snapshot.response',
194
+ requestId: msg.requestId,
195
+ shared: null,
196
+ personal: null,
197
+ });
198
+ return true;
199
+ case 'library.authority.delete.request':
200
+ this.handleLibraryAuthorityDelete(msg);
201
+ return true;
202
+ case 'library.authority.rename.request':
203
+ this.handleLibraryAuthorityRename(msg);
204
+ return true;
205
+ case 'workspace.inspect.request':
206
+ this.handleWorkspaceInspect(msg);
207
+ return true;
208
+ case 'workspace.reset.request':
209
+ this.handleWorkspaceReset(msg);
210
+ return true;
211
+ case 'host.close':
212
+ return true;
213
+ default:
214
+ return false;
215
+ }
216
+ }
217
+ async handleRunDispatch(msg) {
218
+ const agentId = msg.channelBridgeConfig?.agentId;
219
+ if (!agentId) {
220
+ log.warn('[scenario-runtime] run.dispatch missing channelBridgeConfig.agentId', { runId: msg.runId });
221
+ return;
222
+ }
223
+ const accepted = this.conversationManager.handleRunAccepted(msg.runId, msg.conversationId);
224
+ if (!accepted) {
225
+ log.debug('[scenario-runtime] ignoring run.dispatch without pending waiter', {
226
+ runId: msg.runId,
227
+ conversationId: msg.conversationId,
228
+ });
229
+ return;
230
+ }
231
+ this.activeRuns.add(msg.runId);
232
+ this.refreshInventory();
233
+ try {
234
+ const script = this.scripts.get(agentId);
235
+ if (!script || script.index >= script.steps.length) {
236
+ await this.finishRun(msg.runId, msg.conversationId);
237
+ return;
238
+ }
239
+ const currentStep = script.steps[script.index];
240
+ this.assertStepMatches(msg, currentStep.when);
241
+ script.index += 1;
242
+ for (const action of currentStep.actions) {
243
+ if (!this.activeRuns.has(msg.runId))
244
+ break;
245
+ await this.executeAction(agentId, msg, action);
246
+ }
247
+ await this.finishRun(msg.runId, msg.conversationId);
248
+ }
249
+ catch (error) {
250
+ log.warn('[scenario-runtime] scripted run failed', {
251
+ runId: msg.runId,
252
+ conversationId: msg.conversationId,
253
+ error: error instanceof Error ? error.message : String(error),
254
+ });
255
+ await this.finishRun(msg.runId, msg.conversationId, error instanceof Error ? error.message : String(error));
256
+ }
257
+ finally {
258
+ this.activeRuns.delete(msg.runId);
259
+ this.refreshInventory();
260
+ }
261
+ }
262
+ async executeAction(agentId, dispatch, action) {
263
+ const agent = this.conversationManager.getAgent(agentId);
264
+ if (!agent)
265
+ throw new Error(`Scenario agent not found: ${agentId}`);
266
+ switch (action.type) {
267
+ case 'send_message': {
268
+ const response = await this.injectJson(`/api/internal/agent/${agentId}/send`, {
269
+ target: action.target,
270
+ content: action.content,
271
+ kind: action.kind,
272
+ conversationId: dispatch.conversationId,
273
+ });
274
+ if (response.statusCode >= 400) {
275
+ throw new Error(`send_message failed: ${JSON.stringify(response.body)}`);
276
+ }
277
+ return;
278
+ }
279
+ case 'create_tasks': {
280
+ const response = await this.injectJson(`/api/internal/agent/${agentId}/tasks`, {
281
+ channel: action.channel ?? this.defaultChannelTarget(agent.channelId),
282
+ tasks: action.tasks,
283
+ conversationId: dispatch.conversationId,
284
+ collaborator_names: action.collaborator_names,
285
+ });
286
+ if (response.statusCode >= 400) {
287
+ throw new Error(`create_tasks failed: ${JSON.stringify(response.body)}`);
288
+ }
289
+ return;
290
+ }
291
+ case 'claim_tasks': {
292
+ const response = await this.injectJson(`/api/internal/agent/${agentId}/tasks/claim`, {
293
+ channel: action.channel ?? this.defaultChannelTarget(agent.channelId),
294
+ task_numbers: action.task_numbers,
295
+ message_ids: action.message_ids,
296
+ conversationId: dispatch.conversationId,
297
+ collaborator_names: action.collaborator_names,
298
+ });
299
+ if (response.statusCode >= 400) {
300
+ throw new Error(`claim_tasks failed: ${JSON.stringify(response.body)}`);
301
+ }
302
+ return;
303
+ }
304
+ case 'update_task_status': {
305
+ const response = await this.injectJson(`/api/internal/agent/${agentId}/tasks/update-status`, {
306
+ channel: action.channel ?? this.defaultChannelTarget(agent.channelId),
307
+ task_number: action.task_number,
308
+ status: action.status,
309
+ conversationId: dispatch.conversationId,
310
+ });
311
+ if (response.statusCode >= 400) {
312
+ throw new Error(`update_task_status failed: ${JSON.stringify(response.body)}`);
313
+ }
314
+ return;
315
+ }
316
+ case 'write_memory': {
317
+ if (!agent.workspacePath) {
318
+ throw new Error(`Agent has no workspace: ${agent.name}`);
319
+ }
320
+ await this.workspaceBroker.writeFile(this.nodeId, agent.workspacePath, action.relativePath, action.content, action.mode ?? 'append', { agentName: agent.name });
321
+ return;
322
+ }
323
+ case 'read_history': {
324
+ const params = new URLSearchParams({ channel: action.channel });
325
+ if (action.limit !== undefined)
326
+ params.set('limit', String(action.limit));
327
+ if (action.around)
328
+ params.set('around', action.around);
329
+ if (action.before !== undefined)
330
+ params.set('before', String(action.before));
331
+ if (action.after !== undefined)
332
+ params.set('after', String(action.after));
333
+ if (action.include_root)
334
+ params.set('include_root', 'true');
335
+ const response = await this.injectGet(`/api/internal/agent/${agentId}/history?${params.toString()}`);
336
+ if (response.statusCode >= 400) {
337
+ throw new Error(`read_history failed: ${JSON.stringify(response.body)}`);
338
+ }
339
+ this.assertTextIncludes('read_history', response.body, action.expect_text_includes);
340
+ return;
341
+ }
342
+ case 'get_self_state': {
343
+ const response = await this.injectGet(`/api/internal/agent/${agentId}/self-state?conversationId=${encodeURIComponent(dispatch.conversationId)}`);
344
+ if (response.statusCode >= 400) {
345
+ throw new Error(`get_self_state failed: ${JSON.stringify(response.body)}`);
346
+ }
347
+ this.assertTextIncludes('get_self_state', response.body, action.expect_text_includes);
348
+ return;
349
+ }
350
+ case 'get_runtime_presence': {
351
+ const response = await this.injectGet(`/api/internal/agent/${agentId}/runtime-presence?conversationId=${encodeURIComponent(dispatch.conversationId)}`);
352
+ if (response.statusCode >= 400) {
353
+ throw new Error(`get_runtime_presence failed: ${JSON.stringify(response.body)}`);
354
+ }
355
+ this.assertTextIncludes('get_runtime_presence', response.body, action.expect_text_includes);
356
+ return;
357
+ }
358
+ case 'get_context_bundle': {
359
+ const response = await this.injectJson(`/api/internal/agent/${agentId}/context-bundle`, {
360
+ conversationId: dispatch.conversationId,
361
+ maxSurfaces: action.maxSurfaces,
362
+ maxTasks: action.maxTasks,
363
+ maxHandoffs: action.maxHandoffs,
364
+ });
365
+ if (response.statusCode >= 400) {
366
+ throw new Error(`get_context_bundle failed: ${JSON.stringify(response.body)}`);
367
+ }
368
+ this.assertTextIncludes('get_context_bundle', response.body, action.expect_text_includes);
369
+ return;
370
+ }
371
+ case 'handoff_to_target': {
372
+ const response = await this.injectJson(`/api/internal/agent/${agentId}/handoff`, {
373
+ conversationId: dispatch.conversationId,
374
+ target: action.target,
375
+ mode: action.mode ?? 'delegate_only',
376
+ goal: action.goal,
377
+ context: {
378
+ why_now: action.why_now,
379
+ constraints: action.constraints,
380
+ already_done: action.already_done,
381
+ expected_output: action.expected_output,
382
+ related_task_ref: action.related_task_ref,
383
+ optional_refs: action.optional_refs,
384
+ },
385
+ });
386
+ if (response.statusCode >= 400) {
387
+ throw new Error(`handoff_to_target failed: ${JSON.stringify(response.body)}`);
388
+ }
389
+ this.assertTextIncludes('handoff_to_target', response.body, action.expect_text_includes);
390
+ return;
391
+ }
392
+ default: {
393
+ const neverAction = action;
394
+ throw new Error(`Unsupported scenario action: ${JSON.stringify(neverAction)}`);
395
+ }
396
+ }
397
+ }
398
+ handleWorkspaceList(msg) {
399
+ try {
400
+ const result = listWorkspaceDirectory(msg.workspaceRoot, msg.relativePath, msg.scaffold !== false, msg.agentName, {
401
+ cursor: msg.cursor,
402
+ limit: msg.limit,
403
+ });
404
+ this.workspaceBroker.handleWorkspaceListResponse({
405
+ type: 'workspace.list.response',
406
+ requestId: msg.requestId,
407
+ relativePath: result.relativePath,
408
+ entries: result.entries,
409
+ directoryPage: result.directoryPage,
410
+ });
411
+ }
412
+ catch (error) {
413
+ this.workspaceBroker.handleWorkspaceListResponse({
414
+ type: 'workspace.list.response',
415
+ requestId: msg.requestId,
416
+ relativePath: msg.relativePath,
417
+ error: error instanceof Error ? error.message : String(error),
418
+ errorCode: 'io_error',
419
+ });
420
+ }
421
+ }
422
+ handleWorkspaceRead(msg) {
423
+ try {
424
+ const result = readWorkspaceFile(msg.workspaceRoot, msg.relativePath, msg.scaffold !== false, msg.agentName);
425
+ this.workspaceBroker.handleWorkspaceReadResponse({
426
+ type: 'workspace.read.response',
427
+ requestId: msg.requestId,
428
+ relativePath: result.relativePath,
429
+ content: result.content,
430
+ mimeType: result.mimeType,
431
+ size: result.size,
432
+ modifiedAt: result.modifiedAt,
433
+ });
434
+ }
435
+ catch (error) {
436
+ this.workspaceBroker.handleWorkspaceReadResponse({
437
+ type: 'workspace.read.response',
438
+ requestId: msg.requestId,
439
+ relativePath: msg.relativePath,
440
+ error: error instanceof Error ? error.message : String(error),
441
+ errorCode: 'io_error',
442
+ });
443
+ }
444
+ }
445
+ handleFsReadPath(msg) {
446
+ try {
447
+ const resolved = resolveWorkspacePath(this.workspaceRoot, msg.absolutePath);
448
+ const stat = fs.statSync(resolved, { throwIfNoEntry: false });
449
+ if (!stat)
450
+ throw new Error('Path not found.');
451
+ if (stat.isDirectory()) {
452
+ const result = listAbsoluteDirectoryPage(resolved, msg.cursor, msg.limit);
453
+ this.workspaceBroker.handleFsReadPathResponse({
454
+ type: 'fs.read_path.response',
455
+ requestId: msg.requestId,
456
+ absolutePath: resolved,
457
+ kind: 'directory',
458
+ entries: result.entries,
459
+ directoryPage: result.directoryPage,
460
+ modifiedAt: Math.floor(stat.mtimeMs),
461
+ });
462
+ return;
463
+ }
464
+ const result = readWorkspaceFile(path.dirname(resolved), path.basename(resolved), false);
465
+ this.workspaceBroker.handleFsReadPathResponse({
466
+ type: 'fs.read_path.response',
467
+ requestId: msg.requestId,
468
+ absolutePath: resolved,
469
+ kind: 'file',
470
+ content: result.content,
471
+ mimeType: result.mimeType,
472
+ size: result.size,
473
+ modifiedAt: result.modifiedAt,
474
+ });
475
+ }
476
+ catch (error) {
477
+ this.workspaceBroker.handleFsReadPathResponse({
478
+ type: 'fs.read_path.response',
479
+ requestId: msg.requestId,
480
+ absolutePath: msg.absolutePath,
481
+ error: error instanceof Error ? error.message : String(error),
482
+ errorCode: 'io_error',
483
+ });
484
+ }
485
+ }
486
+ handleWorkspaceWrite(msg) {
487
+ try {
488
+ const result = writeWorkspaceFile(msg.workspaceRoot, msg.relativePath, msg.content, msg.mode, msg.scaffold !== false, msg.agentName);
489
+ this.workspaceBroker.handleWorkspaceWriteResponse({
490
+ type: 'workspace.write.response',
491
+ requestId: msg.requestId,
492
+ relativePath: result.relativePath,
493
+ modifiedAt: result.modifiedAt,
494
+ ok: true,
495
+ });
496
+ }
497
+ catch (error) {
498
+ this.workspaceBroker.handleWorkspaceWriteResponse({
499
+ type: 'workspace.write.response',
500
+ requestId: msg.requestId,
501
+ relativePath: msg.relativePath,
502
+ error: error instanceof Error ? error.message : String(error),
503
+ errorCode: 'io_error',
504
+ });
505
+ }
506
+ }
507
+ handleLibraryAuthorityDelete(msg) {
508
+ try {
509
+ const libraryRoot = path.resolve(msg.libraryRoot);
510
+ const normalized = normalizeScenarioLibraryRelativePath(msg.relativePath);
511
+ const target = path.resolve(libraryRoot, normalized);
512
+ if (target !== libraryRoot && !target.startsWith(`${libraryRoot}${path.sep}`)) {
513
+ throw new Error('Invalid library path');
514
+ }
515
+ const stat = fs.lstatSync(target, { throwIfNoEntry: false });
516
+ if (stat) {
517
+ if (stat.isDirectory()) {
518
+ if (msg.recursive !== true) {
519
+ throw new Error('Directory deletion requires recursive=true');
520
+ }
521
+ fs.rmSync(target, { recursive: true, force: true });
522
+ }
523
+ else {
524
+ fs.rmSync(target, { force: true });
525
+ }
526
+ }
527
+ this.workspaceBroker.handleLibraryAuthorityDeleteResponse({
528
+ type: 'library.authority.delete.response',
529
+ requestId: msg.requestId,
530
+ ok: true,
531
+ });
532
+ }
533
+ catch (error) {
534
+ this.workspaceBroker.handleLibraryAuthorityDeleteResponse({
535
+ type: 'library.authority.delete.response',
536
+ requestId: msg.requestId,
537
+ error: error instanceof Error ? error.message : String(error),
538
+ errorCode: 'io_error',
539
+ });
540
+ }
541
+ }
542
+ handleLibraryAuthorityRename(msg) {
543
+ try {
544
+ const libraryRoot = path.resolve(msg.libraryRoot);
545
+ const normalized = normalizeScenarioLibraryRelativePath(msg.relativePath);
546
+ const normalizedNext = normalizeScenarioLibraryRelativePath(msg.nextRelativePath);
547
+ const target = path.resolve(libraryRoot, normalized);
548
+ const nextTarget = path.resolve(libraryRoot, normalizedNext);
549
+ if ((target !== libraryRoot && !target.startsWith(`${libraryRoot}${path.sep}`))
550
+ || (nextTarget !== libraryRoot && !nextTarget.startsWith(`${libraryRoot}${path.sep}`))) {
551
+ throw new Error('Invalid library path');
552
+ }
553
+ const sourceStat = fs.lstatSync(target, { throwIfNoEntry: false });
554
+ if (!sourceStat) {
555
+ throw new Error('Path not found');
556
+ }
557
+ if (fs.lstatSync(nextTarget, { throwIfNoEntry: false })) {
558
+ throw new Error('Destination already exists');
559
+ }
560
+ fs.mkdirSync(path.dirname(nextTarget), { recursive: true });
561
+ fs.renameSync(target, nextTarget);
562
+ this.workspaceBroker.handleLibraryAuthorityRenameResponse({
563
+ type: 'library.authority.rename.response',
564
+ requestId: msg.requestId,
565
+ ok: true,
566
+ });
567
+ }
568
+ catch (error) {
569
+ this.workspaceBroker.handleLibraryAuthorityRenameResponse({
570
+ type: 'library.authority.rename.response',
571
+ requestId: msg.requestId,
572
+ error: error instanceof Error ? error.message : String(error),
573
+ errorCode: 'io_error',
574
+ });
575
+ }
576
+ }
577
+ handleWorkspaceInspect(msg) {
578
+ try {
579
+ this.inspectBroker.handleInspectResponse({
580
+ type: 'workspace.inspect.response',
581
+ requestId: msg.requestId,
582
+ inspect: inspectWorkspace(msg.workspaceRoot),
583
+ });
584
+ }
585
+ catch (error) {
586
+ this.inspectBroker.handleInspectResponse({
587
+ type: 'workspace.inspect.response',
588
+ requestId: msg.requestId,
589
+ error: error instanceof Error ? error.message : String(error),
590
+ errorCode: 'io_error',
591
+ });
592
+ }
593
+ }
594
+ handleWorkspaceReset(msg) {
595
+ try {
596
+ resetWorkspaceDirectory(msg.workspaceRoot, msg.agentName);
597
+ this.workspaceBroker.handleWorkspaceResetResponse({
598
+ type: 'workspace.reset.response',
599
+ requestId: msg.requestId,
600
+ workspaceRoot: msg.workspaceRoot,
601
+ ok: true,
602
+ });
603
+ }
604
+ catch (error) {
605
+ this.workspaceBroker.handleWorkspaceResetResponse({
606
+ type: 'workspace.reset.response',
607
+ requestId: msg.requestId,
608
+ workspaceRoot: msg.workspaceRoot,
609
+ error: error instanceof Error ? error.message : String(error),
610
+ errorCode: 'io_error',
611
+ });
612
+ }
613
+ }
614
+ ensureChannel(channel) {
615
+ const existing = this.conversationManager.listChannels().find((entry) => entry.name === channel.name);
616
+ if (existing) {
617
+ this.conversationManager.updateChannel(existing.channelId, {
618
+ description: channel.description,
619
+ collaborationMode: channel.collaborationMode,
620
+ });
621
+ return existing;
622
+ }
623
+ return this.conversationManager.createChannel({
624
+ name: channel.name,
625
+ description: channel.description,
626
+ collaborationMode: channel.collaborationMode,
627
+ workspacePath: path.join(this.workspaceRoot, 'scenarios', 'channels', channel.name),
628
+ });
629
+ }
630
+ ensureAgent(params) {
631
+ const existing = this.conversationManager.listAgents().find((entry) => entry.name === params.agent.name);
632
+ const homeChannelName = params.agent.homeChannel?.trim()
633
+ || params.agent.channel?.trim()
634
+ || params.fallbackChannelName;
635
+ const homeChannel = this.requireChannelByName(homeChannelName);
636
+ if (existing) {
637
+ const joinedChannels = new Set(existing.channelIds ?? [existing.channelId]);
638
+ for (const channelName of params.agent.channels ?? []) {
639
+ const channel = this.requireChannelByName(channelName);
640
+ this.conversationManager.joinChannel(existing.agentId, channel.channelId);
641
+ joinedChannels.add(channel.channelId);
642
+ }
643
+ this.conversationManager.joinChannel(existing.agentId, homeChannel.channelId);
644
+ for (const channelId of joinedChannels) {
645
+ this.conversationManager.openAgentChannelThread(existing.agentId, channelId, null);
646
+ }
647
+ return existing;
648
+ }
649
+ const workspacePath = path.join(this.workspaceRoot, 'scenarios', params.scenarioId, slugify(params.agent.name));
650
+ ensureLocalWorkspaceScaffold(workspacePath, params.agent.name);
651
+ const created = this.conversationManager.createAgent({
652
+ name: params.agent.name,
653
+ agentType: params.agent.agentType ?? 'codex_app_server',
654
+ channelId: homeChannel.channelId,
655
+ nodeId: this.nodeId,
656
+ workspacePath,
657
+ systemPrompt: params.agent.systemPrompt ?? 'Scenario fake agent. Actions are scripted; platform execution is real.',
658
+ description: params.agent.description ?? 'Scenario-mode fake agent for scripted collaboration tests.',
659
+ });
660
+ const allChannelNames = new Set([
661
+ homeChannel.name,
662
+ ...(params.agent.channels ?? []),
663
+ ]);
664
+ for (const channelName of allChannelNames) {
665
+ const channel = this.requireChannelByName(channelName);
666
+ this.conversationManager.joinChannel(created.agentId, channel.channelId);
667
+ this.conversationManager.openAgentChannelThread(created.agentId, channel.channelId, null);
668
+ }
669
+ return created;
670
+ }
671
+ requireChannelByName(name) {
672
+ const channel = this.conversationManager.listChannels().find((entry) => entry.name === name);
673
+ if (!channel) {
674
+ throw new Error(`Scenario channel not found: ${name}`);
675
+ }
676
+ return channel;
677
+ }
678
+ setScript(agentId, script) {
679
+ if (!script) {
680
+ this.scripts.delete(agentId);
681
+ return;
682
+ }
683
+ this.scripts.set(agentId, {
684
+ steps: script.steps ?? [],
685
+ index: 0,
686
+ });
687
+ }
688
+ assertStepMatches(dispatch, when) {
689
+ if (!when)
690
+ return;
691
+ const conversation = this.conversationManager.getConversation(dispatch.conversationId);
692
+ const replyTarget = conversation?.replyTarget ?? null;
693
+ if (when.target && replyTarget !== when.target) {
694
+ throw new Error(`Scenario step target mismatch: expected ${when.target}, got ${replyTarget ?? 'null'}`);
695
+ }
696
+ for (const fragment of when.promptIncludes ?? []) {
697
+ if (!dispatch.prompt.includes(fragment)) {
698
+ throw new Error(`Scenario step prompt mismatch: missing "${fragment}"`);
699
+ }
700
+ }
701
+ for (const fragment of when.contextIncludes ?? []) {
702
+ if (!(dispatch.contextText ?? '').includes(fragment)) {
703
+ throw new Error(`Scenario step context mismatch: missing "${fragment}"`);
704
+ }
705
+ }
706
+ }
707
+ async finishRun(runId, conversationId, error) {
708
+ const endedAt = Date.now();
709
+ if (error) {
710
+ this.db.prepare(`UPDATE runs
711
+ SET ended_at = ?, error = ?
712
+ WHERE run_id = ?`).run(endedAt, error, runId);
713
+ this.db.prepare(`UPDATE conversations SET status = 'failed', updated_at = ? WHERE id = ?`).run(endedAt, conversationId);
714
+ }
715
+ else {
716
+ this.db.prepare(`UPDATE runs
717
+ SET ended_at = ?, stop_reason = COALESCE(stop_reason, 'end_turn')
718
+ WHERE run_id = ?`).run(endedAt, runId);
719
+ this.db.prepare(`UPDATE conversations SET status = 'idle', updated_at = ? WHERE id = ?`).run(endedAt, conversationId);
720
+ }
721
+ await this.conversationManager.onConversationSettled(conversationId);
722
+ }
723
+ refreshInventory() {
724
+ const now = Date.now();
725
+ this.db.prepare(`UPDATE nodes SET last_seen = ?, status = 'online' WHERE node_id = ?`).run(now, this.nodeId);
726
+ const hostRows = this.db.prepare(`SELECT c.id as conversationId,
727
+ c.session_key as sessionKey,
728
+ c.workspace_path as workspacePath,
729
+ c.agent_type as agentType,
730
+ c.status,
731
+ r.run_id as currentRunId
732
+ FROM conversations c
733
+ LEFT JOIN runs r ON r.run_id = (
734
+ SELECT run_id
735
+ FROM runs
736
+ WHERE session_key = c.session_key AND ended_at IS NULL
737
+ ORDER BY started_at DESC
738
+ LIMIT 1
739
+ )
740
+ WHERE c.node_id = ?`).all(this.nodeId);
741
+ persistScenarioHostSnapshots(this.db, {
742
+ nodeId: this.nodeId,
743
+ snapshotAt: now,
744
+ hosts: hostRows.map((row) => ({
745
+ hostKey: `conversation:${row.conversationId}:${row.agentType}`,
746
+ sessionKey: row.sessionKey,
747
+ workspaceRoot: row.workspacePath ?? '',
748
+ agentType: row.agentType,
749
+ state: scenarioHostStateForConversationStatus(row.status),
750
+ currentRunId: row.currentRunId,
751
+ hasPendingApproval: false,
752
+ inboxSize: 0,
753
+ pendingDispatchCount: 0,
754
+ lastWakeAt: now,
755
+ lastSleepAt: row.currentRunId ? null : now,
756
+ lastError: null,
757
+ resumable: true,
758
+ })),
759
+ });
760
+ }
761
+ async injectJson(url, body) {
762
+ if (!this.app) {
763
+ throw new Error('Scenario runtime is not bound to Fastify yet.');
764
+ }
765
+ const headers = {
766
+ 'content-type': 'application/json',
767
+ };
768
+ if (this.internalAgentAuthToken) {
769
+ headers.authorization = `Bearer ${this.internalAgentAuthToken}`;
770
+ }
771
+ const response = await this.app.inject({
772
+ method: 'POST',
773
+ url,
774
+ payload: body,
775
+ headers,
776
+ });
777
+ return {
778
+ statusCode: response.statusCode,
779
+ body: response.body ? response.json() : {},
780
+ };
781
+ }
782
+ async injectGet(url) {
783
+ if (!this.app) {
784
+ throw new Error('Scenario runtime is not bound to Fastify yet.');
785
+ }
786
+ const headers = {};
787
+ if (this.internalAgentAuthToken) {
788
+ headers.authorization = `Bearer ${this.internalAgentAuthToken}`;
789
+ }
790
+ const response = await this.app.inject({
791
+ method: 'GET',
792
+ url,
793
+ headers,
794
+ });
795
+ return {
796
+ statusCode: response.statusCode,
797
+ body: response.body ? response.json() : {},
798
+ };
799
+ }
800
+ assertTextIncludes(label, value, fragments) {
801
+ if (!fragments?.length)
802
+ return;
803
+ const haystack = JSON.stringify(value);
804
+ for (const fragment of fragments) {
805
+ if (!haystack.includes(fragment)) {
806
+ throw new Error(`${label} assertion failed: missing "${fragment}" in ${haystack}`);
807
+ }
808
+ }
809
+ }
810
+ defaultChannelTarget(channelId) {
811
+ const channel = this.conversationManager.getChannel(channelId);
812
+ if (!channel)
813
+ throw new Error(`Scenario channel not found: ${channelId}`);
814
+ return `#${channel.name}`;
815
+ }
816
+ }
817
+ function normalizeScenarioId(value) {
818
+ const normalized = value.trim().toLowerCase();
819
+ return normalized || DEFAULT_SCENARIO_ID;
820
+ }
821
+ function slugify(value) {
822
+ return value
823
+ .trim()
824
+ .toLowerCase()
825
+ .replace(/[^a-z0-9]+/g, '-')
826
+ .replace(/^-+|-+$/g, '')
827
+ || 'scenario-agent';
828
+ }
829
+ function scenarioHostStateForConversationStatus(status) {
830
+ switch (status) {
831
+ case 'active':
832
+ return 'running';
833
+ case 'queued':
834
+ return 'queued';
835
+ case 'failed':
836
+ return 'error';
837
+ default:
838
+ return 'idle';
839
+ }
840
+ }
841
+ function listWorkspaceDirectory(workspaceRoot, relativePath, scaffold, agentName, options = {}) {
842
+ if (scaffold)
843
+ ensureLocalWorkspaceScaffold(workspaceRoot, agentName);
844
+ const resolved = resolveRelativeWorkspacePath(workspaceRoot, relativePath);
845
+ const stat = fs.statSync(resolved, { throwIfNoEntry: false });
846
+ if (!stat || !stat.isDirectory())
847
+ throw new Error('Path is not a directory.');
848
+ const candidates = fs.readdirSync(resolved, { withFileTypes: true })
849
+ .map((entry) => {
850
+ const absoluteEntry = path.join(resolved, entry.name);
851
+ return classifyWorkspaceEntryCandidate(entry, absoluteEntry);
852
+ })
853
+ .sort((a, b) => {
854
+ if (a.kind !== b.kind)
855
+ return a.kind === 'directory' ? -1 : 1;
856
+ return a.dirent.name.localeCompare(b.dirent.name);
857
+ });
858
+ const { candidates: pageCandidates, directoryPage } = paginateWorkspaceEntryCandidates(candidates, options);
859
+ const entries = pageCandidates.map((candidate) => workspaceEntryFromCandidate(workspaceRoot, candidate));
860
+ return {
861
+ relativePath: normalizeRelativePath(relativePath),
862
+ entries,
863
+ directoryPage,
864
+ };
865
+ }
866
+ function readWorkspaceFile(workspaceRoot, relativePath, scaffold, agentName) {
867
+ if (scaffold)
868
+ ensureLocalWorkspaceScaffold(workspaceRoot, agentName);
869
+ const resolved = resolveRelativeWorkspacePath(workspaceRoot, relativePath);
870
+ const stat = fs.statSync(resolved, { throwIfNoEntry: false });
871
+ if (!stat || !stat.isFile())
872
+ throw new Error('Path is not a file.');
873
+ return {
874
+ relativePath: normalizeRelativePath(relativePath),
875
+ content: fs.readFileSync(resolved, 'utf8'),
876
+ mimeType: resolved.toLowerCase().endsWith('.md') ? 'text/markdown' : 'text/plain',
877
+ size: stat.size,
878
+ modifiedAt: Math.floor(stat.mtimeMs),
879
+ };
880
+ }
881
+ function writeWorkspaceFile(workspaceRoot, relativePath, content, mode, scaffold, agentName) {
882
+ if (scaffold)
883
+ ensureLocalWorkspaceScaffold(workspaceRoot, agentName);
884
+ const resolved = resolveRelativeWorkspacePath(workspaceRoot, relativePath);
885
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
886
+ if (mode === 'append') {
887
+ fs.appendFileSync(resolved, content, 'utf8');
888
+ }
889
+ else {
890
+ fs.writeFileSync(resolved, content, 'utf8');
891
+ }
892
+ const stat = fs.statSync(resolved, { throwIfNoEntry: false });
893
+ return {
894
+ relativePath: normalizeRelativePath(relativePath),
895
+ modifiedAt: stat?.mtimeMs ? Math.floor(stat.mtimeMs) : null,
896
+ };
897
+ }
898
+ function resetWorkspaceDirectory(workspaceRoot, agentName) {
899
+ const resolvedRoot = path.resolve(workspaceRoot);
900
+ fs.mkdirSync(resolvedRoot, { recursive: true });
901
+ for (const entry of fs.readdirSync(resolvedRoot, { withFileTypes: true })) {
902
+ const target = path.join(resolvedRoot, entry.name);
903
+ fs.rmSync(target, { recursive: true, force: true });
904
+ }
905
+ ensureLocalWorkspaceScaffold(resolvedRoot, agentName);
906
+ }
907
+ function inspectWorkspace(workspaceRoot) {
908
+ const resolvedRoot = path.resolve(workspaceRoot);
909
+ const gitDir = findGitDir(resolvedRoot);
910
+ return {
911
+ workspaceRoot: resolvedRoot,
912
+ isGit: Boolean(gitDir),
913
+ repoRoot: gitDir,
914
+ workspaceKind: 'directory',
915
+ branchName: null,
916
+ remoteUrl: null,
917
+ };
918
+ }
919
+ function findGitDir(startPath) {
920
+ let current = path.resolve(startPath);
921
+ while (true) {
922
+ if (fs.existsSync(path.join(current, '.git')))
923
+ return current;
924
+ const parent = path.dirname(current);
925
+ if (parent === current)
926
+ return null;
927
+ current = parent;
928
+ }
929
+ }
930
+ function resolveRelativeWorkspacePath(workspaceRoot, relativePath) {
931
+ const normalized = normalizeRelativePath(relativePath);
932
+ const absoluteRequested = normalized
933
+ ? path.resolve(workspaceRoot, normalized)
934
+ : path.resolve(workspaceRoot);
935
+ return resolveWorkspacePath(workspaceRoot, absoluteRequested);
936
+ }
937
+ function normalizeRelativePath(relativePath) {
938
+ return relativePath.replace(/^\/+/, '').trim();
939
+ }
940
+ function toRelativeWorkspacePath(workspaceRoot, absolutePath) {
941
+ const relative = path.relative(path.resolve(workspaceRoot), path.resolve(absolutePath));
942
+ return relative === '.' ? '' : relative.split(path.sep).join('/');
943
+ }
944
+ function persistScenarioHostSnapshots(db, params) {
945
+ db.transaction(() => {
946
+ if (params.hosts.length === 0) {
947
+ db.prepare(`DELETE FROM node_host_snapshots WHERE node_id = ?`).run(params.nodeId);
948
+ return;
949
+ }
950
+ const placeholders = params.hosts.map(() => '?').join(', ');
951
+ db.prepare(`DELETE FROM node_host_snapshots WHERE node_id = ? AND host_key NOT IN (${placeholders})`).run(params.nodeId, ...params.hosts.map((host) => host.hostKey));
952
+ const upsert = db.prepare(`INSERT INTO node_host_snapshots(
953
+ node_id, host_key, session_key, workspace_root, agent_type, state, current_run_id,
954
+ has_pending_approval, inbox_size, pending_dispatch_count, last_wake_at, last_sleep_at,
955
+ last_error, resumable, snapshot_at
956
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
957
+ ON CONFLICT(node_id, host_key) DO UPDATE SET
958
+ session_key = excluded.session_key,
959
+ workspace_root = excluded.workspace_root,
960
+ agent_type = excluded.agent_type,
961
+ state = excluded.state,
962
+ current_run_id = excluded.current_run_id,
963
+ has_pending_approval = excluded.has_pending_approval,
964
+ inbox_size = excluded.inbox_size,
965
+ pending_dispatch_count = excluded.pending_dispatch_count,
966
+ last_wake_at = excluded.last_wake_at,
967
+ last_sleep_at = excluded.last_sleep_at,
968
+ last_error = excluded.last_error,
969
+ resumable = excluded.resumable,
970
+ snapshot_at = excluded.snapshot_at`);
971
+ for (const host of params.hosts) {
972
+ upsert.run(params.nodeId, host.hostKey, host.sessionKey, host.workspaceRoot, host.agentType, host.state, host.currentRunId, host.hasPendingApproval ? 1 : 0, host.inboxSize, host.pendingDispatchCount, host.lastWakeAt, host.lastSleepAt, host.lastError, host.resumable ? 1 : 0, params.snapshotAt);
973
+ }
974
+ })();
975
+ }
976
+ function persistScenarioRuntimeSnapshot(db, params) {
977
+ db.prepare(`INSERT INTO node_runtime_snapshots(
978
+ node_id, workspace_root, terminal_backend_available, runtime_drivers_json, capabilities_json, snapshot_at
979
+ ) VALUES(?, ?, ?, ?, ?, ?)
980
+ ON CONFLICT(node_id) DO UPDATE SET
981
+ workspace_root = excluded.workspace_root,
982
+ terminal_backend_available = excluded.terminal_backend_available,
983
+ runtime_drivers_json = excluded.runtime_drivers_json,
984
+ capabilities_json = excluded.capabilities_json,
985
+ snapshot_at = excluded.snapshot_at`).run(params.nodeId, params.runtime.workspaceRoot, params.runtime.terminalBackendAvailable ? 1 : 0, JSON.stringify(params.runtime.runtimeDrivers), JSON.stringify(params.runtime.capabilities), params.snapshotAt);
986
+ }
987
+ function listAbsoluteDirectoryPage(absolutePath, cursor, rawLimit) {
988
+ const limit = normalizeDirectoryLimit(rawLimit);
989
+ const sorted = fs
990
+ .readdirSync(absolutePath, { withFileTypes: true })
991
+ .filter((entry) => !entry.isSymbolicLink())
992
+ .sort(compareDirents);
993
+ const offset = parseDirectoryCursor(cursor, sorted);
994
+ const pageDirents = sorted.slice(offset, offset + limit);
995
+ const entries = pageDirents
996
+ .map((entry) => {
997
+ const entryPath = path.join(absolutePath, entry.name);
998
+ const stat = fs.lstatSync(entryPath, { throwIfNoEntry: false });
999
+ if (!stat || stat.isSymbolicLink())
1000
+ return null;
1001
+ return {
1002
+ name: entry.name,
1003
+ path: entryPath,
1004
+ kind: stat.isDirectory() ? 'directory' : 'file',
1005
+ size: stat.isDirectory() ? null : stat.size,
1006
+ modifiedAt: stat.mtimeMs ? Math.floor(stat.mtimeMs) : null,
1007
+ };
1008
+ })
1009
+ .filter((entry) => entry != null);
1010
+ const nextOffset = offset + pageDirents.length;
1011
+ const hasMore = nextOffset < sorted.length;
1012
+ return {
1013
+ entries,
1014
+ directoryPage: {
1015
+ nextCursor: hasMore && pageDirents.length ? encodeDirectoryCursor(pageDirents[pageDirents.length - 1]) : null,
1016
+ hasMore,
1017
+ limit,
1018
+ },
1019
+ };
1020
+ }
1021
+ const DIRECTORY_CURSOR_PREFIX = 'k:';
1022
+ function classifyWorkspaceEntryCandidate(entry, absolutePath) {
1023
+ const isSymlink = entry.isSymbolicLink();
1024
+ if (isSymlink) {
1025
+ const targetStat = fs.statSync(absolutePath, { throwIfNoEntry: false });
1026
+ return {
1027
+ dirent: entry,
1028
+ absolutePath,
1029
+ kind: targetStat?.isDirectory() ? 'directory' : 'file',
1030
+ isSymlink,
1031
+ };
1032
+ }
1033
+ return {
1034
+ dirent: entry,
1035
+ absolutePath,
1036
+ kind: entry.isDirectory() ? 'directory' : 'file',
1037
+ isSymlink,
1038
+ };
1039
+ }
1040
+ function workspaceEntryFromCandidate(workspaceRoot, candidate) {
1041
+ const stat = fs.statSync(candidate.absolutePath, { throwIfNoEntry: false });
1042
+ return {
1043
+ name: candidate.dirent.name,
1044
+ path: toRelativeWorkspacePath(workspaceRoot, candidate.absolutePath),
1045
+ kind: candidate.kind,
1046
+ size: candidate.kind === 'directory' ? null : (stat?.size ?? null),
1047
+ modifiedAt: stat?.mtimeMs ? Math.floor(stat.mtimeMs) : null,
1048
+ ...(candidate.isSymlink ? { isSymlink: true } : {}),
1049
+ };
1050
+ }
1051
+ function paginateWorkspaceEntryCandidates(candidates, options) {
1052
+ const limit = normalizeWorkbenchDirectoryLimit(options.limit);
1053
+ const offset = parseWorkspaceEntryCursor(options.cursor, candidates);
1054
+ const pageCandidates = candidates.slice(offset, offset + limit);
1055
+ const nextOffset = offset + pageCandidates.length;
1056
+ const hasMore = nextOffset < candidates.length;
1057
+ return {
1058
+ candidates: pageCandidates,
1059
+ directoryPage: {
1060
+ nextCursor: hasMore && pageCandidates.length ? encodeWorkspaceEntryCursor(pageCandidates[pageCandidates.length - 1]) : null,
1061
+ hasMore,
1062
+ limit,
1063
+ },
1064
+ };
1065
+ }
1066
+ function compareDirents(left, right) {
1067
+ return compareDirectoryEntryKeys(direntKind(left), left.name, direntKind(right), right.name);
1068
+ }
1069
+ function direntKind(entry) {
1070
+ return entry.isDirectory() ? 'directory' : 'file';
1071
+ }
1072
+ function compareDirectoryEntryKeys(leftKind, leftName, rightKind, rightName) {
1073
+ if (leftKind !== rightKind)
1074
+ return leftKind === 'directory' ? -1 : 1;
1075
+ return leftName.localeCompare(rightName);
1076
+ }
1077
+ function encodeDirectoryCursor(entry) {
1078
+ const payload = { kind: direntKind(entry), name: entry.name };
1079
+ return `${DIRECTORY_CURSOR_PREFIX}${Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url')}`;
1080
+ }
1081
+ function normalizeDirectoryLimit(value) {
1082
+ if (typeof value !== 'number' || !Number.isFinite(value))
1083
+ return 100;
1084
+ return Math.min(200, Math.max(1, Math.floor(value)));
1085
+ }
1086
+ function normalizeWorkbenchDirectoryLimit(value) {
1087
+ if (typeof value !== 'number' || !Number.isFinite(value))
1088
+ return 500;
1089
+ return Math.min(1000, Math.max(1, Math.floor(value)));
1090
+ }
1091
+ function parseWorkspaceEntryCursor(value, candidates) {
1092
+ const decoded = decodeDirectoryCursor(value);
1093
+ if (!decoded)
1094
+ return 0;
1095
+ const index = candidates.findIndex((entry) => compareDirectoryEntryKeys(entry.kind, entry.dirent.name, decoded.kind, decoded.name) > 0);
1096
+ return index === -1 ? candidates.length : index;
1097
+ }
1098
+ function parseDirectoryCursor(value, entries) {
1099
+ if (!value)
1100
+ return 0;
1101
+ const decoded = decodeDirectoryCursor(value);
1102
+ if (decoded) {
1103
+ const index = entries.findIndex((entry) => compareDirectoryEntryKeys(direntKind(entry), entry.name, decoded.kind, decoded.name) > 0);
1104
+ return index === -1 ? entries.length : index;
1105
+ }
1106
+ const parsed = Number.parseInt(value, 10);
1107
+ if (!Number.isFinite(parsed) || parsed < 0)
1108
+ return 0;
1109
+ return parsed;
1110
+ }
1111
+ function encodeWorkspaceEntryCursor(entry) {
1112
+ const payload = { kind: entry.kind, name: entry.dirent.name };
1113
+ return `${DIRECTORY_CURSOR_PREFIX}${Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url')}`;
1114
+ }
1115
+ function decodeDirectoryCursor(value) {
1116
+ if (!value?.startsWith(DIRECTORY_CURSOR_PREFIX))
1117
+ return null;
1118
+ try {
1119
+ const parsed = JSON.parse(Buffer.from(value.slice(DIRECTORY_CURSOR_PREFIX.length), 'base64url').toString('utf8'));
1120
+ const cursorKind = parsed.kind;
1121
+ const cursorName = parsed.name;
1122
+ if ((cursorKind === 'directory' || cursorKind === 'file') && typeof cursorName === 'string') {
1123
+ return { kind: cursorKind, name: cursorName };
1124
+ }
1125
+ }
1126
+ catch {
1127
+ return null;
1128
+ }
1129
+ return null;
1130
+ }