@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,105 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ function buildRoleScaffoldLine(agentName) {
4
+ const normalizedAgentName = agentName
5
+ ?.replace(/[\r\n]+/g, ' ')
6
+ .replace(/\s+/g, ' ')
7
+ .trim();
8
+ if (!normalizedAgentName)
9
+ return '- Capture your current role and responsibilities here.';
10
+ return `- you are ${normalizedAgentName}. Capture your current role and responsibilities here.`;
11
+ }
12
+ const BEIJING_DATE_FORMATTER = new Intl.DateTimeFormat('en-CA', {
13
+ timeZone: 'Asia/Shanghai',
14
+ year: 'numeric',
15
+ month: '2-digit',
16
+ day: '2-digit',
17
+ });
18
+ function getCurrentBeijingISOWeek() {
19
+ const parts = BEIJING_DATE_FORMATTER.formatToParts(new Date());
20
+ const getPart = (type) => Number(parts.find((part) => part.type === type)?.value);
21
+ const year = getPart('year');
22
+ const month = getPart('month');
23
+ const day = getPart('day');
24
+ const beijingDate = new Date(Date.UTC(year, month - 1, day));
25
+ const dayNum = beijingDate.getUTCDay() || 7;
26
+ beijingDate.setUTCDate(beijingDate.getUTCDate() + 4 - dayNum);
27
+ const yearStart = new Date(Date.UTC(beijingDate.getUTCFullYear(), 0, 1));
28
+ const weekNo = Math.ceil((((beijingDate.getTime() - yearStart.getTime()) / 86_400_000) + 1) / 7);
29
+ return `${beijingDate.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
30
+ }
31
+ /** One-time migration: move legacy flat work-log to the weekly directory structure. */
32
+ export function migrateLocalWorkspaceLegacyFiles(workspaceRoot) {
33
+ const legacyWorkLogPath = path.join(workspaceRoot, 'notes', 'work-log.md');
34
+ const legacyDestPath = path.join(workspaceRoot, 'notes', 'work-log', 'legacy.md');
35
+ if (!fs.existsSync(legacyWorkLogPath))
36
+ return;
37
+ if (!fs.existsSync(legacyDestPath)) {
38
+ fs.renameSync(legacyWorkLogPath, legacyDestPath);
39
+ }
40
+ else {
41
+ const orphan = fs.readFileSync(legacyWorkLogPath, 'utf8').trim();
42
+ if (orphan) {
43
+ fs.appendFileSync(legacyDestPath, `\n\n<!-- re-migrated -->\n${orphan}\n`, 'utf8');
44
+ }
45
+ fs.rmSync(legacyWorkLogPath);
46
+ }
47
+ }
48
+ export function ensureLocalWorkspaceScaffold(workspaceRoot, agentName) {
49
+ fs.mkdirSync(workspaceRoot, { recursive: true });
50
+ fs.mkdirSync(path.join(workspaceRoot, 'notes'), { recursive: true });
51
+ fs.mkdirSync(path.join(workspaceRoot, 'notes', 'channels'), { recursive: true });
52
+ fs.mkdirSync(path.join(workspaceRoot, 'notes', 'work-log'), { recursive: true });
53
+ fs.mkdirSync(path.join(workspaceRoot, 'notes', 'archive'), { recursive: true });
54
+ migrateLocalWorkspaceLegacyFiles(workspaceRoot);
55
+ const taskNotesPath = path.join(workspaceRoot, 'notes', 'tasks.md');
56
+ if (!fs.existsSync(taskNotesPath)) {
57
+ fs.writeFileSync(taskNotesPath, [
58
+ '# Task Notes',
59
+ '',
60
+ 'Active tasks only (todo / in_progress / in_review). Remove a task here when it is done and archive it.',
61
+ '',
62
+ ].join('\n'), 'utf8');
63
+ }
64
+ const archiveIndexPath = path.join(workspaceRoot, 'notes', 'archive', 'INDEX.md');
65
+ if (!fs.existsSync(archiveIndexPath)) {
66
+ fs.writeFileSync(archiveIndexPath, [
67
+ '# Archive Index',
68
+ '',
69
+ 'One line per week: YYYY-Www: <task title> — <one-line result>',
70
+ '',
71
+ ].join('\n'), 'utf8');
72
+ }
73
+ const currentWeek = getCurrentBeijingISOWeek();
74
+ const currentWorkLogPath = path.join(workspaceRoot, 'notes', 'work-log', `${currentWeek}.md`);
75
+ if (!fs.existsSync(currentWorkLogPath)) {
76
+ fs.writeFileSync(currentWorkLogPath, [
77
+ `# Work Log ${currentWeek}`,
78
+ '',
79
+ 'Append key decisions, completed work, and follow-ups for this Beijing week here.',
80
+ '',
81
+ ].join('\n'), 'utf8');
82
+ }
83
+ const memoryPath = path.join(workspaceRoot, 'MEMORY.md');
84
+ if (!fs.existsSync(memoryPath)) {
85
+ fs.writeFileSync(memoryPath, [
86
+ '# Memory',
87
+ '',
88
+ '## Role',
89
+ buildRoleScaffoldLine(agentName),
90
+ '',
91
+ '## Key Knowledge',
92
+ '- Read `notes/user-preferences.md` for stable user preferences and conventions.',
93
+ '- Read `notes/channels/<name>.md` for each channel\'s current purpose and active context.',
94
+ '- Read `notes/tasks.md` for active tasks only (todo / in_progress / in_review).',
95
+ '- Weekly work log in `notes/work-log/`. Read the current week\'s file for recent decisions; if `notes/work-log/legacy.md` exists, scan it for pre-migration history when older decisions may matter.',
96
+ '- Completed task archive in `notes/archive/`. Scan `notes/archive/INDEX.md` to locate a specific week.',
97
+ '- Shared documents, when available, are mounted at `.library/shared`. Before modifying them, read `.library/shared/README.txt`.',
98
+ '- Add topic-specific knowledge under `notes/<topic>.md` as needed.',
99
+ '',
100
+ '## Active Context',
101
+ '- First startup.',
102
+ '',
103
+ ].join('\n'), 'utf8');
104
+ }
105
+ }
@@ -0,0 +1,576 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getRuntimeDriver } from '@bbigbang/protocol';
3
+ import { createRun, finishRun, log } from '@bbigbang/runtime-acp';
4
+ import { WorkspaceSessionServiceError, } from './workspaceSessionService.js';
5
+ import { getWorkspaceRunDispatchNodeId, getWorkspaceSessionOwnerNodeId, persistWorkspaceRunDispatchContext, } from './workspaceRunOwnership.js';
6
+ const WORKSPACE_APPROVAL_RESPONSE_SENT_METHOD = 'approval/response.sent';
7
+ const WORKSPACE_APPROVAL_PROGRESS_TYPES_SQL = [
8
+ "'content.delta'",
9
+ "'thinking.delta'",
10
+ "'tool.call'",
11
+ "'tool.result'",
12
+ "'plan.update'",
13
+ "'plan.phase'",
14
+ "'task.update'",
15
+ "'runtime.compact'",
16
+ ].join(', ');
17
+ export class WorkspaceSessionRuntimeService {
18
+ db;
19
+ nodeRegistry;
20
+ workspaceSessionService;
21
+ nodeDispatchAckTimeoutMs;
22
+ pendingDispatchAcceptances = new Map();
23
+ constructor(params) {
24
+ this.db = params.db;
25
+ this.nodeRegistry = params.nodeRegistry;
26
+ this.workspaceSessionService = params.workspaceSessionService;
27
+ this.nodeDispatchAckTimeoutMs = params.nodeDispatchAckTimeoutMs;
28
+ }
29
+ async submitPrompt(params) {
30
+ const promptText = normalizePromptText(params.text);
31
+ const context = this.workspaceSessionService.getRuntimeContext({
32
+ workspaceSessionId: params.workspaceSessionId,
33
+ user: params.user,
34
+ canAccessAgent: params.canAccessAgent,
35
+ });
36
+ if (context.session.archivedAt != null) {
37
+ throw new WorkspaceSessionServiceError(409, 'Archived workspace sessions cannot dispatch prompts.');
38
+ }
39
+ const nodeId = getWorkspaceSessionOwnerNodeId(this.db, context.sessionKey)
40
+ ?? context.root.nodeId
41
+ ?? context.agent.nodeId
42
+ ?? null;
43
+ if (!nodeId) {
44
+ throw new WorkspaceSessionServiceError(409, 'No agent node assigned for this workspace session.');
45
+ }
46
+ const runId = randomUUID();
47
+ const hostKey = `workspace_session:${context.session.workspaceSessionId}:${context.agent.agentType}`;
48
+ const dispatchMode = this.getDispatchMode(context.sessionKey);
49
+ const queued = this.hasOpenRun(context.sessionKey);
50
+ const driver = getRuntimeDriver(context.agent.agentType);
51
+ const runtimeOverrides = normalizePromptRuntimeOverrides(params.runtimeOverrides);
52
+ const systemPromptText = buildWorkspaceSessionSystemPrompt(context.agent, context.root.rootPath);
53
+ const contextText = buildWorkspaceSessionContextText(context.session.workspaceSessionId, context.root.rootPath);
54
+ updateSessionCwd(this.db, context.sessionKey, context.root.rootPath);
55
+ createRun(this.db, { runId, sessionKey: context.sessionKey, promptText });
56
+ persistWorkspaceRunDispatchContext(this.db, runId, {
57
+ nodeId,
58
+ workspaceSessionId: context.session.workspaceSessionId,
59
+ agentId: context.agent.agentId,
60
+ workspaceRootId: context.session.workspaceRootId,
61
+ });
62
+ const session = this.workspaceSessionService.updateSessionStatus(context.session.workspaceSessionId, this.getStatusAfterDispatchStart(context.sessionKey, queued), { touchLastActive: true }) ?? context.session;
63
+ const node = this.nodeRegistry.getNode(nodeId);
64
+ if (!node) {
65
+ this.finishDispatchFailure({
66
+ runId,
67
+ error: 'Node not connected',
68
+ workspaceSessionId: context.session.workspaceSessionId,
69
+ sessionKey: context.sessionKey,
70
+ previousStatus: context.session.status,
71
+ });
72
+ log.warn('[workspace-session-runtime] node not connected', {
73
+ nodeId,
74
+ workspaceSessionId: context.session.workspaceSessionId,
75
+ runId,
76
+ });
77
+ throw new WorkspaceSessionServiceError(409, `Node not connected: ${nodeId}`);
78
+ }
79
+ if (!node.agentTypes.includes(context.agent.agentType)) {
80
+ const errorMessage = `Node does not support agent type ${context.agent.agentType}`;
81
+ this.finishDispatchFailure({
82
+ runId,
83
+ error: errorMessage,
84
+ workspaceSessionId: context.session.workspaceSessionId,
85
+ sessionKey: context.sessionKey,
86
+ previousStatus: context.session.status,
87
+ });
88
+ throw new WorkspaceSessionServiceError(409, `${errorMessage}: ${nodeId}`);
89
+ }
90
+ const acceptance = this.waitForDispatchAcceptance(runId, nodeId, context.session.workspaceSessionId, context.sessionKey);
91
+ const dispatchSent = this.nodeRegistry.send(nodeId, {
92
+ type: 'run.dispatch',
93
+ runId,
94
+ conversationId: context.session.workspaceSessionId,
95
+ agentType: context.agent.agentType,
96
+ ...(context.agent.model ? { model: context.agent.model } : {}),
97
+ ...(context.agent.reasoningEffort ? { reasoningEffort: context.agent.reasoningEffort } : {}),
98
+ ...(context.agent.agentType === 'codex_app_server' && runtimeOverrides?.codexMode ? { codexMode: runtimeOverrides.codexMode } : {}),
99
+ ...(context.agent.agentType === 'codex_app_server' && context.agent.codexServiceTier ? { codexServiceTier: context.agent.codexServiceTier } : {}),
100
+ workspacePath: context.root.rootPath,
101
+ agentName: context.agent.name,
102
+ ...(context.agent.skillRoots ? { skillRoots: context.agent.skillRoots } : {}),
103
+ envVars: {
104
+ ...(context.agent.envVars ?? {}),
105
+ ...(driver.defaultEnv ?? {}),
106
+ },
107
+ disabledToolKinds: context.agent.disabledToolKinds,
108
+ prompt: promptText,
109
+ ...(runtimeOverrides ? { runtimeOverrides } : {}),
110
+ sessionKey: context.sessionKey,
111
+ hostKey,
112
+ dispatchMode,
113
+ systemPromptText,
114
+ contextText,
115
+ });
116
+ if (!dispatchSent) {
117
+ this.clearPendingDispatchAcceptance(runId);
118
+ this.finishDispatchFailure({
119
+ runId,
120
+ error: 'Node disconnected during dispatch',
121
+ workspaceSessionId: context.session.workspaceSessionId,
122
+ sessionKey: context.sessionKey,
123
+ previousStatus: context.session.status,
124
+ });
125
+ throw new WorkspaceSessionServiceError(409, `Node disconnected: ${nodeId}`);
126
+ }
127
+ try {
128
+ await acceptance;
129
+ }
130
+ catch (error) {
131
+ const message = String(error?.message ?? error);
132
+ this.finishDispatchFailure({
133
+ runId,
134
+ error: message,
135
+ workspaceSessionId: context.session.workspaceSessionId,
136
+ sessionKey: context.sessionKey,
137
+ previousStatus: context.session.status,
138
+ });
139
+ throw new WorkspaceSessionServiceError(409, message);
140
+ }
141
+ log.info('[workspace-session-runtime] dispatching prompt', {
142
+ nodeId,
143
+ workspaceSessionId: context.session.workspaceSessionId,
144
+ runId,
145
+ dispatchMode,
146
+ hostKey,
147
+ });
148
+ return {
149
+ queued,
150
+ runId,
151
+ dispatchMode,
152
+ session: this.workspaceSessionService.updateSessionStatus(context.session.workspaceSessionId, this.hasOpenPendingApproval(context.sessionKey) ? 'awaiting_approval' : 'active', { touchLastActive: true }) ?? session,
153
+ };
154
+ }
155
+ respondToApproval(params) {
156
+ const context = this.workspaceSessionService.getRuntimeContext({
157
+ workspaceSessionId: params.workspaceSessionId,
158
+ user: params.user,
159
+ canAccessAgent: params.canAccessAgent,
160
+ });
161
+ if (context.session.archivedAt != null) {
162
+ throw new WorkspaceSessionServiceError(409, 'Archived workspace sessions cannot answer approvals.');
163
+ }
164
+ const requestId = normalizeRequiredString(params.request?.requestId, 'Approval request id is required.');
165
+ const decision = normalizeApprovalDecision(params.request?.decision);
166
+ const selectedActionId = normalizeOptionalString(params.request?.selectedActionId);
167
+ const responseText = normalizeOptionalString(params.request?.responseText);
168
+ const runId = findPendingApprovalRunId(this.db, context.sessionKey, requestId);
169
+ if (!runId) {
170
+ throw new WorkspaceSessionServiceError(409, 'No pending approval request found for this workspace session.');
171
+ }
172
+ const nodeId = getWorkspaceRunDispatchNodeId(this.db, runId);
173
+ if (!nodeId) {
174
+ throw new WorkspaceSessionServiceError(409, 'Workspace run owner is not recorded for this approval request.');
175
+ }
176
+ const sent = this.nodeRegistry.send(nodeId, {
177
+ type: 'permission.response',
178
+ requestId,
179
+ decision,
180
+ ...(selectedActionId ? { selectedActionId } : {}),
181
+ ...(responseText ? { responseText } : {}),
182
+ });
183
+ if (!sent) {
184
+ throw new WorkspaceSessionServiceError(409, `Node not connected: ${nodeId}`);
185
+ }
186
+ persistApprovalResponseSentMarker(this.db, runId, requestId, decision, selectedActionId, responseText);
187
+ const session = this.workspaceSessionService.updateSessionStatus(context.session.workspaceSessionId, this.hasOpenPendingApproval(context.sessionKey) ? 'awaiting_approval' : 'active', { touchLastActive: true }) ?? context.session;
188
+ return { ok: true, session };
189
+ }
190
+ cancelCurrentRun(params) {
191
+ const context = this.workspaceSessionService.getRuntimeContext({
192
+ workspaceSessionId: params.workspaceSessionId,
193
+ user: params.user,
194
+ canAccessAgent: params.canAccessAgent,
195
+ });
196
+ if (context.session.archivedAt != null) {
197
+ throw new WorkspaceSessionServiceError(409, 'Archived workspace sessions cannot cancel runs.');
198
+ }
199
+ const runId = this.getCurrentOpenRunId(context.sessionKey);
200
+ if (!runId) {
201
+ throw new WorkspaceSessionServiceError(409, 'No active workspace run to cancel.');
202
+ }
203
+ const nodeId = getWorkspaceRunDispatchNodeId(this.db, runId);
204
+ if (!nodeId) {
205
+ throw new WorkspaceSessionServiceError(409, 'Workspace run owner is not recorded for this run.');
206
+ }
207
+ const sent = this.nodeRegistry.send(nodeId, {
208
+ type: 'run.cancel',
209
+ runId,
210
+ });
211
+ if (!sent) {
212
+ throw new WorkspaceSessionServiceError(409, `Node not connected: ${nodeId}`);
213
+ }
214
+ const session = this.workspaceSessionService.updateSessionStatus(context.session.workspaceSessionId, context.session.status, { touchLastActive: true }) ?? context.session;
215
+ return { ok: true, runId, session };
216
+ }
217
+ listEvents(params) {
218
+ const context = this.workspaceSessionService.getRuntimeContext({
219
+ workspaceSessionId: params.workspaceSessionId,
220
+ user: params.user,
221
+ canAccessAgent: params.canAccessAgent,
222
+ });
223
+ return {
224
+ session: context.session,
225
+ events: listWorkspaceSessionEvents(this.db, context.sessionKey),
226
+ };
227
+ }
228
+ handleRunAccepted(runId) {
229
+ const pending = this.pendingDispatchAcceptances.get(runId);
230
+ if (!pending)
231
+ return false;
232
+ clearTimeout(pending.timer);
233
+ this.pendingDispatchAcceptances.delete(runId);
234
+ this.workspaceSessionService.updateSessionStatus(pending.workspaceSessionId, this.hasOpenPendingApproval(pending.sessionKey) ? 'awaiting_approval' : 'active', { touchLastActive: true });
235
+ pending.resolve();
236
+ return true;
237
+ }
238
+ rejectPendingDispatchesForNode(nodeId, errorMessage) {
239
+ for (const [runId, pending] of this.pendingDispatchAcceptances.entries()) {
240
+ if (pending.nodeId !== nodeId)
241
+ continue;
242
+ clearTimeout(pending.timer);
243
+ pending.reject(new Error(errorMessage));
244
+ this.pendingDispatchAcceptances.delete(runId);
245
+ }
246
+ }
247
+ getDispatchMode(sessionKey) {
248
+ const row = this.db.prepare('SELECT acp_session_id as acpSessionId FROM sessions WHERE session_key = ?').get(sessionKey);
249
+ return row?.acpSessionId?.trim() ? 'resume' : 'cold_start';
250
+ }
251
+ hasOpenRun(sessionKey) {
252
+ const row = this.db.prepare('SELECT 1 as present FROM runs WHERE session_key = ? AND ended_at IS NULL LIMIT 1').get(sessionKey);
253
+ return Boolean(row);
254
+ }
255
+ getCurrentOpenRunId(sessionKey) {
256
+ const row = this.db.prepare(`SELECT run_id as runId
257
+ FROM runs
258
+ WHERE session_key = ?
259
+ AND ended_at IS NULL
260
+ ORDER BY started_at ASC, rowid ASC
261
+ LIMIT 1`).get(sessionKey);
262
+ return row?.runId ?? null;
263
+ }
264
+ waitForDispatchAcceptance(runId, nodeId, workspaceSessionId, sessionKey) {
265
+ return new Promise((resolve, reject) => {
266
+ const timer = setTimeout(() => {
267
+ this.pendingDispatchAcceptances.delete(runId);
268
+ reject(new Error('Node did not acknowledge workspace dispatch in time'));
269
+ }, this.nodeDispatchAckTimeoutMs);
270
+ this.pendingDispatchAcceptances.set(runId, {
271
+ nodeId,
272
+ workspaceSessionId,
273
+ sessionKey,
274
+ timer,
275
+ resolve,
276
+ reject,
277
+ });
278
+ });
279
+ }
280
+ clearPendingDispatchAcceptance(runId) {
281
+ const pending = this.pendingDispatchAcceptances.get(runId);
282
+ if (!pending)
283
+ return;
284
+ clearTimeout(pending.timer);
285
+ this.pendingDispatchAcceptances.delete(runId);
286
+ }
287
+ finishDispatchFailure(params) {
288
+ if (this.isRunEnded(params.runId))
289
+ return;
290
+ finishRun(this.db, { runId: params.runId, error: params.error });
291
+ this.restoreStatusAfterDispatchFailure(params.workspaceSessionId, params.sessionKey, params.previousStatus);
292
+ }
293
+ isRunEnded(runId) {
294
+ const row = this.db.prepare('SELECT ended_at as endedAt FROM runs WHERE run_id = ?').get(runId);
295
+ return row?.endedAt != null;
296
+ }
297
+ restoreStatusAfterDispatchFailure(workspaceSessionId, sessionKey, previousStatus) {
298
+ const hasRemainingOpenRun = this.hasOpenRun(sessionKey);
299
+ const hasPendingApproval = this.hasOpenPendingApproval(sessionKey);
300
+ const status = hasRemainingOpenRun
301
+ ? hasPendingApproval
302
+ ? 'awaiting_approval'
303
+ : previousStatus === 'idle' || previousStatus === 'failed'
304
+ ? 'active'
305
+ : previousStatus
306
+ : 'failed';
307
+ this.workspaceSessionService.updateSessionStatus(workspaceSessionId, status, { touchLastActive: true });
308
+ }
309
+ getStatusAfterDispatchStart(sessionKey, queued) {
310
+ if (this.hasOpenPendingApproval(sessionKey))
311
+ return 'awaiting_approval';
312
+ return queued ? 'queued' : 'active';
313
+ }
314
+ hasOpenPendingApproval(sessionKey) {
315
+ return hasOpenPendingWorkspaceApproval(this.db, sessionKey);
316
+ }
317
+ }
318
+ export function hasOpenPendingWorkspaceApproval(db, sessionKey) {
319
+ const row = db.prepare(`SELECT 1 as present
320
+ FROM runs r
321
+ WHERE r.session_key = ?
322
+ AND r.ended_at IS NULL
323
+ AND EXISTS (
324
+ SELECT 1
325
+ FROM events approval
326
+ WHERE approval.run_id = r.run_id
327
+ AND approval.method = 'node/event'
328
+ AND json_valid(approval.payload_json)
329
+ AND json_extract(approval.payload_json, '$.type') = 'approval.request'
330
+ AND NOT EXISTS (
331
+ SELECT 1
332
+ FROM events response
333
+ WHERE response.run_id = approval.run_id
334
+ AND response.seq > approval.seq
335
+ AND json_valid(response.payload_json)
336
+ AND (
337
+ (
338
+ response.method = 'approval/response'
339
+ AND json_extract(response.payload_json, '$.type') = 'approval.response'
340
+ AND json_extract(response.payload_json, '$.requestId') = json_extract(approval.payload_json, '$.requestId')
341
+ )
342
+ OR (
343
+ response.method = 'node/event'
344
+ AND json_extract(response.payload_json, '$.type') IN (${WORKSPACE_APPROVAL_PROGRESS_TYPES_SQL})
345
+ )
346
+ )
347
+ )
348
+ )
349
+ LIMIT 1`).get(sessionKey);
350
+ return Boolean(row);
351
+ }
352
+ export function listWorkspaceSessionEvents(db, sessionKey) {
353
+ const runs = db.prepare(`SELECT run_id as runId,
354
+ prompt_text as promptText,
355
+ started_at as startedAt,
356
+ ended_at as endedAt,
357
+ stop_reason as stopReason,
358
+ error
359
+ FROM runs
360
+ WHERE session_key = ?
361
+ ORDER BY started_at ASC, run_id ASC`).all(sessionKey);
362
+ const entries = [];
363
+ for (const run of runs) {
364
+ entries.push({
365
+ runId: run.runId,
366
+ seq: 0,
367
+ createdAt: run.startedAt,
368
+ event: {
369
+ type: 'turn.begin',
370
+ turnId: run.runId,
371
+ startedAt: run.startedAt,
372
+ promptText: run.promptText,
373
+ },
374
+ });
375
+ const rows = db.prepare(`SELECT seq,
376
+ payload_json as payloadJson,
377
+ created_at as createdAt
378
+ FROM events
379
+ WHERE run_id = ?
380
+ AND method IN ('node/event', 'approval/response')
381
+ ORDER BY seq ASC`).all(run.runId);
382
+ let maxSeq = 0;
383
+ for (const row of rows) {
384
+ maxSeq = Math.max(maxSeq, row.seq);
385
+ const event = parseServerEvent(row.payloadJson);
386
+ if (!event)
387
+ continue;
388
+ entries.push({
389
+ runId: run.runId,
390
+ seq: row.seq,
391
+ createdAt: row.createdAt,
392
+ event,
393
+ });
394
+ }
395
+ if (run.endedAt != null) {
396
+ entries.push({
397
+ runId: run.runId,
398
+ seq: maxSeq + 1,
399
+ createdAt: run.endedAt,
400
+ event: {
401
+ type: 'turn.end',
402
+ turnId: run.runId,
403
+ endedAt: run.endedAt,
404
+ ...(run.stopReason ? { stopReason: run.stopReason } : {}),
405
+ ...(run.error ? { error: run.error } : {}),
406
+ },
407
+ });
408
+ }
409
+ }
410
+ return entries.sort((left, right) => left.createdAt - right.createdAt || left.seq - right.seq);
411
+ }
412
+ function normalizePromptText(value) {
413
+ if (typeof value !== 'string') {
414
+ throw new WorkspaceSessionServiceError(400, 'Prompt text is required.');
415
+ }
416
+ const trimmed = value.trim();
417
+ if (!trimmed)
418
+ throw new WorkspaceSessionServiceError(400, 'Prompt text is required.');
419
+ return trimmed;
420
+ }
421
+ function normalizePromptRuntimeOverrides(value) {
422
+ if (!value)
423
+ return undefined;
424
+ const codexMode = value.codexMode === 'default' || value.codexMode === 'plan'
425
+ ? value.codexMode
426
+ : null;
427
+ return codexMode ? { codexMode } : undefined;
428
+ }
429
+ function normalizeRequiredString(value, message) {
430
+ if (typeof value !== 'string')
431
+ throw new WorkspaceSessionServiceError(400, message);
432
+ const trimmed = value.trim();
433
+ if (!trimmed)
434
+ throw new WorkspaceSessionServiceError(400, message);
435
+ return trimmed;
436
+ }
437
+ function normalizeOptionalString(value) {
438
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
439
+ }
440
+ function normalizeApprovalDecision(value) {
441
+ if (value === 'allow' || value === 'deny')
442
+ return value;
443
+ throw new WorkspaceSessionServiceError(400, 'Approval decision must be allow or deny.');
444
+ }
445
+ function buildWorkspaceSessionSystemPrompt(agent, workspacePath) {
446
+ return [
447
+ `You are "${agent.name}", an AI agent in Bigbang Workspace.`,
448
+ '',
449
+ 'You are running inside the Workspace surface, not a Chat conversation.',
450
+ 'Use normal assistant output for user-visible replies in this Workspace surface. Do not use Bigbang chat command surfaces here.',
451
+ `Your current workspace root is: ${workspacePath}`,
452
+ 'Use regular filesystem and terminal tools for work in this root. Maintain MEMORY.md and notes/*.md when durable knowledge should persist.',
453
+ agent.systemPrompt?.trim() ? `\nInitial role:\n${agent.systemPrompt.trim()}` : '',
454
+ agent.description?.trim() ? `\nAgent description:\n${agent.description.trim()}` : '',
455
+ ].filter(Boolean).join('\n');
456
+ }
457
+ function buildWorkspaceSessionContextText(workspaceSessionId, workspacePath) {
458
+ return [
459
+ '[Workspace session]',
460
+ `workspace_session_id: ${workspaceSessionId}`,
461
+ `workspace_root: ${workspacePath}`,
462
+ 'surface: Workspace',
463
+ ].join('\n');
464
+ }
465
+ function updateSessionCwd(db, sessionKey, cwd) {
466
+ db.prepare('UPDATE sessions SET cwd = ?, updated_at = ? WHERE session_key = ?').run(cwd, Date.now(), sessionKey);
467
+ }
468
+ function findPendingApprovalRunId(db, sessionKey, requestId) {
469
+ const row = db.prepare(`SELECT r.run_id as runId
470
+ FROM runs r
471
+ WHERE r.session_key = ?
472
+ AND r.ended_at IS NULL
473
+ AND EXISTS (
474
+ SELECT 1
475
+ FROM events approval
476
+ WHERE approval.run_id = r.run_id
477
+ AND approval.method = 'node/event'
478
+ AND json_valid(approval.payload_json)
479
+ AND json_extract(approval.payload_json, '$.type') = 'approval.request'
480
+ AND json_extract(approval.payload_json, '$.requestId') = ?
481
+ AND NOT EXISTS (
482
+ SELECT 1
483
+ FROM events later
484
+ WHERE later.run_id = approval.run_id
485
+ AND later.seq > approval.seq
486
+ AND json_valid(later.payload_json)
487
+ AND (
488
+ (
489
+ later.method = 'approval/response'
490
+ AND json_extract(later.payload_json, '$.type') = 'approval.response'
491
+ AND json_extract(later.payload_json, '$.requestId') = ?
492
+ )
493
+ OR (
494
+ later.method = '${WORKSPACE_APPROVAL_RESPONSE_SENT_METHOD}'
495
+ AND json_extract(later.payload_json, '$.type') = 'approval.response'
496
+ AND json_extract(later.payload_json, '$.requestId') = ?
497
+ )
498
+ OR (
499
+ later.method = 'node/event'
500
+ AND json_extract(later.payload_json, '$.type') IN (${WORKSPACE_APPROVAL_PROGRESS_TYPES_SQL})
501
+ )
502
+ )
503
+ )
504
+ )
505
+ ORDER BY r.started_at DESC
506
+ LIMIT 1`).get(sessionKey, requestId, requestId, requestId);
507
+ return row?.runId ?? null;
508
+ }
509
+ export function isWorkspaceApprovalProgressEvent(event) {
510
+ return event.type === 'content.delta'
511
+ || event.type === 'thinking.delta'
512
+ || event.type === 'tool.call'
513
+ || event.type === 'tool.result'
514
+ || event.type === 'plan.update'
515
+ || event.type === 'plan.phase'
516
+ || event.type === 'task.update'
517
+ || event.type === 'runtime.compact';
518
+ }
519
+ export function promoteWorkspaceSentApprovalResponses(db, runId) {
520
+ const rows = db.prepare(`SELECT sent.payload_json as payloadJson
521
+ FROM events sent
522
+ JOIN events approval
523
+ ON approval.run_id = sent.run_id
524
+ AND approval.method = 'node/event'
525
+ AND json_valid(approval.payload_json)
526
+ AND json_extract(approval.payload_json, '$.type') = 'approval.request'
527
+ AND json_extract(approval.payload_json, '$.requestId') = json_extract(sent.payload_json, '$.requestId')
528
+ AND sent.seq > approval.seq
529
+ WHERE sent.run_id = ?
530
+ AND sent.method = '${WORKSPACE_APPROVAL_RESPONSE_SENT_METHOD}'
531
+ AND json_valid(sent.payload_json)
532
+ AND json_extract(sent.payload_json, '$.type') = 'approval.response'
533
+ AND NOT EXISTS (
534
+ SELECT 1
535
+ FROM events response
536
+ WHERE response.run_id = sent.run_id
537
+ AND response.seq > approval.seq
538
+ AND response.method = 'approval/response'
539
+ AND json_valid(response.payload_json)
540
+ AND json_extract(response.payload_json, '$.type') = 'approval.response'
541
+ AND json_extract(response.payload_json, '$.requestId') = json_extract(sent.payload_json, '$.requestId')
542
+ )
543
+ ORDER BY sent.seq ASC`).all(runId);
544
+ for (const row of rows) {
545
+ const seqRow = db.prepare(`SELECT COALESCE(MAX(seq), 0) + 1 as nextSeq
546
+ FROM events
547
+ WHERE run_id = ?
548
+ AND seq > 0`).get(runId);
549
+ db.prepare(`INSERT OR IGNORE INTO events(run_id, seq, method, payload_json, created_at)
550
+ VALUES(?, ?, 'approval/response', ?, ?)`).run(runId, seqRow?.nextSeq ?? 1, row.payloadJson, Date.now());
551
+ }
552
+ }
553
+ function persistApprovalResponseSentMarker(db, runId, requestId, decision, selectedActionId, responseText) {
554
+ const seqRow = db.prepare(`SELECT COALESCE(MAX(seq), 0) + 1 as nextSeq
555
+ FROM events
556
+ WHERE run_id = ?
557
+ AND seq > 0`).get(runId);
558
+ const payload = {
559
+ type: 'approval.response',
560
+ requestId,
561
+ decision,
562
+ ...(selectedActionId ? { selectedActionId } : {}),
563
+ ...(responseText ? { responseText } : {}),
564
+ };
565
+ db.prepare(`INSERT OR IGNORE INTO events(run_id, seq, method, payload_json, created_at)
566
+ VALUES(?, ?, '${WORKSPACE_APPROVAL_RESPONSE_SENT_METHOD}', ?, ?)`).run(runId, seqRow?.nextSeq ?? 1, JSON.stringify(payload), Date.now());
567
+ }
568
+ function parseServerEvent(value) {
569
+ try {
570
+ const parsed = JSON.parse(value);
571
+ return parsed && typeof parsed === 'object' && typeof parsed.type === 'string' ? parsed : null;
572
+ }
573
+ catch {
574
+ return null;
575
+ }
576
+ }