@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,685 @@
1
+ import { log } from '@bbigbang/runtime-acp';
2
+ import { deleteTaskThreadFollowupsForTask } from './taskThreadFollowups.js';
3
+ import { getThreadBindingForTask } from './threadTaskBindings.js';
4
+ import { NOTIFICATION_REASON_MASK, supersedeNotificationRoundsForThreadRoots, } from './notificationRounds.js';
5
+ import { hasEffectiveAgentRuntimeCapability } from './runtimeCapabilities.js';
6
+ const taskThreadClosingStates = new Map();
7
+ const taskThreadReopenedRunBlocks = new Map();
8
+ function listTaskThreadConversations(db, params) {
9
+ if (params.channelId.startsWith('dm:')) {
10
+ const agentId = params.channelId.slice(3);
11
+ const dmUserId = params.dmUserId?.trim() || null;
12
+ return db.prepare(`SELECT c.id,
13
+ c.session_key as sessionKey,
14
+ c.status,
15
+ c.agent_id as agentId,
16
+ a.name as agentName,
17
+ c.agent_type as agentType,
18
+ c.node_id as nodeId
19
+ FROM conversations c
20
+ LEFT JOIN agents a ON a.agent_id = c.agent_id
21
+ WHERE c.thread_kind = 'direct'
22
+ AND c.thread_root_id = ?
23
+ AND c.agent_id = ?
24
+ AND ${dmUserId ? 'c.user_id = ?' : '1 = 1'}`).all(params.threadRootId, agentId, ...(dmUserId ? [dmUserId] : []));
25
+ }
26
+ return db.prepare(`SELECT c.id,
27
+ c.session_key as sessionKey,
28
+ c.status,
29
+ c.agent_id as agentId,
30
+ a.name as agentName,
31
+ c.agent_type as agentType,
32
+ c.node_id as nodeId
33
+ FROM conversations c
34
+ LEFT JOIN agents a ON a.agent_id = c.agent_id
35
+ WHERE c.thread_kind = 'branch'
36
+ AND c.channel_id = ?
37
+ AND c.thread_root_id = ?`).all(params.channelId, params.threadRootId);
38
+ }
39
+ function setTaskThreadClosingState(conversationId, state) {
40
+ taskThreadClosingStates.set(conversationId, state);
41
+ }
42
+ export function getTaskThreadClosingState(conversationId) {
43
+ if (!conversationId)
44
+ return null;
45
+ return taskThreadClosingStates.get(conversationId) ?? null;
46
+ }
47
+ export function getTaskThreadReopenedRunBlock(conversationId) {
48
+ if (!conversationId)
49
+ return null;
50
+ return taskThreadReopenedRunBlocks.get(conversationId) ?? null;
51
+ }
52
+ function findOpenRunForConversation(db, conversationId) {
53
+ const row = db.prepare(`SELECT r.run_id as runId,
54
+ r.started_at as startedAt
55
+ FROM conversations c
56
+ JOIN runs r
57
+ ON r.session_key = c.session_key
58
+ AND r.ended_at IS NULL
59
+ WHERE c.id = ?
60
+ ORDER BY r.started_at DESC
61
+ LIMIT 1`).get(conversationId);
62
+ return row ?? null;
63
+ }
64
+ function findRunStartedAt(db, conversationId, runId) {
65
+ const row = db.prepare(`SELECT r.started_at as startedAt
66
+ FROM conversations c
67
+ JOIN runs r
68
+ ON r.session_key = c.session_key
69
+ AND r.run_id = ?
70
+ WHERE c.id = ?
71
+ LIMIT 1`).get(runId, conversationId);
72
+ return typeof row?.startedAt === 'number' ? row.startedAt : null;
73
+ }
74
+ function findLatestUserReopenEventAt(db, taskId) {
75
+ const row = db.prepare(`SELECT created_at as createdAt
76
+ FROM task_events
77
+ WHERE task_id = ?
78
+ AND event_type = 'status_changed'
79
+ AND actor_type = 'user'
80
+ AND from_status = 'in_review'
81
+ AND to_status = 'in_progress'
82
+ ORDER BY created_at DESC
83
+ LIMIT 1`).get(taskId);
84
+ return typeof row?.createdAt === 'number' ? row.createdAt : null;
85
+ }
86
+ export function getStaleTaskThreadReopenedRunBlock(db, params) {
87
+ const conversationId = params.conversationId?.trim();
88
+ const taskId = params.taskId?.trim();
89
+ if (!conversationId || !taskId)
90
+ return null;
91
+ const openRun = params.runId?.trim()
92
+ ? {
93
+ runId: params.runId.trim(),
94
+ startedAt: findRunStartedAt(db, conversationId, params.runId.trim()),
95
+ }
96
+ : findOpenRunForConversation(db, conversationId);
97
+ if (!openRun?.runId)
98
+ return null;
99
+ const memoryBlock = taskThreadReopenedRunBlocks.get(conversationId);
100
+ if (memoryBlock?.taskId === taskId && memoryBlock.blockedRunId === openRun.runId) {
101
+ return {
102
+ taskId,
103
+ blockedRunId: openRun.runId,
104
+ source: 'memory',
105
+ ...(typeof openRun.startedAt === 'number' ? { runStartedAt: openRun.startedAt } : {}),
106
+ };
107
+ }
108
+ if (typeof openRun.startedAt !== 'number')
109
+ return null;
110
+ const reopenEventAt = findLatestUserReopenEventAt(db, taskId);
111
+ if (typeof reopenEventAt !== 'number')
112
+ return null;
113
+ if (openRun.startedAt >= reopenEventAt)
114
+ return null;
115
+ return {
116
+ taskId,
117
+ blockedRunId: openRun.runId,
118
+ source: 'task_event',
119
+ reopenEventAt,
120
+ runStartedAt: openRun.startedAt,
121
+ };
122
+ }
123
+ function clearTaskThreadClosingStateForConversation(conversationId) {
124
+ if (!conversationId)
125
+ return;
126
+ taskThreadClosingStates.delete(conversationId);
127
+ }
128
+ function clearTaskThreadReopenedRunBlockForConversation(conversationId, runId) {
129
+ if (!conversationId)
130
+ return;
131
+ if (!runId) {
132
+ taskThreadReopenedRunBlocks.delete(conversationId);
133
+ return;
134
+ }
135
+ const current = taskThreadReopenedRunBlocks.get(conversationId);
136
+ if (current?.blockedRunId === runId) {
137
+ taskThreadReopenedRunBlocks.delete(conversationId);
138
+ }
139
+ }
140
+ export function clearTaskThreadClosingStateForTask(db, taskId) {
141
+ const binding = getThreadBindingForTask(db, taskId);
142
+ if (!binding)
143
+ return;
144
+ const conversations = listTaskThreadConversations(db, {
145
+ channelId: binding.channelId,
146
+ threadRootId: binding.threadRootId,
147
+ });
148
+ for (const conversation of conversations) {
149
+ clearTaskThreadClosingStateForConversation(conversation.id);
150
+ }
151
+ }
152
+ export function clearTaskThreadReopenedRunBlock(conversationId, runId) {
153
+ clearTaskThreadReopenedRunBlockForConversation(conversationId, runId);
154
+ }
155
+ export function clearTaskThreadReopenedRunBlocksForTask(db, taskId) {
156
+ const binding = getThreadBindingForTask(db, taskId);
157
+ if (!binding)
158
+ return;
159
+ const conversations = listTaskThreadConversations(db, {
160
+ channelId: binding.channelId,
161
+ threadRootId: binding.threadRootId,
162
+ });
163
+ for (const conversation of conversations) {
164
+ clearTaskThreadReopenedRunBlockForConversation(conversation.id);
165
+ }
166
+ }
167
+ export function blockActiveTaskThreadRunsAfterReopen(db, taskId) {
168
+ const binding = getThreadBindingForTask(db, taskId);
169
+ if (!binding)
170
+ return [];
171
+ const conversations = listTaskThreadConversations(db, {
172
+ channelId: binding.channelId,
173
+ threadRootId: binding.threadRootId,
174
+ });
175
+ const blocked = [];
176
+ for (const conversation of conversations) {
177
+ const row = db.prepare(`SELECT run_id as runId
178
+ FROM runs
179
+ WHERE session_key = ?
180
+ AND ended_at IS NULL
181
+ ORDER BY started_at DESC
182
+ LIMIT 1`).get(conversation.sessionKey);
183
+ if (!row?.runId)
184
+ continue;
185
+ taskThreadReopenedRunBlocks.set(conversation.id, {
186
+ taskId,
187
+ blockedRunId: row.runId,
188
+ });
189
+ blocked.push({
190
+ conversationId: conversation.id,
191
+ runId: row.runId,
192
+ });
193
+ }
194
+ return blocked;
195
+ }
196
+ export function markTaskThreadClosingFinalSent(conversationId, runId) {
197
+ const current = getTaskThreadClosingState(conversationId);
198
+ if (!current)
199
+ return false;
200
+ if (!current.allowOneFinal || current.finalSent)
201
+ return false;
202
+ if (!current.ownerRunId || current.ownerRunId !== runId)
203
+ return false;
204
+ taskThreadClosingStates.set(conversationId, {
205
+ ...current,
206
+ allowOneFinal: false,
207
+ finalSent: true,
208
+ });
209
+ return true;
210
+ }
211
+ function restoreIdleConversationStatus(db, conversation) {
212
+ const hasQueuedPrompt = Boolean(db.prepare(`SELECT 1
213
+ FROM conversation_prompt_queue
214
+ WHERE conversation_id = ?
215
+ AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
216
+ LIMIT 1`).get(conversation.id));
217
+ if (hasQueuedPrompt)
218
+ return false;
219
+ const hasOpenRun = Boolean(db.prepare(`SELECT 1
220
+ FROM runs
221
+ WHERE session_key = ?
222
+ AND ended_at IS NULL
223
+ LIMIT 1`).get(conversation.sessionKey));
224
+ if (hasOpenRun)
225
+ return false;
226
+ const result = db.prepare(`UPDATE conversations
227
+ SET status = 'idle', updated_at = ?
228
+ WHERE id = ?
229
+ AND status IN ('queued', 'active', 'recovering', 'awaiting_approval')`).run(Date.now(), conversation.id);
230
+ return result.changes > 0;
231
+ }
232
+ function findOpenRunId(db, sessionKey) {
233
+ const row = db.prepare(`SELECT run_id as runId
234
+ FROM runs
235
+ WHERE session_key = ?
236
+ AND ended_at IS NULL
237
+ ORDER BY started_at DESC
238
+ LIMIT 1`).get(sessionKey);
239
+ return row?.runId ?? null;
240
+ }
241
+ export function findActiveTaskThreadRunForAgent(db, params) {
242
+ const agentId = params.agentId?.trim();
243
+ if (!agentId)
244
+ return null;
245
+ const binding = getThreadBindingForTask(db, params.taskId);
246
+ if (!binding)
247
+ return null;
248
+ const conversations = listTaskThreadConversations(db, {
249
+ channelId: binding.channelId,
250
+ threadRootId: binding.threadRootId,
251
+ dmUserId: params.dmUserId,
252
+ });
253
+ for (const conversation of conversations) {
254
+ if (conversation.agentId !== agentId)
255
+ continue;
256
+ const runId = findOpenRunId(db, conversation.sessionKey);
257
+ if (!runId)
258
+ continue;
259
+ return {
260
+ conversationId: conversation.id,
261
+ runId,
262
+ agentId: conversation.agentId,
263
+ };
264
+ }
265
+ return null;
266
+ }
267
+ export function listActiveTaskThreadPeerRuns(db, params) {
268
+ const binding = getThreadBindingForTask(db, params.taskId);
269
+ if (!binding)
270
+ return [];
271
+ const excludedAgentId = params.excludeAgentId?.trim() || null;
272
+ const conversations = listTaskThreadConversations(db, {
273
+ channelId: binding.channelId,
274
+ threadRootId: binding.threadRootId,
275
+ });
276
+ const runs = [];
277
+ for (const conversation of conversations) {
278
+ if (!conversation.agentId)
279
+ continue;
280
+ if (excludedAgentId && conversation.agentId === excludedAgentId)
281
+ continue;
282
+ const runId = findOpenRunId(db, conversation.sessionKey);
283
+ if (!runId)
284
+ continue;
285
+ runs.push({
286
+ conversationId: conversation.id,
287
+ runId,
288
+ agentId: conversation.agentId,
289
+ name: conversation.agentName ?? conversation.agentId,
290
+ });
291
+ }
292
+ return runs;
293
+ }
294
+ export function countQueuedHumanPromptsForTaskThread(db, taskId) {
295
+ const binding = getThreadBindingForTask(db, taskId);
296
+ if (!binding)
297
+ return 0;
298
+ const conversations = listTaskThreadConversations(db, {
299
+ channelId: binding.channelId,
300
+ threadRootId: binding.threadRootId,
301
+ });
302
+ const conversationIds = conversations.map((conversation) => conversation.id);
303
+ if (conversationIds.length === 0)
304
+ return 0;
305
+ const placeholders = conversationIds.map(() => '?').join(', ');
306
+ const row = db.prepare(`SELECT COUNT(*) as count
307
+ FROM conversation_prompt_queue
308
+ WHERE record_as_user_message != 0
309
+ AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
310
+ AND conversation_id IN (${placeholders})`).get(...conversationIds);
311
+ return Number(row?.count ?? 0);
312
+ }
313
+ export function countPendingHumanNotificationRoundsForTaskThread(db, taskId) {
314
+ const binding = getThreadBindingForTask(db, taskId);
315
+ if (!binding)
316
+ return 0;
317
+ const humanChatReasonMask = NOTIFICATION_REASON_MASK.participant_heads_up
318
+ | NOTIFICATION_REASON_MASK.explicit_agent_mention;
319
+ const row = db.prepare(`SELECT COUNT(*) as count
320
+ FROM agent_notification_rounds
321
+ WHERE channel_id = ?
322
+ AND surface_thread_key = ?
323
+ AND status IN ('ready', 'leased', 'delivered')
324
+ AND acked_at IS NULL
325
+ AND COALESCE(from_agent_id, '') LIKE 'user:%'
326
+ AND (reason_mask & ?) != 0`).get(binding.channelId, binding.threadRootId, humanChatReasonMask);
327
+ return Number(row?.count ?? 0);
328
+ }
329
+ export function closeTaskThreadRuntimeState(params) {
330
+ const now = Date.now();
331
+ const binding = getThreadBindingForTask(params.db, params.taskId);
332
+ clearTaskThreadClosingStateForTask(params.db, params.taskId);
333
+ clearTaskThreadReopenedRunBlocksForTask(params.db, params.taskId);
334
+ if (!binding) {
335
+ return {
336
+ closed: false,
337
+ conversationIds: [],
338
+ cancelledRunIds: [],
339
+ clearedQueuedPromptCount: 0,
340
+ clearedAggregateCount: 0,
341
+ clearedCooldownCount: 0,
342
+ clearedFollowupCount: 0,
343
+ idledConversationIds: [],
344
+ };
345
+ }
346
+ const conversations = listTaskThreadConversations(params.db, {
347
+ channelId: binding.channelId,
348
+ threadRootId: binding.threadRootId,
349
+ });
350
+ const cancelledRunIds = [];
351
+ const idledConversationIds = [];
352
+ let clearedQueuedPromptCount = 0;
353
+ for (const conversation of conversations) {
354
+ const cancelResult = params.conversationManager.cancelConversationRun(conversation.id);
355
+ if (cancelResult.ok && cancelResult.runId) {
356
+ cancelledRunIds.push(cancelResult.runId);
357
+ }
358
+ else if (cancelResult.message && cancelResult.message !== 'No active run to cancel.') {
359
+ log.warn('[task-thread-runtime-closure] failed to cancel active run', {
360
+ taskId: params.taskId,
361
+ conversationId: conversation.id,
362
+ reason: params.reason,
363
+ error: cancelResult.message,
364
+ });
365
+ }
366
+ clearedQueuedPromptCount += params.db.prepare(`DELETE FROM conversation_prompt_queue
367
+ WHERE conversation_id = ?`).run(conversation.id).changes;
368
+ if (restoreIdleConversationStatus(params.db, conversation)) {
369
+ idledConversationIds.push(conversation.id);
370
+ params.onConversationStatusChange?.(conversation.id, 'idle');
371
+ }
372
+ }
373
+ const clearedAggregateCount = 0;
374
+ const clearedCooldownCount = 0;
375
+ const clearedFollowupCount = deleteTaskThreadFollowupsForTask(params.db, params.taskId);
376
+ supersedeNotificationRoundsForThreadRoots(params.db, {
377
+ channelId: binding.channelId,
378
+ threadRootIds: binding.threadRootIds,
379
+ now,
380
+ reason: `Task thread closed: ${params.reason}`,
381
+ });
382
+ return {
383
+ closed: true,
384
+ conversationIds: conversations.map((conversation) => conversation.id),
385
+ cancelledRunIds,
386
+ clearedQueuedPromptCount,
387
+ clearedAggregateCount,
388
+ clearedCooldownCount,
389
+ clearedFollowupCount,
390
+ idledConversationIds,
391
+ };
392
+ }
393
+ export function sealTaskThreadRuntimeState(params) {
394
+ const now = Date.now();
395
+ const binding = getThreadBindingForTask(params.db, params.taskId);
396
+ if (!binding) {
397
+ return {
398
+ sealed: false,
399
+ conversationIds: [],
400
+ preservedActiveRuns: [],
401
+ clearedQueuedPromptCount: 0,
402
+ clearedAggregateCount: 0,
403
+ clearedCooldownCount: 0,
404
+ clearedFollowupCount: 0,
405
+ idledConversationIds: [],
406
+ };
407
+ }
408
+ const conversations = listTaskThreadConversations(params.db, {
409
+ channelId: binding.channelId,
410
+ threadRootId: binding.threadRootId,
411
+ dmUserId: params.dmUserId,
412
+ });
413
+ const preservedActiveRuns = [];
414
+ const idledConversationIds = [];
415
+ let clearedQueuedPromptCount = 0;
416
+ const ownerConversationId = params.ownerConversationId ?? null;
417
+ const ownerRunId = params.ownerRunId ?? null;
418
+ const ownerAgentId = params.ownerAgentId ?? null;
419
+ for (const conversation of conversations) {
420
+ clearTaskThreadClosingStateForConversation(conversation.id);
421
+ clearTaskThreadReopenedRunBlockForConversation(conversation.id);
422
+ const openRunId = findOpenRunId(params.db, conversation.sessionKey);
423
+ const isOwnerConversation = Boolean(params.reason === 'in_review'
424
+ && ownerConversationId
425
+ && ownerRunId
426
+ && conversation.id === ownerConversationId
427
+ && openRunId === ownerRunId
428
+ && (!ownerAgentId || conversation.agentId === ownerAgentId));
429
+ setTaskThreadClosingState(conversation.id, {
430
+ taskId: params.taskId,
431
+ channelId: binding.channelId,
432
+ threadRootId: binding.threadRootId,
433
+ reason: params.reason,
434
+ ownerAgentId,
435
+ ownerRunId,
436
+ allowOneFinal: isOwnerConversation,
437
+ finalSent: false,
438
+ });
439
+ clearedQueuedPromptCount += params.db.prepare(`DELETE FROM conversation_prompt_queue
440
+ WHERE conversation_id = ?`).run(conversation.id).changes;
441
+ if (openRunId) {
442
+ preservedActiveRuns.push({
443
+ conversationId: conversation.id,
444
+ runId: openRunId,
445
+ agentId: conversation.agentId,
446
+ });
447
+ }
448
+ if (restoreIdleConversationStatus(params.db, conversation)) {
449
+ idledConversationIds.push(conversation.id);
450
+ params.onConversationStatusChange?.(conversation.id, 'idle');
451
+ }
452
+ }
453
+ const clearedAggregateCount = 0;
454
+ const clearedCooldownCount = 0;
455
+ const clearedFollowupCount = deleteTaskThreadFollowupsForTask(params.db, params.taskId);
456
+ supersedeNotificationRoundsForThreadRoots(params.db, {
457
+ channelId: binding.channelId,
458
+ threadRootIds: binding.threadRootIds,
459
+ now,
460
+ reason: `Task thread sealed: ${params.reason}`,
461
+ });
462
+ return {
463
+ sealed: true,
464
+ conversationIds: conversations.map((conversation) => conversation.id),
465
+ preservedActiveRuns,
466
+ clearedQueuedPromptCount,
467
+ clearedAggregateCount,
468
+ clearedCooldownCount,
469
+ clearedFollowupCount,
470
+ idledConversationIds,
471
+ };
472
+ }
473
+ export function buildTaskThreadReviewCleanupNotice(params) {
474
+ return [
475
+ '[System notice]',
476
+ `Task #${params.taskNumber} "${params.title}" has moved to in_review.`,
477
+ 'This channel task thread is now closed for user-visible chat.',
478
+ 'Finish any private cleanup now, including MEMORY.md or notes updates if needed, then stop.',
479
+ 'Do not send visible chat, and do not post to this task thread.',
480
+ ].join('\n');
481
+ }
482
+ export function softCloseTaskThreadForReview(params) {
483
+ const now = Date.now();
484
+ const binding = getThreadBindingForTask(params.db, params.taskId);
485
+ clearTaskThreadClosingStateForTask(params.db, params.taskId);
486
+ clearTaskThreadReopenedRunBlocksForTask(params.db, params.taskId);
487
+ if (!binding) {
488
+ return {
489
+ closed: false,
490
+ conversationIds: [],
491
+ preservedActiveRuns: [],
492
+ clearedQueuedPromptCount: 0,
493
+ clearedAggregateCount: 0,
494
+ clearedCooldownCount: 0,
495
+ clearedFollowupCount: 0,
496
+ idledConversationIds: [],
497
+ preservedConversationId: null,
498
+ cancelledRunIds: [],
499
+ };
500
+ }
501
+ const conversations = listTaskThreadConversations(params.db, {
502
+ channelId: binding.channelId,
503
+ threadRootId: binding.threadRootId,
504
+ });
505
+ const preservedActiveRuns = [];
506
+ const cancelledRunIds = [];
507
+ const idledConversationIds = [];
508
+ let clearedQueuedPromptCount = 0;
509
+ const ownerConversationId = params.ownerConversationId ?? null;
510
+ const ownerRunId = params.ownerRunId ?? null;
511
+ const ownerAgentId = params.ownerAgentId ?? null;
512
+ for (const conversation of conversations) {
513
+ const openRunId = findOpenRunId(params.db, conversation.sessionKey);
514
+ const isOwnerConversation = Boolean(ownerConversationId
515
+ && ownerRunId
516
+ && conversation.id === ownerConversationId
517
+ && openRunId === ownerRunId
518
+ && (!ownerAgentId || conversation.agentId === ownerAgentId));
519
+ setTaskThreadClosingState(conversation.id, {
520
+ taskId: params.taskId,
521
+ channelId: binding.channelId,
522
+ threadRootId: binding.threadRootId,
523
+ reason: 'in_review',
524
+ ownerAgentId,
525
+ ownerRunId,
526
+ allowOneFinal: isOwnerConversation,
527
+ finalSent: false,
528
+ });
529
+ clearedQueuedPromptCount += params.db.prepare(`DELETE FROM conversation_prompt_queue
530
+ WHERE conversation_id = ?`).run(conversation.id).changes;
531
+ if (openRunId && !isOwnerConversation) {
532
+ if (hasEffectiveAgentRuntimeCapability(params.db, {
533
+ nodeId: conversation.nodeId,
534
+ agentType: conversation.agentType,
535
+ capability: 'activeTurnSteer',
536
+ })) {
537
+ preservedActiveRuns.push({
538
+ conversationId: conversation.id,
539
+ runId: openRunId,
540
+ agentId: conversation.agentId,
541
+ });
542
+ }
543
+ else if (params.conversationManager) {
544
+ const cancelResult = params.conversationManager.cancelConversationRun(conversation.id, openRunId);
545
+ if (cancelResult.ok && cancelResult.runId) {
546
+ cancelledRunIds.push(cancelResult.runId);
547
+ const activeResult = params.db.prepare(`UPDATE conversations
548
+ SET status = 'active', updated_at = ?
549
+ WHERE id = ?
550
+ AND status = 'queued'`).run(Date.now(), conversation.id);
551
+ if (activeResult.changes > 0) {
552
+ params.onConversationStatusChange?.(conversation.id, 'active');
553
+ }
554
+ }
555
+ else if (cancelResult.message && cancelResult.message !== 'No active run to cancel.') {
556
+ log.warn('[task-thread-runtime-closure] failed to cancel non-steerable active run during review soft-close', {
557
+ taskId: params.taskId,
558
+ conversationId: conversation.id,
559
+ error: cancelResult.message,
560
+ });
561
+ }
562
+ }
563
+ else {
564
+ log.warn('[task-thread-runtime-closure] cannot preserve non-steerable active run during review soft-close without a conversation manager', {
565
+ taskId: params.taskId,
566
+ conversationId: conversation.id,
567
+ runId: openRunId,
568
+ agentType: conversation.agentType,
569
+ });
570
+ }
571
+ }
572
+ if (restoreIdleConversationStatus(params.db, conversation)) {
573
+ idledConversationIds.push(conversation.id);
574
+ params.onConversationStatusChange?.(conversation.id, 'idle');
575
+ }
576
+ }
577
+ const clearedAggregateCount = 0;
578
+ const clearedCooldownCount = 0;
579
+ const clearedFollowupCount = deleteTaskThreadFollowupsForTask(params.db, params.taskId);
580
+ supersedeNotificationRoundsForThreadRoots(params.db, {
581
+ channelId: binding.channelId,
582
+ threadRootIds: binding.threadRootIds,
583
+ now,
584
+ reason: 'Task thread soft-closed for review',
585
+ });
586
+ return {
587
+ closed: true,
588
+ conversationIds: conversations.map((conversation) => conversation.id),
589
+ preservedActiveRuns,
590
+ clearedQueuedPromptCount,
591
+ clearedAggregateCount,
592
+ clearedCooldownCount,
593
+ clearedFollowupCount,
594
+ idledConversationIds,
595
+ preservedConversationId: ownerConversationId,
596
+ cancelledRunIds,
597
+ };
598
+ }
599
+ export function beginTaskThreadClosing(params) {
600
+ const binding = getThreadBindingForTask(params.db, params.taskId);
601
+ clearTaskThreadClosingStateForTask(params.db, params.taskId);
602
+ clearTaskThreadReopenedRunBlocksForTask(params.db, params.taskId);
603
+ if (!binding) {
604
+ return {
605
+ closed: false,
606
+ conversationIds: [],
607
+ cancelledRunIds: [],
608
+ clearedQueuedPromptCount: 0,
609
+ clearedAggregateCount: 0,
610
+ clearedCooldownCount: 0,
611
+ clearedFollowupCount: 0,
612
+ idledConversationIds: [],
613
+ preservedConversationId: null,
614
+ };
615
+ }
616
+ const conversations = listTaskThreadConversations(params.db, {
617
+ channelId: binding.channelId,
618
+ threadRootId: binding.threadRootId,
619
+ });
620
+ const ownerConversation = conversations.find((conversation) => conversation.id === params.ownerConversationId);
621
+ if (!ownerConversation) {
622
+ return {
623
+ closed: false,
624
+ conversationIds: conversations.map((conversation) => conversation.id),
625
+ cancelledRunIds: [],
626
+ clearedQueuedPromptCount: 0,
627
+ clearedAggregateCount: 0,
628
+ clearedCooldownCount: 0,
629
+ clearedFollowupCount: 0,
630
+ idledConversationIds: [],
631
+ preservedConversationId: null,
632
+ };
633
+ }
634
+ const cancelledRunIds = [];
635
+ const idledConversationIds = [];
636
+ let clearedQueuedPromptCount = 0;
637
+ for (const conversation of conversations) {
638
+ const isOwnerConversation = conversation.id === params.ownerConversationId;
639
+ setTaskThreadClosingState(conversation.id, {
640
+ taskId: params.taskId,
641
+ channelId: binding.channelId,
642
+ threadRootId: binding.threadRootId,
643
+ reason: params.reason,
644
+ ownerAgentId: params.ownerAgentId ?? null,
645
+ ownerRunId: params.ownerRunId,
646
+ allowOneFinal: isOwnerConversation,
647
+ finalSent: false,
648
+ });
649
+ clearedQueuedPromptCount += params.db.prepare(`DELETE FROM conversation_prompt_queue
650
+ WHERE conversation_id = ?`).run(conversation.id).changes;
651
+ if (isOwnerConversation) {
652
+ continue;
653
+ }
654
+ const cancelResult = params.conversationManager.cancelConversationRun(conversation.id);
655
+ if (cancelResult.ok && cancelResult.runId) {
656
+ cancelledRunIds.push(cancelResult.runId);
657
+ }
658
+ else if (cancelResult.message && cancelResult.message !== 'No active run to cancel.') {
659
+ log.warn('[task-thread-runtime-closure] failed to cancel active run during graceful close', {
660
+ taskId: params.taskId,
661
+ conversationId: conversation.id,
662
+ reason: params.reason,
663
+ error: cancelResult.message,
664
+ });
665
+ }
666
+ if (restoreIdleConversationStatus(params.db, conversation)) {
667
+ idledConversationIds.push(conversation.id);
668
+ params.onConversationStatusChange?.(conversation.id, 'idle');
669
+ }
670
+ }
671
+ const clearedAggregateCount = 0;
672
+ const clearedCooldownCount = 0;
673
+ const clearedFollowupCount = deleteTaskThreadFollowupsForTask(params.db, params.taskId);
674
+ return {
675
+ closed: true,
676
+ conversationIds: conversations.map((conversation) => conversation.id),
677
+ cancelledRunIds,
678
+ clearedQueuedPromptCount,
679
+ clearedAggregateCount,
680
+ clearedCooldownCount,
681
+ clearedFollowupCount,
682
+ idledConversationIds,
683
+ preservedConversationId: params.ownerConversationId,
684
+ };
685
+ }