@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,143 @@
1
+ import { channelMemoryNotePath } from './channelMemoryNotes.js';
2
+ const BEIJING_DATE_FORMATTER = new Intl.DateTimeFormat('en-CA', {
3
+ timeZone: 'Asia/Shanghai',
4
+ year: 'numeric',
5
+ month: '2-digit',
6
+ day: '2-digit',
7
+ });
8
+ function getBeijingDateParts(input) {
9
+ const parts = BEIJING_DATE_FORMATTER.formatToParts(input);
10
+ const getPart = (type) => Number(parts.find((part) => part.type === type)?.value);
11
+ const year = getPart('year');
12
+ const month = getPart('month');
13
+ const day = getPart('day');
14
+ const date = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
15
+ return { year, month, day, date };
16
+ }
17
+ /** Returns ISO 8601 week string and Beijing date string for the current moment. */
18
+ export function getCurrentISOWeek() {
19
+ const today = getBeijingDateParts(new Date());
20
+ const beijingDate = new Date(Date.UTC(today.year, today.month - 1, today.day));
21
+ // Shift to Thursday of the ISO week to determine the week's year and number
22
+ const d = new Date(beijingDate);
23
+ const dayNum = d.getUTCDay() || 7;
24
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
25
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
26
+ const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86_400_000) + 1) / 7);
27
+ const week = `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
28
+ return { week, date: today.date };
29
+ }
30
+ export function isThreadTarget(target) {
31
+ if (target.startsWith('dm:@')) {
32
+ return target.split(':').length >= 3;
33
+ }
34
+ if (target.startsWith('#')) {
35
+ return target.split(':').length >= 2;
36
+ }
37
+ return false;
38
+ }
39
+ export function buildWorkspaceMemoryHints(params) {
40
+ const hints = ['MEMORY.md'];
41
+ if (params.channelName && params.includeChannelNote !== false) {
42
+ hints.push(channelMemoryNotePath(params.channelName));
43
+ }
44
+ if (params.includeTaskNotes) {
45
+ hints.push('notes/tasks.md');
46
+ }
47
+ if (params.includeWorkLog) {
48
+ hints.push(params.currentWeek ? `notes/work-log/${params.currentWeek}.md` : 'notes/work-log/');
49
+ }
50
+ return Array.from(new Set(hints));
51
+ }
52
+ export function resolveChannelTaskBoardPresencePolicy(params) {
53
+ const threadSurface = isThreadTarget(params.target);
54
+ if (params.includeOpenTasks) {
55
+ return { activationMode: 'full', renderMode: 'full' };
56
+ }
57
+ return {
58
+ activationMode: threadSurface ? 'off' : 'count',
59
+ renderMode: params.hasBoundTask || threadSurface ? 'off' : 'hint',
60
+ };
61
+ }
62
+ export function resolveDirectTaskThreadPresencePolicy(params) {
63
+ const threadSurface = isThreadTarget(params.target);
64
+ if (params.includeDmActiveTaskThreads) {
65
+ return { activationMode: 'full', renderMode: 'full' };
66
+ }
67
+ return {
68
+ activationMode: threadSurface ? 'off' : 'count',
69
+ renderMode: threadSurface ? 'off' : 'hint',
70
+ };
71
+ }
72
+ export function buildDefaultChannelMemoryHints(params) {
73
+ return buildWorkspaceMemoryHints({
74
+ channelName: params.channelName,
75
+ includeTaskNotes: Boolean(params.forceTaskNotes || params.hasBoundTask),
76
+ includeWorkLog: Boolean(params.forceWorkLog),
77
+ currentWeek: params.currentWeek,
78
+ });
79
+ }
80
+ export function buildDefaultDirectMemoryHints(params) {
81
+ return buildWorkspaceMemoryHints({
82
+ includeChannelNote: false,
83
+ includeTaskNotes: Boolean(params.forceTaskNotes || params.threadRootId),
84
+ includeWorkLog: Boolean(params.forceWorkLog),
85
+ currentWeek: params.currentWeek,
86
+ });
87
+ }
88
+ export function buildWorkspaceMemoryHintSection(memoryHints, today) {
89
+ const hints = (memoryHints ?? []).map((hint) => hint.trim()).filter(Boolean);
90
+ if (hints.length === 0)
91
+ return '';
92
+ return [
93
+ '[Workspace memory to check if you need it]',
94
+ ...(today ? [`Today: ${today.week} (${today.date} Beijing / UTC+8)`] : []),
95
+ ...hints,
96
+ ].join('\n');
97
+ }
98
+ export function buildWorkspaceMemoryReminder(memoryHints, today) {
99
+ const hints = (memoryHints ?? []).map((hint) => hint.trim()).filter(Boolean);
100
+ const summary = hints.length > 0 ? hints.join(', ') : 'MEMORY.md';
101
+ const todayLine = today ? `Today: ${today.week} (${today.date} Beijing / UTC+8)\n` : '';
102
+ return `${todayLine}This work spans multiple turns. Check workspace memory if you need it (${summary}) before deeper history lookup.`;
103
+ }
104
+ export function buildActiveProjectSection(params) {
105
+ if (!params)
106
+ return '';
107
+ return [
108
+ '[Active project]',
109
+ `name: ${params.name}`,
110
+ `backend: ${params.backendType}`,
111
+ `root: ${params.rootPath}`,
112
+ params.backendType === 'shared_mount'
113
+ ? 'This project root may be shared across multiple machines; use workspace/git/terminal actions against this project root.'
114
+ : 'This is a node-local project root attached to the current conversation context.',
115
+ ].join('\n');
116
+ }
117
+ export function combinePromptSections(parts) {
118
+ return parts
119
+ .map((part) => part?.trim() ?? '')
120
+ .filter(Boolean)
121
+ .join('\n\n');
122
+ }
123
+ export function shouldIncludeParticipantsInActivationContext(params) {
124
+ if (params.hasBoundTask)
125
+ return true;
126
+ if (isThreadTarget(params.target))
127
+ return true;
128
+ if (params.reason === 'agent_mention')
129
+ return true;
130
+ return (params.participantsCount ?? 0) > 1;
131
+ }
132
+ export function shouldIncludeOpenTaskBoardSummary(params) {
133
+ void params;
134
+ // Root-surface task context is now carried as a light presence hint by default.
135
+ // Keep the detailed board summary opt-in only.
136
+ return false;
137
+ }
138
+ export function shouldIncludeDmContextSnapshot(params) {
139
+ void params.target;
140
+ // The full DM snapshot is no longer a default activation payload. Only explicit
141
+ // recovery flows should opt back into it.
142
+ return Boolean(params.includeRecoverySnapshot);
143
+ }
@@ -0,0 +1,30 @@
1
+ export function buildWorkspaceToolGuidancePromptSection(options) {
2
+ if (options?.layoutPlan) {
3
+ return [
4
+ '[Workspace tool guidance]',
5
+ 'Workspace-tool layout planning mode is enabled for this chat conversation.',
6
+ 'Before designing the tool, run bigbang panel components and use the mounted workspace-tool-builder skill to define the action/status/view contract, including any explicit view.summary and view.parameterForm layout.',
7
+ 'Do not write the tool bundle or run bigbang tool publish in this run. Send a compact chat wireframe and wait for the user to confirm in a later turn.',
8
+ 'The wireframe must cover: tool purpose, actions and execution modes, status data source, fields/filter/sort labels, view.summary overview, RowTemplateGrid Columns layout for the visible view, parameterForm layout for grouped controls, action button placement, and an explicit confirmation prompt.',
9
+ 'Use the mounted ui-panel skill only as the source of truth for view.template and controlled primitives syntax. Do not guess props, template nodes, dataset fields, media slot shapes, or action keys.',
10
+ ].join('\n');
11
+ }
12
+ return [
13
+ '[Workspace tool guidance]',
14
+ 'Workspace-tool mode is enabled for this chat conversation.',
15
+ 'This slash command requires a published Workspace Tool. Do not satisfy it by only writing a script, describing a bundle, or sending ordinary text.',
16
+ 'Use the mounted workspace-tool-builder skill for the end-to-end tool workflow: define actions/status/view, write the bundle under .agent-tools/<slug>/, run bigbang tool validate --manifest-path <path> to check the manifest before publishing, and then run bigbang tool publish after validation passes.',
17
+ 'After bigbang tool publish returns successfully, immediately send one normal visible final reply that confirms what was created or updated. The platform will automatically attach any newly published tool to that next visible agent message; you may also pass `bigbang message send --kind final --tool-id <tool_id>`.',
18
+ 'The platform rejects a /tool final that has not published or attached a Workspace Tool; if publishing is impossible, send final with `--slash-command-failed "<reason>"`.',
19
+ 'Registered tool maintainer agents can republish the same slug only from the owner agent\'s same node/workspace; cross-workspace republish is rejected to avoid implicit execution-owner transfer.',
20
+ 'Do not inspect MEMORY.md, notes/tasks.md, or other workspace files after a successful publish unless the user explicitly asks for that extra analysis.',
21
+ 'Use the mounted ui-panel skill only as the source of truth for view.template, view.summary, view.parameterForm, and controlled primitives syntax. Do not use bigbang panel create, bigbang panel patch, or bigbang panel read-rows to publish a workspace tool.',
22
+ 'Workspace Tool actions must declare the intended interaction mode when it is not the default: omit mode or use mode="platform_exec" for direct command execution, and use mode="notify_agent" only when the button should send a structured request to the owner maintenance chat instead of running a command.',
23
+ 'platform_exec actions already leave non-waking audit notes in the existing owner maintenance chat; do not duplicate them with notify_agent unless the agent must actively handle the request.',
24
+ 'For status, monitoring, dashboard, or tool-control views, prefer view.summary for display-only overview facts and RowTemplateGrid Columns/Grid/Stack for the visible view instead of stacking every field vertically in one Card. Do not put ActionBar, Button, FileUpload, or parameter inputs in view.summary. When the user wants grouped or multi-column controls, express them through view.parameterForm instead of relying on the platform auto-generated paramsSchema form.',
25
+ 'Status actions must write a flat JSON object to stdout whose keys exactly match view.fields[].name. The platform parses this object and maps each value to the corresponding field declaration; nested objects or extra keys are ignored.',
26
+ 'actions[].paramsSchema is the canonical execution contract; view.parameterForm is the layout layer for those params. Parameter controls must bind with tool_action.<actionId>.<paramName> names. Supported paramsSchema.input types are: text, textarea, number, checkbox, select, multiselect, tags, date, and file. Tool FileUpload is supported only for platform_exec action params and is materialized into bounded run-scoped workspace upload refs.',
27
+ 'For persistent start/restart actions, set maxRunSeconds when there is a safe upper bound; set idleTimeoutSeconds when a silent stdout period should be treated as stale.',
28
+ 'Use manifest env.condaEnv, env.vars, and env.cwd for shared execution setup instead of repeating conda/env/cd boilerplate in every action command.',
29
+ ].join('\n');
30
+ }
@@ -0,0 +1,397 @@
1
+ import { log } from '@bbigbang/runtime-acp';
2
+ import { iterateParsedNodeEvents } from '../nodeEventHistory.js';
3
+ import { buildPrimaryDmTaskRootUpdateEventForThreadConversation } from './dmTaskThreadStatusProjection.js';
4
+ // Active WebSocket connections per conversation
5
+ const connectionsByConversation = new Map();
6
+ /** Broadcast a ServerEvent to all connected clients for a conversation */
7
+ export function broadcast(conversationId, event) {
8
+ const sockets = connectionsByConversation.get(conversationId);
9
+ if (!sockets)
10
+ return;
11
+ const data = JSON.stringify(event);
12
+ for (const ws of sockets) {
13
+ if (ws.readyState === ws.OPEN) {
14
+ ws.send(data);
15
+ }
16
+ }
17
+ }
18
+ function stripPromptAttachmentNote(text) {
19
+ const fileIdx = text.indexOf('\n\n[Attached file');
20
+ if (fileIdx >= 0)
21
+ return text.slice(0, fileIdx);
22
+ const imageIdx = text.indexOf('\n\n[Attached image');
23
+ return imageIdx >= 0 ? text.slice(0, imageIdx) : text;
24
+ }
25
+ /** Handle a new WebSocket connection for a conversation */
26
+ export function handleWebSocket(socket, conversationId, manager, senderName, broadcastStatus, resolvePromptAttachments) {
27
+ // Register this connection
28
+ let sockets = connectionsByConversation.get(conversationId);
29
+ if (!sockets) {
30
+ sockets = new Set();
31
+ connectionsByConversation.set(conversationId, sockets);
32
+ }
33
+ sockets.add(socket);
34
+ // Verify conversation exists
35
+ const conv = manager.getConversation(conversationId);
36
+ if (!conv) {
37
+ const errEvent = { type: 'error', message: 'Conversation not found' };
38
+ socket.send(JSON.stringify(errEvent));
39
+ socket.close();
40
+ return;
41
+ }
42
+ // Send current status
43
+ const statusEvent = {
44
+ type: 'conversation.status',
45
+ conversationId,
46
+ status: conv.status,
47
+ };
48
+ socket.send(JSON.stringify(statusEvent));
49
+ // 回放历史消息
50
+ replayHistory(socket, conversationId, manager);
51
+ // Signal history replay complete (client can now expect live events)
52
+ const historyDone = { type: 'history.complete' };
53
+ socket.send(JSON.stringify(historyDone));
54
+ // Handle incoming messages
55
+ socket.on('message', (raw) => {
56
+ let event;
57
+ try {
58
+ event = JSON.parse(String(raw));
59
+ }
60
+ catch {
61
+ const errEvent = { type: 'error', message: 'Invalid JSON' };
62
+ socket.send(JSON.stringify(errEvent));
63
+ return;
64
+ }
65
+ handleClientEvent(conversationId, event, manager, senderName, broadcastStatus, resolvePromptAttachments).catch((err) => {
66
+ log.warn('WebSocket client event error', err);
67
+ broadcast(conversationId, { type: 'error', message: String(err?.message ?? err) });
68
+ });
69
+ });
70
+ // Clean up on close
71
+ socket.on('close', () => {
72
+ sockets.delete(socket);
73
+ if (sockets.size === 0) {
74
+ connectionsByConversation.delete(conversationId);
75
+ }
76
+ });
77
+ }
78
+ /** 从 DB 读取历史 runs/events,回放为 ServerEvent 发送给客户端 */
79
+ function replayHistory(socket, conversationId, manager) {
80
+ const db = manager.getDb();
81
+ const row = db
82
+ .prepare('SELECT session_key as sessionKey, status FROM conversations WHERE id = ?')
83
+ .get(conversationId);
84
+ if (!row)
85
+ return;
86
+ // 获取所有 runs,按时间正序
87
+ const runs = db
88
+ .prepare(`SELECT run_id as runId, prompt_text as promptText, stop_reason as stopReason, ended_at as endedAt, started_at as startedAt
89
+ , error
90
+ FROM runs WHERE session_key = ? ORDER BY started_at ASC`)
91
+ .all(row.sessionKey);
92
+ const send = (event) => {
93
+ if (socket.readyState === socket.OPEN) {
94
+ socket.send(JSON.stringify(event));
95
+ }
96
+ };
97
+ for (const run of runs) {
98
+ // 发送用户消息
99
+ send({ type: 'history.user_message', text: run.promptText });
100
+ // 发送 turn
101
+ const turnId = `replay-${run.runId}`;
102
+ send({ type: 'turn.begin', turnId, startedAt: run.startedAt, promptText: run.promptText });
103
+ // 读取 node/event(remote runs)— 直接回放 ServerEvent
104
+ let replayedNodeEvent = false;
105
+ for (const evt of iterateParsedNodeEvents(db, {
106
+ runId: run.runId,
107
+ types: ['content.delta', 'activity.delta', 'tool.call', 'tool.result', 'thinking.delta', 'plan.update', 'task.update', 'run.usage', 'run.steer.result'],
108
+ })) {
109
+ const parsed = evt.event;
110
+ if (!parsed)
111
+ continue;
112
+ replayedNodeEvent = true;
113
+ if (parsed.type === 'tool.call') {
114
+ send({ ...parsed, startedAt: parsed.startedAt ?? evt.createdAt });
115
+ }
116
+ else if (parsed.type === 'tool.result') {
117
+ send({ ...parsed, endedAt: parsed.endedAt ?? evt.createdAt });
118
+ }
119
+ else if (parsed.type === 'run.usage') {
120
+ send({ ...parsed, createdAt: parsed.createdAt ?? evt.createdAt });
121
+ }
122
+ else if (parsed.type === 'run.steer.result') {
123
+ send({ ...parsed, createdAt: parsed.createdAt ?? evt.createdAt });
124
+ }
125
+ else {
126
+ send(parsed);
127
+ }
128
+ }
129
+ if (!replayedNodeEvent) {
130
+ // 本地 run:读取 session/update 事件并重新合并
131
+ const events = db
132
+ .prepare(`SELECT payload_json as payloadJson, created_at as createdAt FROM events
133
+ WHERE run_id = ? AND method = 'session/update'
134
+ ORDER BY seq ASC`)
135
+ .all(run.runId);
136
+ let agentText = '';
137
+ let thinkingText = '';
138
+ const toolCalls = new Map();
139
+ for (const evt of events) {
140
+ try {
141
+ const payload = JSON.parse(evt.payloadJson);
142
+ const update = payload?.update;
143
+ if (!update)
144
+ continue;
145
+ if (update.sessionUpdate === 'agent_message_chunk') {
146
+ agentText += update.content?.text ?? '';
147
+ }
148
+ if (update.sessionUpdate === 'agent_thought_chunk') {
149
+ thinkingText += update.content?.text ?? '';
150
+ }
151
+ if (update.sessionUpdate === 'tool_call') {
152
+ const toolCallId = extractToolCallIdFromUpdate(update) ?? '';
153
+ const name = String(update.title ?? toolCallId ?? 'tool');
154
+ toolCalls.set(toolCallId, { name, startedAt: evt.createdAt });
155
+ }
156
+ if (update.sessionUpdate === 'tool_call_update') {
157
+ const toolCallId = extractToolCallIdFromUpdate(update) ?? '';
158
+ const existing = toolCalls.get(toolCallId);
159
+ const status = `${update.status ?? update.state ?? update.outcome ?? ''}`.toLowerCase();
160
+ if (status.includes('done') ||
161
+ status.includes('complete') ||
162
+ status.includes('success') ||
163
+ status.includes('error') ||
164
+ status.includes('fail') ||
165
+ status.includes('cancel')) {
166
+ if (existing) {
167
+ existing.output = update.output ?? status;
168
+ existing.error = status.includes('error') || status.includes('fail');
169
+ existing.status = status.includes('cancel')
170
+ ? 'cancelled'
171
+ : existing.error
172
+ ? 'failed'
173
+ : 'completed';
174
+ existing.endedAt = evt.createdAt;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ catch {
180
+ // 跳过解析失败的事件
181
+ }
182
+ }
183
+ if (agentText) {
184
+ send({ type: 'content.delta', text: agentText });
185
+ }
186
+ if (thinkingText) {
187
+ send({ type: 'thinking.delta', text: thinkingText });
188
+ }
189
+ for (const [toolCallId, tc] of toolCalls) {
190
+ send({ type: 'tool.call', toolCallId, name: tc.name, input: null, startedAt: tc.startedAt });
191
+ if (tc.output !== undefined) {
192
+ send({
193
+ type: 'tool.result',
194
+ toolCallId,
195
+ output: tc.output,
196
+ error: tc.error,
197
+ status: tc.status,
198
+ endedAt: tc.endedAt,
199
+ });
200
+ }
201
+ }
202
+ }
203
+ if (run.endedAt === null) {
204
+ const approvalEvent = getReplayablePendingApprovalRequest(db, run.runId);
205
+ if (approvalEvent) {
206
+ if (row.status !== 'awaiting_approval') {
207
+ send({ type: 'conversation.status', conversationId, status: 'awaiting_approval' });
208
+ }
209
+ send(approvalEvent);
210
+ }
211
+ }
212
+ if (run.endedAt !== null) {
213
+ send({
214
+ type: 'turn.end',
215
+ turnId,
216
+ stopReason: run.error ? 'error' : (run.stopReason ?? 'end_turn'),
217
+ endedAt: run.endedAt,
218
+ error: run.error ?? undefined,
219
+ });
220
+ }
221
+ }
222
+ // 补发还在队列中(未被处理)的用户消息,避免刷新后消失
223
+ const queued = db
224
+ .prepare(`SELECT prompt_text as promptText, created_at as createdAt
225
+ FROM conversation_prompt_queue
226
+ WHERE conversation_id = ?
227
+ AND record_as_user_message = 1
228
+ AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
229
+ AND (
230
+ client_message_id IS NULL
231
+ OR NOT EXISTS (
232
+ SELECT 1
233
+ FROM channel_messages cm
234
+ WHERE cm.message_id = conversation_prompt_queue.client_message_id
235
+ )
236
+ )
237
+ ORDER BY created_at ASC`)
238
+ .all(conversationId);
239
+ for (const q of queued) {
240
+ send({ type: 'history.user_message', text: q.promptText });
241
+ }
242
+ }
243
+ function getReplayablePendingApprovalRequest(db, runId) {
244
+ const approvalRow = db.prepare(`SELECT approval.payload_json as payloadJson
245
+ FROM events approval
246
+ WHERE approval.run_id = ?
247
+ AND approval.method = 'node/event'
248
+ AND json_valid(approval.payload_json)
249
+ AND json_extract(approval.payload_json, '$.type') = 'approval.request'
250
+ AND NOT EXISTS (
251
+ SELECT 1
252
+ FROM events later
253
+ WHERE later.run_id = approval.run_id
254
+ AND later.seq > approval.seq
255
+ AND json_valid(later.payload_json)
256
+ AND (
257
+ (
258
+ later.method = 'approval/response'
259
+ AND json_extract(later.payload_json, '$.type') = 'approval.response'
260
+ AND json_extract(later.payload_json, '$.requestId') = json_extract(approval.payload_json, '$.requestId')
261
+ )
262
+ OR (
263
+ later.method = 'node/event'
264
+ AND json_extract(later.payload_json, '$.type') IN (
265
+ 'content.delta',
266
+ 'activity.delta',
267
+ 'thinking.delta',
268
+ 'tool.call',
269
+ 'tool.result',
270
+ 'plan.update',
271
+ 'plan.phase',
272
+ 'task.update',
273
+ 'runtime.compact'
274
+ )
275
+ )
276
+ )
277
+ )
278
+ ORDER BY approval.seq DESC
279
+ LIMIT 1`).get(runId);
280
+ if (!approvalRow)
281
+ return null;
282
+ try {
283
+ const approvalEvent = JSON.parse(approvalRow.payloadJson);
284
+ return approvalEvent.type === 'approval.request' ? approvalEvent : null;
285
+ }
286
+ catch {
287
+ // Ignore malformed persisted approval events; live approval flow is unaffected.
288
+ return null;
289
+ }
290
+ }
291
+ function persistApprovalResponseMarker(db, conversationId, requestId, decision, selectedActionId) {
292
+ const row = db.prepare(`SELECT runs.run_id as runId
293
+ FROM runs
294
+ JOIN conversations ON conversations.session_key = runs.session_key
295
+ WHERE conversations.id = ?
296
+ AND runs.ended_at IS NULL
297
+ AND EXISTS (
298
+ SELECT 1
299
+ FROM events
300
+ WHERE events.run_id = runs.run_id
301
+ AND events.method = 'node/event'
302
+ AND json_valid(events.payload_json)
303
+ AND json_extract(events.payload_json, '$.type') = 'approval.request'
304
+ AND json_extract(events.payload_json, '$.requestId') = ?
305
+ )
306
+ ORDER BY runs.started_at DESC
307
+ LIMIT 1`).get(conversationId, requestId);
308
+ if (!row)
309
+ return;
310
+ const seqRow = db.prepare(`SELECT COALESCE(MAX(seq), 0) + 1 as nextSeq
311
+ FROM events
312
+ WHERE run_id = ?`).get(row.runId);
313
+ const payload = {
314
+ type: 'approval.response',
315
+ requestId,
316
+ decision,
317
+ ...(selectedActionId ? { selectedActionId } : {}),
318
+ };
319
+ db.prepare(`INSERT OR IGNORE INTO events(run_id, seq, method, payload_json, created_at)
320
+ VALUES(?, ?, 'approval/response', ?, ?)`).run(row.runId, seqRow?.nextSeq ?? 1, JSON.stringify(payload), Date.now());
321
+ }
322
+ /** 从 update 中提取 toolCallId */
323
+ function extractToolCallIdFromUpdate(update) {
324
+ return update?.toolCallId ?? update?.tool_call_id ?? update?.callId ?? update?.call_id ?? null;
325
+ }
326
+ async function handleClientEvent(conversationId, event, manager, senderName, broadcastStatus, resolvePromptAttachments) {
327
+ switch (event.type) {
328
+ case 'prompt': {
329
+ const conv = manager.getConversation(conversationId);
330
+ if (!conv?.nodeId) {
331
+ broadcast(conversationId, { type: 'error', message: 'No agent node assigned. Connect an agent-node first.' });
332
+ break;
333
+ }
334
+ log.info('[ws] prompt → remote node', { conversationId, nodeId: conv.nodeId });
335
+ try {
336
+ const promptAttachments = resolvePromptAttachments?.(event.text, event.attachments) ?? { ids: [], refs: [] };
337
+ const promptText = promptAttachments.ids.length > 0 ? stripPromptAttachmentNote(event.text) : event.text;
338
+ const result = await manager.submitPrompt(conversationId, promptText, {
339
+ senderName,
340
+ attachments: promptAttachments.refs,
341
+ attachmentIds: promptAttachments.ids,
342
+ runtimeOverrides: event.runtimeOverrides,
343
+ });
344
+ if (result.queued) {
345
+ if (broadcastStatus) {
346
+ broadcastStatus(conversationId, 'queued');
347
+ }
348
+ else {
349
+ broadcast(conversationId, { type: 'conversation.status', conversationId, status: 'queued' });
350
+ }
351
+ }
352
+ }
353
+ catch (error) {
354
+ broadcast(conversationId, { type: 'error', message: String(error?.message ?? error) });
355
+ if (broadcastStatus) {
356
+ broadcastStatus(conversationId, 'idle');
357
+ }
358
+ else {
359
+ broadcast(conversationId, { type: 'conversation.status', conversationId, status: 'idle' });
360
+ }
361
+ }
362
+ // turn.end and idle status come from nodeWsHandler on run.end
363
+ break;
364
+ }
365
+ case 'approval.response': {
366
+ const result = await manager.handleApproval(conversationId, event.requestId, event.decision, event.selectedActionId, event.responseText, event.answers);
367
+ if (!result.ok) {
368
+ broadcast(conversationId, { type: 'error', message: result.message });
369
+ }
370
+ else if (broadcastStatus) {
371
+ persistApprovalResponseMarker(manager.getDb(), conversationId, event.requestId, event.decision, event.selectedActionId);
372
+ const primaryDmTaskRootUpdate = buildPrimaryDmTaskRootUpdateEventForThreadConversation(manager.getDb(), conversationId, 'active');
373
+ if (primaryDmTaskRootUpdate) {
374
+ broadcast(primaryDmTaskRootUpdate.conversationId, primaryDmTaskRootUpdate.event);
375
+ }
376
+ broadcastStatus(conversationId, 'active');
377
+ }
378
+ else {
379
+ persistApprovalResponseMarker(manager.getDb(), conversationId, event.requestId, event.decision, event.selectedActionId);
380
+ const primaryDmTaskRootUpdate = buildPrimaryDmTaskRootUpdateEventForThreadConversation(manager.getDb(), conversationId, 'active');
381
+ if (primaryDmTaskRootUpdate) {
382
+ broadcast(primaryDmTaskRootUpdate.conversationId, primaryDmTaskRootUpdate.event);
383
+ }
384
+ broadcast(conversationId, { type: 'conversation.status', conversationId, status: 'active' });
385
+ }
386
+ break;
387
+ }
388
+ case 'cancel':
389
+ {
390
+ const result = manager.cancelConversationRun(conversationId);
391
+ if (!result.ok) {
392
+ broadcast(conversationId, { type: 'error', message: result.message });
393
+ }
394
+ }
395
+ break;
396
+ }
397
+ }