@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,599 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { log } from '@bbigbang/runtime-acp';
3
+ import { buildChannelSurfaceActivationEnvelope, buildDirectSurfaceActivationEnvelope, } from './activationContext.js';
4
+ import { getExpectedBootstrapStopReason } from './planApprovalState.js';
5
+ import { getBoundTaskForThread, listThreadCollaborationParticipants } from './threadTaskBindings.js';
6
+ export const TASK_THREAD_FOLLOWUP_KIND = 'task_status_reminder';
7
+ export const TASK_THREAD_FOLLOWUP_DELAY_MS = 10 * 60 * 1000;
8
+ export const TASK_THREAD_FOLLOWUP_POLL_INTERVAL_MS = 30 * 1000;
9
+ export const TASK_THREAD_FOLLOWUP_EVENT_METHOD = 'platform/task-thread-followup';
10
+ function nextSyntheticRunEventSeq(db, runId) {
11
+ const row = db.prepare(`SELECT COALESCE(MAX(seq), 0) as maxSeq
12
+ FROM events
13
+ WHERE run_id = ?`).get(runId);
14
+ return (row?.maxSeq ?? 0) + 1;
15
+ }
16
+ function appendTaskThreadFollowupEvent(db, runId, payload) {
17
+ if (!runId)
18
+ return;
19
+ db.prepare(`INSERT INTO events(run_id, seq, method, payload_json, created_at)
20
+ VALUES(?, ?, ?, ?, ?)`).run(runId, nextSyntheticRunEventSeq(db, runId), TASK_THREAD_FOLLOWUP_EVENT_METHOD, JSON.stringify(payload), Date.now());
21
+ }
22
+ function isCancelStopReason(stopReason) {
23
+ return stopReason === 'cancelled' || stopReason === 'canceled';
24
+ }
25
+ function wasRunHandedOff(db, runId) {
26
+ return Boolean(db.prepare(`SELECT 1
27
+ FROM events
28
+ WHERE run_id = ?
29
+ AND method = 'platform/handoff'
30
+ LIMIT 1`).get(runId));
31
+ }
32
+ export function getRunSystemFollowupMetadata(db, runId) {
33
+ if (!runId)
34
+ return null;
35
+ const row = db.prepare(`SELECT activation_metadata_json as activationMetadataJson
36
+ FROM run_debug_inputs
37
+ WHERE run_id = ?
38
+ LIMIT 1`).get(runId);
39
+ if (!row?.activationMetadataJson)
40
+ return null;
41
+ try {
42
+ const parsed = JSON.parse(row.activationMetadataJson);
43
+ if (parsed?.systemFollowup?.kind === TASK_THREAD_FOLLOWUP_KIND
44
+ && typeof parsed.systemFollowup.followupId === 'string'
45
+ && parsed.systemFollowup.followupId.trim()
46
+ && typeof parsed.systemFollowup.taskId === 'string'
47
+ && parsed.systemFollowup.taskId.trim()
48
+ && typeof parsed.systemFollowup.channelId === 'string'
49
+ && parsed.systemFollowup.channelId.trim()
50
+ && typeof parsed.systemFollowup.threadRootId === 'string'
51
+ && parsed.systemFollowup.threadRootId.trim()) {
52
+ return {
53
+ kind: TASK_THREAD_FOLLOWUP_KIND,
54
+ followupId: parsed.systemFollowup.followupId.trim(),
55
+ taskId: parsed.systemFollowup.taskId.trim(),
56
+ channelId: parsed.systemFollowup.channelId.trim(),
57
+ threadRootId: parsed.systemFollowup.threadRootId.trim(),
58
+ ...(typeof parsed.systemFollowup.delayMs === 'number'
59
+ && Number.isFinite(parsed.systemFollowup.delayMs)
60
+ && parsed.systemFollowup.delayMs > 0
61
+ ? { delayMs: parsed.systemFollowup.delayMs }
62
+ : {}),
63
+ ...(typeof parsed.systemFollowup.baselineThreadSeq === 'number'
64
+ && Number.isFinite(parsed.systemFollowup.baselineThreadSeq)
65
+ && parsed.systemFollowup.baselineThreadSeq >= 0
66
+ ? { baselineThreadSeq: parsed.systemFollowup.baselineThreadSeq }
67
+ : {}),
68
+ };
69
+ }
70
+ }
71
+ catch {
72
+ // ignore malformed activation metadata
73
+ }
74
+ return null;
75
+ }
76
+ function loadLatestThreadSeq(db, params) {
77
+ const row = db.prepare(`SELECT COALESCE(MAX(seq), 0) as maxSeq
78
+ FROM channel_messages
79
+ WHERE channel_id = ? AND thread_root_id = ?`).get(params.channelId, params.threadRootId);
80
+ return row?.maxSeq ?? 0;
81
+ }
82
+ function resolveTaskThreadChannelId(params) {
83
+ if (params.threadKind === 'direct' && params.agentId) {
84
+ return `dm:${params.agentId}`;
85
+ }
86
+ return params.conversationChannelId;
87
+ }
88
+ function isBlockedTask(task) {
89
+ return Boolean(task && (task.blockedAt ?? task.blockedBy ?? task.blockedReason));
90
+ }
91
+ function shouldKeepTaskThreadFollowup(task) {
92
+ if (!task)
93
+ return false;
94
+ if (task.status !== 'todo' && task.status !== 'in_progress')
95
+ return false;
96
+ return !isBlockedTask(task);
97
+ }
98
+ function readFollowupDelayMsFromMetadata(metadata) {
99
+ const delayMs = metadata?.delayMs;
100
+ return typeof delayMs === 'number' && Number.isFinite(delayMs) && delayMs > 0
101
+ ? delayMs
102
+ : TASK_THREAD_FOLLOWUP_DELAY_MS;
103
+ }
104
+ function readFollowupDelayMsFromRow(row) {
105
+ const derivedDelay = row.scheduledFor - row.updatedAt;
106
+ return Number.isFinite(derivedDelay) && derivedDelay > 0
107
+ ? derivedDelay
108
+ : TASK_THREAD_FOLLOWUP_DELAY_MS;
109
+ }
110
+ function hasExternalThreadActivitySinceBaseline(params) {
111
+ if (!Number.isFinite(params.baselineThreadSeq))
112
+ return true;
113
+ return Boolean(params.db.prepare(`SELECT 1
114
+ FROM channel_messages
115
+ WHERE channel_id = ?
116
+ AND thread_root_id = ?
117
+ AND seq > ?
118
+ AND (run_id IS NULL OR run_id != ?)
119
+ LIMIT 1`).get(params.channelId, params.threadRootId, params.baselineThreadSeq, params.currentRunId));
120
+ }
121
+ export function scheduleTaskThreadFollowupForRun(params) {
122
+ const { db, conversationId, runId } = params;
123
+ if (params.error)
124
+ return;
125
+ const systemFollowup = getRunSystemFollowupMetadata(db, runId);
126
+ if (params.stopReason === 'handoff' || isCancelStopReason(params.stopReason)) {
127
+ if (wasRunHandedOff(db, runId) || getExpectedBootstrapStopReason(db, runId) === 'handoff_bootstrap') {
128
+ return;
129
+ }
130
+ return;
131
+ }
132
+ if (params.stopReason === 'handoff_bootstrap')
133
+ return;
134
+ const conversation = db.prepare(`SELECT channel_id as channelId,
135
+ thread_kind as threadKind,
136
+ agent_id as agentId,
137
+ thread_root_id as threadRootId
138
+ FROM conversations
139
+ WHERE id = ?
140
+ LIMIT 1`).get(conversationId);
141
+ if (!conversation?.threadRootId)
142
+ return;
143
+ const taskThreadChannelId = resolveTaskThreadChannelId({
144
+ conversationChannelId: conversation.channelId,
145
+ threadKind: conversation.threadKind,
146
+ agentId: conversation.agentId,
147
+ });
148
+ const boundTask = getBoundTaskForThread(db, {
149
+ channelId: taskThreadChannelId,
150
+ threadRootId: conversation.threadRootId,
151
+ });
152
+ if (!boundTask || !shouldKeepTaskThreadFollowup(boundTask))
153
+ return;
154
+ const delayMs = systemFollowup?.kind === TASK_THREAD_FOLLOWUP_KIND
155
+ ? (hasExternalThreadActivitySinceBaseline({
156
+ db,
157
+ channelId: taskThreadChannelId,
158
+ threadRootId: conversation.threadRootId,
159
+ baselineThreadSeq: systemFollowup.baselineThreadSeq,
160
+ currentRunId: runId,
161
+ })
162
+ ? TASK_THREAD_FOLLOWUP_DELAY_MS
163
+ : readFollowupDelayMsFromMetadata(systemFollowup) * 2)
164
+ : TASK_THREAD_FOLLOWUP_DELAY_MS;
165
+ const now = params.now ?? Date.now();
166
+ const scheduledFor = now + delayMs;
167
+ const baselineThreadSeq = loadLatestThreadSeq(db, {
168
+ channelId: taskThreadChannelId,
169
+ threadRootId: conversation.threadRootId,
170
+ });
171
+ const existing = db.prepare(`SELECT followup_id as followupId
172
+ FROM task_thread_followups
173
+ WHERE kind = ? AND channel_id = ? AND thread_root_id = ?
174
+ LIMIT 1`).get(TASK_THREAD_FOLLOWUP_KIND, taskThreadChannelId, conversation.threadRootId);
175
+ const followupId = existing?.followupId ?? randomUUID();
176
+ db.prepare(`INSERT INTO task_thread_followups(
177
+ followup_id, kind, task_id, channel_id, thread_root_id, source_conversation_id,
178
+ created_by_run_id, baseline_thread_seq, scheduled_for, created_at, updated_at
179
+ )
180
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
181
+ ON CONFLICT(kind, channel_id, thread_root_id) DO UPDATE SET
182
+ task_id = excluded.task_id,
183
+ source_conversation_id = excluded.source_conversation_id,
184
+ created_by_run_id = excluded.created_by_run_id,
185
+ baseline_thread_seq = excluded.baseline_thread_seq,
186
+ scheduled_for = excluded.scheduled_for,
187
+ updated_at = excluded.updated_at`).run(followupId, TASK_THREAD_FOLLOWUP_KIND, boundTask.taskId, taskThreadChannelId, conversation.threadRootId, conversationId, runId, baselineThreadSeq, scheduledFor, now, now);
188
+ appendTaskThreadFollowupEvent(db, runId, {
189
+ followupId,
190
+ state: 'scheduled',
191
+ kind: TASK_THREAD_FOLLOWUP_KIND,
192
+ taskId: boundTask.taskId,
193
+ channelId: taskThreadChannelId,
194
+ threadRootId: conversation.threadRootId,
195
+ sourceConversationId: conversationId,
196
+ baselineThreadSeq,
197
+ delayMs,
198
+ scheduledFor,
199
+ });
200
+ }
201
+ export function deleteTaskThreadFollowupsForTask(db, taskId) {
202
+ return db.prepare(`DELETE FROM task_thread_followups
203
+ WHERE task_id = ?`).run(taskId).changes;
204
+ }
205
+ function loadOpenTask(db, taskId) {
206
+ const row = db.prepare(`SELECT task_id as taskId,
207
+ channel_id as channelId,
208
+ task_number as taskNumber,
209
+ title,
210
+ description,
211
+ status,
212
+ claimed_by_agent_id as claimedByAgentId,
213
+ claimed_by_name as claimedByName,
214
+ blocked_reason as blockedReason,
215
+ blocked_at as blockedAt,
216
+ blocked_by as blockedBy
217
+ FROM tasks
218
+ WHERE task_id = ?
219
+ LIMIT 1`).get(taskId);
220
+ return row ?? null;
221
+ }
222
+ function deleteFollowup(db, followupId) {
223
+ db.prepare('DELETE FROM task_thread_followups WHERE followup_id = ?').run(followupId);
224
+ }
225
+ function buildTaskBoardTarget(replyTarget) {
226
+ if (replyTarget.startsWith('dm:@')) {
227
+ const parts = replyTarget.split(':');
228
+ return parts.length >= 3 ? parts.slice(0, 2).join(':') : replyTarget;
229
+ }
230
+ if (replyTarget.startsWith('#')) {
231
+ const idx = replyTarget.lastIndexOf(':');
232
+ return idx > 0 ? replyTarget.slice(0, idx) : replyTarget;
233
+ }
234
+ return replyTarget;
235
+ }
236
+ function formatFollowupDelayLabel(delayMs) {
237
+ const roundedMinutes = Math.round(delayMs / 60_000);
238
+ if (roundedMinutes > 0 && roundedMinutes % 60 === 0) {
239
+ const hours = roundedMinutes / 60;
240
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
241
+ }
242
+ return `${roundedMinutes} minute${roundedMinutes === 1 ? '' : 's'}`;
243
+ }
244
+ function buildTaskThreadFollowupPrompt(params) {
245
+ const boardTarget = buildTaskBoardTarget(params.replyTarget);
246
+ return [
247
+ '[System: Task-thread follow-up]',
248
+ `It has been ${formatFollowupDelayLabel(params.delayMs)} since your last run on this task thread ended, and the task is still open.`,
249
+ '',
250
+ '[Current conversation target]',
251
+ `reply_target: ${params.replyTarget}`,
252
+ `Task: #${params.task.taskNumber} ${params.task.title}`,
253
+ `Current task status: ${params.task.status}`,
254
+ '',
255
+ 'Rules:',
256
+ `- If the substantive result is ready, send it in this thread and run bigbang task update --channel "${boardTarget}" --number ${params.task.taskNumber} --status in_review.`,
257
+ '- If the work is still ongoing, continue the task and send a progress update in this thread.',
258
+ '- If you are blocked on user input, approval, or missing context, ask the user exactly what you need in this thread.',
259
+ '- If the latest visible state is already waiting on the user or reviewer and you have nothing new to add, do not repeat the waiting message; update memory/notes only if needed, then stop.',
260
+ '- Do not send a redundant second completion-summary after the substantive final result.',
261
+ ].join('\n');
262
+ }
263
+ function canAttemptFollowupDispatch(db, nodeRegistry, conversationId) {
264
+ const row = db.prepare(`SELECT session_key as sessionKey,
265
+ node_id as nodeId,
266
+ status
267
+ FROM conversations
268
+ WHERE id = ?
269
+ LIMIT 1`).get(conversationId);
270
+ if (!row?.nodeId)
271
+ return false;
272
+ if (nodeRegistry && !nodeRegistry.getNode(row.nodeId))
273
+ return false;
274
+ if (row.status === 'queued' || row.status === 'active' || row.status === 'recovering' || row.status === 'awaiting_approval') {
275
+ return false;
276
+ }
277
+ return !db.prepare(`SELECT 1
278
+ FROM runs
279
+ WHERE session_key = ? AND ended_at IS NULL
280
+ LIMIT 1`).get(row.sessionKey);
281
+ }
282
+ function resolveDmConversation(db, conversationManager, row) {
283
+ const expectedAgentId = row.channelId.startsWith('dm:')
284
+ ? row.channelId.slice(3)
285
+ : null;
286
+ const sourceConversation = row.sourceConversationId
287
+ ? conversationManager.getConversation(row.sourceConversationId)
288
+ : null;
289
+ if (sourceConversation
290
+ && sourceConversation.threadKind === 'direct'
291
+ && sourceConversation.threadRootId === row.threadRootId
292
+ && (!expectedAgentId || sourceConversation.agentId === expectedAgentId)) {
293
+ return sourceConversation;
294
+ }
295
+ const existing = db.prepare(`SELECT id
296
+ FROM conversations
297
+ WHERE thread_kind = 'direct'
298
+ AND thread_root_id = ?
299
+ AND (? IS NULL OR agent_id = ?)
300
+ ORDER BY updated_at DESC
301
+ LIMIT 1`).get(row.threadRootId, expectedAgentId, expectedAgentId);
302
+ return existing ? conversationManager.getConversation(existing.id) : null;
303
+ }
304
+ function resolveChannelConversation(conversationManager, task, row) {
305
+ if (task.claimedByAgentId) {
306
+ const ownerConversation = conversationManager.openAgentChannelThread(task.claimedByAgentId, row.channelId, row.threadRootId);
307
+ if (ownerConversation)
308
+ return ownerConversation;
309
+ }
310
+ const recentParticipants = listThreadCollaborationParticipants(conversationManager.getDb(), {
311
+ channelId: row.channelId,
312
+ threadRootId: row.threadRootId,
313
+ });
314
+ const fallbackParticipant = recentParticipants.find((participant) => Boolean(conversationManager.getAgent(participant.agentId)));
315
+ if (!fallbackParticipant)
316
+ return null;
317
+ return conversationManager.openAgentChannelThread(fallbackParticipant.agentId, row.channelId, row.threadRootId);
318
+ }
319
+ function deleteLatestQueuedFollowupPrompt(db, params) {
320
+ db.prepare(`DELETE FROM conversation_prompt_queue
321
+ WHERE queue_id IN (
322
+ SELECT queue_id
323
+ FROM conversation_prompt_queue
324
+ WHERE conversation_id = ?
325
+ AND prompt_text = ?
326
+ AND record_as_user_message = 0
327
+ AND created_at >= ?
328
+ ORDER BY queue_id DESC
329
+ LIMIT 1
330
+ )`).run(params.conversationId, params.promptText, params.createdAfter);
331
+ }
332
+ function restoreIdleStatusAfterQueuedFollowupRemoval(db, conversationId) {
333
+ const row = db.prepare(`SELECT session_key as sessionKey,
334
+ status
335
+ FROM conversations
336
+ WHERE id = ?
337
+ LIMIT 1`).get(conversationId);
338
+ if (!row || row.status !== 'queued')
339
+ return;
340
+ const hasQueuedPrompt = Boolean(db.prepare(`SELECT 1
341
+ FROM conversation_prompt_queue
342
+ WHERE conversation_id = ?
343
+ AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
344
+ LIMIT 1`).get(conversationId));
345
+ if (hasQueuedPrompt)
346
+ return;
347
+ const hasOpenRun = Boolean(db.prepare(`SELECT 1
348
+ FROM runs
349
+ WHERE session_key = ? AND ended_at IS NULL
350
+ LIMIT 1`).get(row.sessionKey));
351
+ if (hasOpenRun)
352
+ return;
353
+ db.prepare(`UPDATE conversations
354
+ SET status = 'idle', updated_at = ?
355
+ WHERE id = ? AND status = 'queued'`).run(Date.now(), conversationId);
356
+ }
357
+ function buildFollowupOptions(params) {
358
+ const conversation = params.conversationManager.getConversation(params.conversationId);
359
+ if (!conversation?.agentId || !conversation.replyTarget)
360
+ return null;
361
+ const activationMetadata = {
362
+ systemFollowup: {
363
+ kind: TASK_THREAD_FOLLOWUP_KIND,
364
+ followupId: params.followupId,
365
+ taskId: params.task.taskId,
366
+ channelId: params.channelId,
367
+ threadRootId: params.threadRootId,
368
+ delayMs: params.delayMs,
369
+ baselineThreadSeq: params.triggerSeq - 1,
370
+ },
371
+ };
372
+ if (conversation.threadKind === 'direct') {
373
+ const envelope = buildDirectSurfaceActivationEnvelope(params.db, {
374
+ agentId: conversation.agentId,
375
+ channelId: params.channelId,
376
+ replyTarget: conversation.replyTarget,
377
+ triggerSeq: params.triggerSeq,
378
+ threadRootId: params.threadRootId,
379
+ forceTaskNotes: true,
380
+ });
381
+ return {
382
+ promptText: buildTaskThreadFollowupPrompt({
383
+ replyTarget: conversation.replyTarget,
384
+ task: params.task,
385
+ delayMs: params.delayMs,
386
+ }),
387
+ options: {
388
+ recordAsUserMessage: false,
389
+ activationContextText: envelope.activationContextText,
390
+ activationContextSections: envelope.activationContextSections,
391
+ activationContextMode: 'resume_only',
392
+ resumeContextText: envelope.resumeContextText,
393
+ resumeContextSections: envelope.resumeContextSections,
394
+ replayOverlapRecentMessages: envelope.replayOverlapRecentMessages,
395
+ activationMetadata,
396
+ },
397
+ };
398
+ }
399
+ const channel = params.conversationManager.getChannel(params.channelId);
400
+ const envelope = buildChannelSurfaceActivationEnvelope(params.db, {
401
+ agentId: conversation.agentId,
402
+ channelId: params.channelId,
403
+ replyTarget: conversation.replyTarget,
404
+ triggerSeq: params.triggerSeq,
405
+ threadRootId: params.threadRootId,
406
+ channelName: channel?.name,
407
+ reason: 'thread_reply',
408
+ });
409
+ return {
410
+ promptText: buildTaskThreadFollowupPrompt({
411
+ replyTarget: conversation.replyTarget,
412
+ task: params.task,
413
+ delayMs: params.delayMs,
414
+ }),
415
+ options: {
416
+ recordAsUserMessage: false,
417
+ activationContextText: envelope.activationContextText,
418
+ activationContextSections: envelope.activationContextSections,
419
+ activationContextMode: 'resume_only',
420
+ resumeContextText: envelope.resumeContextText,
421
+ resumeContextSections: envelope.resumeContextSections,
422
+ replayOverlapRecentMessages: envelope.replayOverlapRecentMessages,
423
+ activationMetadata,
424
+ },
425
+ };
426
+ }
427
+ async function processFollowupRow(params) {
428
+ const { db, row, now, conversationManager, nodeRegistry } = params;
429
+ const task = loadOpenTask(db, row.taskId);
430
+ if (task && isBlockedTask(task)) {
431
+ return;
432
+ }
433
+ if (!task || !shouldKeepTaskThreadFollowup(task)) {
434
+ appendTaskThreadFollowupEvent(db, row.createdByRunId, {
435
+ followupId: row.followupId,
436
+ state: 'skipped',
437
+ reason: task ? 'task_closed' : 'task_missing',
438
+ kind: row.kind,
439
+ taskId: row.taskId,
440
+ channelId: row.channelId,
441
+ threadRootId: row.threadRootId,
442
+ });
443
+ deleteFollowup(db, row.followupId);
444
+ return;
445
+ }
446
+ const latestThreadSeq = loadLatestThreadSeq(db, {
447
+ channelId: row.channelId,
448
+ threadRootId: row.threadRootId,
449
+ });
450
+ if (latestThreadSeq > row.baselineThreadSeq) {
451
+ appendTaskThreadFollowupEvent(db, row.createdByRunId, {
452
+ followupId: row.followupId,
453
+ state: 'cancelled',
454
+ reason: 'thread_activity',
455
+ kind: row.kind,
456
+ taskId: row.taskId,
457
+ channelId: row.channelId,
458
+ threadRootId: row.threadRootId,
459
+ latestThreadSeq,
460
+ baselineThreadSeq: row.baselineThreadSeq,
461
+ });
462
+ deleteFollowup(db, row.followupId);
463
+ return;
464
+ }
465
+ const conversation = row.channelId.startsWith('dm:')
466
+ ? resolveDmConversation(db, conversationManager, row)
467
+ : resolveChannelConversation(conversationManager, task, row);
468
+ if (!conversation?.id) {
469
+ appendTaskThreadFollowupEvent(db, row.createdByRunId, {
470
+ followupId: row.followupId,
471
+ state: 'skipped',
472
+ reason: 'no_target_conversation',
473
+ kind: row.kind,
474
+ taskId: row.taskId,
475
+ channelId: row.channelId,
476
+ threadRootId: row.threadRootId,
477
+ });
478
+ deleteFollowup(db, row.followupId);
479
+ return;
480
+ }
481
+ if (!canAttemptFollowupDispatch(db, nodeRegistry, conversation.id)) {
482
+ return;
483
+ }
484
+ const prompt = buildFollowupOptions({
485
+ db,
486
+ conversationManager,
487
+ conversationId: conversation.id,
488
+ channelId: row.channelId,
489
+ threadRootId: row.threadRootId,
490
+ followupId: row.followupId,
491
+ task,
492
+ delayMs: readFollowupDelayMsFromRow(row),
493
+ triggerSeq: Math.max(row.baselineThreadSeq, latestThreadSeq) + 1,
494
+ });
495
+ if (!prompt) {
496
+ appendTaskThreadFollowupEvent(db, row.createdByRunId, {
497
+ followupId: row.followupId,
498
+ state: 'skipped',
499
+ reason: 'target_context_missing',
500
+ kind: row.kind,
501
+ taskId: row.taskId,
502
+ channelId: row.channelId,
503
+ threadRootId: row.threadRootId,
504
+ });
505
+ deleteFollowup(db, row.followupId);
506
+ return;
507
+ }
508
+ const attemptStartedAt = now;
509
+ try {
510
+ const result = await conversationManager.submitPrompt(conversation.id, prompt.promptText, prompt.options);
511
+ if (result.queued) {
512
+ deleteLatestQueuedFollowupPrompt(db, {
513
+ conversationId: conversation.id,
514
+ promptText: prompt.promptText,
515
+ createdAfter: attemptStartedAt,
516
+ });
517
+ restoreIdleStatusAfterQueuedFollowupRemoval(db, conversation.id);
518
+ return;
519
+ }
520
+ appendTaskThreadFollowupEvent(db, row.createdByRunId, {
521
+ followupId: row.followupId,
522
+ state: 'dispatched',
523
+ kind: row.kind,
524
+ taskId: row.taskId,
525
+ channelId: row.channelId,
526
+ threadRootId: row.threadRootId,
527
+ targetConversationId: conversation.id,
528
+ reminderRunId: result.runId ?? null,
529
+ });
530
+ deleteFollowup(db, row.followupId);
531
+ }
532
+ catch (error) {
533
+ log.warn('[task-thread-followup] dispatch failed; keeping pending follow-up', {
534
+ followupId: row.followupId,
535
+ taskId: row.taskId,
536
+ conversationId: conversation.id,
537
+ error: String(error?.message ?? error),
538
+ });
539
+ }
540
+ }
541
+ export async function processDueTaskThreadFollowups(params) {
542
+ const now = params.now ?? Date.now();
543
+ const rows = params.db.prepare(`SELECT followup_id as followupId,
544
+ kind,
545
+ task_id as taskId,
546
+ channel_id as channelId,
547
+ thread_root_id as threadRootId,
548
+ source_conversation_id as sourceConversationId,
549
+ created_by_run_id as createdByRunId,
550
+ baseline_thread_seq as baselineThreadSeq,
551
+ scheduled_for as scheduledFor,
552
+ created_at as createdAt,
553
+ updated_at as updatedAt
554
+ FROM task_thread_followups
555
+ WHERE scheduled_for <= ?
556
+ ORDER BY scheduled_for ASC, created_at ASC`).all(now);
557
+ for (const row of rows) {
558
+ await processFollowupRow({
559
+ db: params.db,
560
+ row,
561
+ now,
562
+ conversationManager: params.conversationManager,
563
+ nodeRegistry: params.nodeRegistry,
564
+ });
565
+ }
566
+ }
567
+ export function startTaskThreadFollowupService(params) {
568
+ const intervalMs = Math.max(5_000, params.intervalMs ?? TASK_THREAD_FOLLOWUP_POLL_INTERVAL_MS);
569
+ let timer = null;
570
+ let running = false;
571
+ const tick = async () => {
572
+ if (running)
573
+ return;
574
+ running = true;
575
+ try {
576
+ await processDueTaskThreadFollowups({
577
+ db: params.db,
578
+ conversationManager: params.conversationManager,
579
+ nodeRegistry: params.nodeRegistry,
580
+ });
581
+ }
582
+ finally {
583
+ running = false;
584
+ }
585
+ };
586
+ timer = setInterval(() => {
587
+ void tick();
588
+ }, intervalMs);
589
+ timer.unref?.();
590
+ return {
591
+ tick,
592
+ stop: () => {
593
+ if (!timer)
594
+ return;
595
+ clearInterval(timer);
596
+ timer = null;
597
+ },
598
+ };
599
+ }