@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,263 @@
1
+ import { createPromptContextSection, } from './promptContextSections.js';
2
+ import { deriveTaskLifecycleKindFromStatusChange, extractTaskLifecycleTitle, formatTaskLifecyclePromptLine, inferTaskLifecycleKindFromText, TASK_LIFECYCLE_SOURCE, } from './taskLifecycleMessages.js';
3
+ import { getBoundTaskForThread } from './threadTaskBindings.js';
4
+ const SURFACE_SYSTEM_STATUS_HEADER = '[System status messages on this surface]';
5
+ const DEFAULT_SURFACE_SYSTEM_STATUS_LIMIT = 10;
6
+ const SURFACE_SYSTEM_STATUS_TRUNCATED_HINT = '- Older task status summaries were omitted. Use bigbang task history or bigbang message read if you need full lifecycle details.';
7
+ const INCLUDED_TASK_EVENT_TYPES = [
8
+ 'handoff_started',
9
+ 'handoff_failed',
10
+ 'status_changed',
11
+ ];
12
+ export function buildSurfaceSystemStatusSection(db, params) {
13
+ const channelId = params.channelId.trim();
14
+ const replyTarget = params.replyTarget.trim();
15
+ if (!channelId || !replyTarget)
16
+ return null;
17
+ const maxTasks = Math.max(1, params.maxEvents ?? DEFAULT_SURFACE_SYSTEM_STATUS_LIMIT);
18
+ const structuredRows = loadStructuredSurfaceStatusRows(db, {
19
+ channelId,
20
+ replyTarget,
21
+ threadRootId: params.threadRootId ?? null,
22
+ boundTaskId: params.boundTaskId ?? null,
23
+ maxTasks: maxTasks + 1,
24
+ });
25
+ if (structuredRows.length > 0) {
26
+ const truncated = structuredRows.length > maxTasks;
27
+ const visibleRows = takeMostRecentRows(structuredRows, maxTasks);
28
+ return createPromptContextSection('surface_system_status', [
29
+ SURFACE_SYSTEM_STATUS_HEADER,
30
+ ...visibleRows.map(renderStructuredSurfaceStatusRow),
31
+ ...(truncated ? [SURFACE_SYSTEM_STATUS_TRUNCATED_HINT] : []),
32
+ ].join('\n'));
33
+ }
34
+ // New prompt facts should come from structured task_events above. This
35
+ // fallback is only for legacy rows written before task_events were complete;
36
+ // task_lifecycle channel messages remain a UI display projection otherwise.
37
+ if (params.threadRootId)
38
+ return null;
39
+ const legacyRows = loadLegacyRootSurfaceStatusRows(db, {
40
+ channelId,
41
+ replyTarget,
42
+ maxTasks: maxTasks + 1,
43
+ });
44
+ if (legacyRows.length === 0)
45
+ return null;
46
+ const truncated = legacyRows.length > maxTasks;
47
+ const visibleRows = takeMostRecentRows(legacyRows, maxTasks);
48
+ return createPromptContextSection('surface_system_status', [
49
+ SURFACE_SYSTEM_STATUS_HEADER,
50
+ ...visibleRows.map(renderLegacySurfaceStatusRow),
51
+ ...(truncated ? [SURFACE_SYSTEM_STATUS_TRUNCATED_HINT] : []),
52
+ ].join('\n'));
53
+ }
54
+ function loadStructuredSurfaceStatusRows(db, params) {
55
+ const eventTypesSql = INCLUDED_TASK_EVENT_TYPES.map(() => '?').join(', ');
56
+ if (params.threadRootId) {
57
+ const boundTaskId = params.boundTaskId?.trim()
58
+ || getBoundTaskForThread(db, {
59
+ channelId: params.channelId,
60
+ threadRootId: params.threadRootId,
61
+ })?.taskId;
62
+ if (!boundTaskId)
63
+ return [];
64
+ return reverseChronologicalRows(db.prepare(`SELECT e.event_id as eventId,
65
+ e.task_id as taskId,
66
+ e.event_type as eventType,
67
+ e.actor_name as actorName,
68
+ e.from_status as fromStatus,
69
+ e.to_status as toStatus,
70
+ e.claimed_by_name_after as claimedByNameAfter,
71
+ e.thread_target as threadTarget,
72
+ e.created_at as createdAt,
73
+ t.task_number as taskNumber,
74
+ t.title as title
75
+ FROM task_events e
76
+ JOIN tasks t ON t.task_id = e.task_id
77
+ WHERE e.task_id = ?
78
+ AND e.event_type IN (${eventTypesSql})
79
+ ORDER BY e.created_at DESC, e.event_id DESC
80
+ LIMIT 1`).all(boundTaskId, ...INCLUDED_TASK_EVENT_TYPES));
81
+ }
82
+ if (params.channelId.startsWith('dm:') && params.replyTarget.startsWith('dm:@')) {
83
+ return reverseChronologicalRows(db.prepare(`SELECT eventId,
84
+ taskId,
85
+ eventType,
86
+ actorName,
87
+ fromStatus,
88
+ toStatus,
89
+ claimedByNameAfter,
90
+ threadTarget,
91
+ createdAt,
92
+ taskNumber,
93
+ title
94
+ FROM (
95
+ SELECT e.event_id as eventId,
96
+ e.task_id as taskId,
97
+ e.event_type as eventType,
98
+ e.actor_name as actorName,
99
+ e.from_status as fromStatus,
100
+ e.to_status as toStatus,
101
+ e.claimed_by_name_after as claimedByNameAfter,
102
+ e.thread_target as threadTarget,
103
+ e.created_at as createdAt,
104
+ t.task_number as taskNumber,
105
+ t.title as title,
106
+ ROW_NUMBER() OVER (
107
+ PARTITION BY e.task_id
108
+ ORDER BY e.created_at DESC, e.event_id DESC
109
+ ) as taskEventRank
110
+ FROM task_events e
111
+ JOIN tasks t ON t.task_id = e.task_id
112
+ LEFT JOIN channel_messages cm ON cm.message_id = t.message_id
113
+ WHERE e.channel_id = ?
114
+ AND COALESCE(NULLIF(t.dm_target, ''), cm.target) = ?
115
+ AND e.event_type IN (${eventTypesSql})
116
+ )
117
+ WHERE taskEventRank = 1
118
+ ORDER BY createdAt DESC, eventId DESC
119
+ LIMIT ?`).all(params.channelId, params.replyTarget, ...INCLUDED_TASK_EVENT_TYPES, params.maxTasks));
120
+ }
121
+ return reverseChronologicalRows(db.prepare(`SELECT eventId,
122
+ taskId,
123
+ eventType,
124
+ actorName,
125
+ fromStatus,
126
+ toStatus,
127
+ claimedByNameAfter,
128
+ threadTarget,
129
+ createdAt,
130
+ taskNumber,
131
+ title
132
+ FROM (
133
+ SELECT e.event_id as eventId,
134
+ e.task_id as taskId,
135
+ e.event_type as eventType,
136
+ e.actor_name as actorName,
137
+ e.from_status as fromStatus,
138
+ e.to_status as toStatus,
139
+ e.claimed_by_name_after as claimedByNameAfter,
140
+ e.thread_target as threadTarget,
141
+ e.created_at as createdAt,
142
+ t.task_number as taskNumber,
143
+ t.title as title,
144
+ ROW_NUMBER() OVER (
145
+ PARTITION BY e.task_id
146
+ ORDER BY e.created_at DESC, e.event_id DESC
147
+ ) as taskEventRank
148
+ FROM task_events e
149
+ JOIN tasks t ON t.task_id = e.task_id
150
+ WHERE e.channel_id = ?
151
+ AND e.event_type IN (${eventTypesSql})
152
+ )
153
+ WHERE taskEventRank = 1
154
+ ORDER BY createdAt DESC, eventId DESC
155
+ LIMIT ?`).all(params.channelId, ...INCLUDED_TASK_EVENT_TYPES, params.maxTasks));
156
+ }
157
+ function loadLegacyRootSurfaceStatusRows(db, params) {
158
+ // Compatibility-only path: parse historical task_lifecycle UI messages when
159
+ // no structured task_events exist for the root surface.
160
+ return db.prepare(`SELECT senderName,
161
+ content,
162
+ createdAt,
163
+ seq,
164
+ taskNumber,
165
+ taskStatus,
166
+ taskAssigneeName
167
+ FROM (
168
+ SELECT cm.sender_name as senderName,
169
+ cm.content as content,
170
+ cm.created_at as createdAt,
171
+ cm.seq as seq,
172
+ COALESCE(
173
+ t.task_number,
174
+ CAST(substr(cm.content, 2, instr(cm.content, ' ') - 2) AS INTEGER)
175
+ ) as taskNumber,
176
+ t.status as taskStatus,
177
+ t.claimed_by_name as taskAssigneeName,
178
+ ROW_NUMBER() OVER (
179
+ PARTITION BY COALESCE(
180
+ t.task_id,
181
+ CAST(substr(cm.content, 2, instr(cm.content, ' ') - 2) AS TEXT),
182
+ cm.message_id
183
+ )
184
+ ORDER BY cm.created_at DESC, cm.seq DESC
185
+ ) as taskEventRank
186
+ FROM channel_messages cm
187
+ LEFT JOIN tasks t ON t.channel_id = cm.channel_id
188
+ AND t.task_number = CAST(substr(cm.content, 2, instr(cm.content, ' ') - 2) AS INTEGER)
189
+ WHERE cm.channel_id = ?
190
+ AND cm.target = ?
191
+ AND cm.thread_root_id IS NULL
192
+ AND cm.message_source = ?
193
+ )
194
+ WHERE taskEventRank = 1
195
+ ORDER BY createdAt DESC, seq DESC
196
+ LIMIT ?`).all(params.channelId, params.replyTarget, TASK_LIFECYCLE_SOURCE, params.maxTasks)
197
+ .reverse();
198
+ }
199
+ function reverseChronologicalRows(rows) {
200
+ return rows.reverse();
201
+ }
202
+ function takeMostRecentRows(rows, maxRows) {
203
+ return rows.length > maxRows ? rows.slice(rows.length - maxRows) : rows;
204
+ }
205
+ function renderStructuredSurfaceStatusRow(row) {
206
+ const kind = kindForStructuredRow(row);
207
+ if (!kind) {
208
+ return `- ${formatTaskLifecyclePromptLine({
209
+ createdAt: row.createdAt,
210
+ kind: 'in_progress',
211
+ taskNumber: row.taskNumber,
212
+ title: row.title,
213
+ actorName: displayActorForStructuredRow(row),
214
+ })}`;
215
+ }
216
+ return `- ${formatTaskLifecyclePromptLine({
217
+ createdAt: row.createdAt,
218
+ kind,
219
+ taskNumber: row.taskNumber,
220
+ title: row.title,
221
+ actorName: displayActorForStructuredRow(row),
222
+ })}`;
223
+ }
224
+ function renderLegacySurfaceStatusRow(row) {
225
+ const inferred = inferTaskLifecycleKindFromText(row.content, row.taskStatus);
226
+ const parsed = extractTaskLifecycleTitle(row.content);
227
+ if (!inferred) {
228
+ const actor = displayActor(row.senderName, row.taskAssigneeName);
229
+ return `- ${formatTaskLifecyclePromptLine({
230
+ createdAt: row.createdAt,
231
+ kind: 'in_progress',
232
+ taskNumber: row.taskNumber ?? parsed.taskNumber,
233
+ title: parsed.title,
234
+ actorName: actor,
235
+ })}`;
236
+ }
237
+ return `- ${formatTaskLifecyclePromptLine({
238
+ createdAt: row.createdAt,
239
+ kind: inferred,
240
+ taskNumber: row.taskNumber ?? parsed.taskNumber,
241
+ title: parsed.title,
242
+ actorName: displayActor(row.senderName, row.taskAssigneeName),
243
+ })}`;
244
+ }
245
+ function kindForStructuredRow(row) {
246
+ if (row.eventType === 'handoff_started')
247
+ return 'started';
248
+ if (row.eventType === 'handoff_failed')
249
+ return 'handoff_failed';
250
+ return deriveTaskLifecycleKindFromStatusChange(row.fromStatus ?? '', row.toStatus ?? '');
251
+ }
252
+ function displayActorForStructuredRow(row) {
253
+ return displayActor(row.actorName, row.claimedByNameAfter);
254
+ }
255
+ function displayActor(actorName, fallbackName) {
256
+ const normalized = normalizeInline(actorName);
257
+ if (normalized && normalized.toLowerCase() !== 'system')
258
+ return normalized;
259
+ return normalizeInline(fallbackName) || null;
260
+ }
261
+ function normalizeInline(value) {
262
+ return (value ?? '').replace(/\s+/gu, ' ').trim();
263
+ }
@@ -0,0 +1,77 @@
1
+ import { normalizeThreadShortIdInput } from '@bbigbang/protocol';
2
+ export const TARGET_PARTICIPANT_ACTIVE_WINDOW_MS = 60 * 60 * 1000;
3
+ function normalizeThreadRootId(threadRootId) {
4
+ return normalizeThreadShortIdInput(threadRootId) ?? '';
5
+ }
6
+ export function upsertTargetParticipant(db, params) {
7
+ const now = params.lastActiveAt ?? Date.now();
8
+ const joinedAt = params.joinedAt ?? now;
9
+ const threadRootId = normalizeThreadRootId(params.threadRootId);
10
+ const isChannelRoot = threadRootId === '';
11
+ const incomingRole = isChannelRoot ? 'participant' : (params.role ?? 'participant');
12
+ const existing = db.prepare(`SELECT role, joined_at as joinedAt
13
+ FROM target_participants
14
+ WHERE agent_id = ? AND channel_id = ? AND thread_root_id = ?`).get(params.agentId, params.channelId, threadRootId);
15
+ const role = isChannelRoot
16
+ ? 'participant'
17
+ : (existing?.role === 'owner' || incomingRole === 'owner' ? 'owner' : 'participant');
18
+ db.prepare(`INSERT INTO target_participants(agent_id, channel_id, thread_root_id, role, joined_at, last_active_at)
19
+ VALUES(?, ?, ?, ?, ?, ?)
20
+ ON CONFLICT(agent_id, channel_id, thread_root_id) DO UPDATE SET
21
+ role = excluded.role,
22
+ joined_at = MIN(target_participants.joined_at, excluded.joined_at),
23
+ last_active_at = MAX(target_participants.last_active_at, excluded.last_active_at)`).run(params.agentId, params.channelId, threadRootId, role, existing?.joinedAt ?? joinedAt, now);
24
+ }
25
+ export function listTargetParticipants(db, params) {
26
+ return db.prepare(`SELECT tp.agent_id as agentId,
27
+ a.name as name,
28
+ tp.role as role,
29
+ tp.joined_at as joinedAt,
30
+ tp.last_active_at as lastActiveAt
31
+ FROM target_participants tp
32
+ JOIN agents a ON a.agent_id = tp.agent_id
33
+ WHERE tp.channel_id = ? AND tp.thread_root_id = ?
34
+ ORDER BY
35
+ CASE tp.role WHEN 'owner' THEN 0 ELSE 1 END ASC,
36
+ tp.last_active_at DESC,
37
+ a.name ASC`).all(params.channelId, normalizeThreadRootId(params.threadRootId));
38
+ }
39
+ export function listRecentTargetParticipants(db, params) {
40
+ return db.prepare(`SELECT tp.agent_id as agentId,
41
+ a.name as name,
42
+ tp.role as role,
43
+ tp.joined_at as joinedAt,
44
+ tp.last_active_at as lastActiveAt
45
+ FROM target_participants tp
46
+ JOIN agents a ON a.agent_id = tp.agent_id
47
+ WHERE tp.channel_id = ? AND tp.thread_root_id = ? AND tp.last_active_at >= ?
48
+ ORDER BY
49
+ CASE tp.role WHEN 'owner' THEN 0 ELSE 1 END ASC,
50
+ tp.last_active_at DESC,
51
+ a.name ASC`).all(params.channelId, normalizeThreadRootId(params.threadRootId), params.activeSince);
52
+ }
53
+ export function setTargetOwner(db, params) {
54
+ const threadRootId = normalizeThreadRootId(params.threadRootId);
55
+ db.prepare(`UPDATE target_participants
56
+ SET role = 'participant'
57
+ WHERE channel_id = ? AND thread_root_id = ?`).run(params.channelId, threadRootId);
58
+ if (params.agentId) {
59
+ upsertTargetParticipant(db, {
60
+ agentId: params.agentId,
61
+ channelId: params.channelId,
62
+ threadRootId,
63
+ role: 'owner',
64
+ lastActiveAt: params.lastActiveAt,
65
+ });
66
+ }
67
+ }
68
+ export function deleteTargetParticipantsForAgent(db, agentId) {
69
+ db.prepare('DELETE FROM target_participants WHERE agent_id = ?').run(agentId);
70
+ }
71
+ export function deleteTargetParticipantsForAgentInChannel(db, params) {
72
+ db.prepare(`DELETE FROM target_participants
73
+ WHERE agent_id = ? AND channel_id = ?`).run(params.agentId, params.channelId);
74
+ }
75
+ export function deleteTargetParticipantsForChannel(db, channelId) {
76
+ db.prepare('DELETE FROM target_participants WHERE channel_id = ?').run(channelId);
77
+ }
@@ -0,0 +1,49 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { buildThreadShortId } from '@bbigbang/protocol';
3
+ function isThreadTarget(target) {
4
+ if (target.startsWith('dm:@'))
5
+ return target.split(':').length >= 3;
6
+ if (target.startsWith('#'))
7
+ return target.includes(':');
8
+ return false;
9
+ }
10
+ export function buildTaskEventThreadTarget(params) {
11
+ if (params.explicitThreadTarget?.trim())
12
+ return params.explicitThreadTarget.trim();
13
+ const sourceTarget = params.sourceTarget?.trim();
14
+ const messageId = params.messageId?.trim();
15
+ if (!sourceTarget || !messageId)
16
+ return null;
17
+ if (!(sourceTarget.startsWith('#') || sourceTarget.startsWith('dm:@')))
18
+ return null;
19
+ if (isThreadTarget(sourceTarget))
20
+ return sourceTarget;
21
+ return `${sourceTarget}:${buildThreadShortId(messageId)}`;
22
+ }
23
+ export function appendTaskEvent(db, params) {
24
+ db.prepare(`INSERT INTO task_events(
25
+ event_id,
26
+ task_id,
27
+ agent_task_ref,
28
+ channel_id,
29
+ task_number,
30
+ event_type,
31
+ actor_type,
32
+ actor_id,
33
+ actor_name,
34
+ from_status,
35
+ to_status,
36
+ claimed_by_agent_id_after,
37
+ claimed_by_name_after,
38
+ message_id,
39
+ thread_target,
40
+ created_at
41
+ )
42
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(randomUUID(), params.taskId, params.agentTaskRef, params.channelId, params.taskNumber, params.eventType, params.actorType, params.actorId ?? null, params.actorName ?? null, params.fromStatus ?? null, params.toStatus ?? null, params.claimedByAgentIdAfter ?? null, params.claimedByNameAfter ?? null, params.messageId ?? null, params.threadTarget ?? null, params.createdAt ?? Date.now());
43
+ }
44
+ export function deleteTaskEventsForChannel(db, channelId) {
45
+ db.prepare(`DELETE FROM task_events WHERE channel_id = ?`).run(channelId);
46
+ }
47
+ export function deleteTaskEventsForTask(db, taskId) {
48
+ db.prepare(`DELETE FROM task_events WHERE task_id = ?`).run(taskId);
49
+ }
@@ -0,0 +1,165 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { BEIJING_TIME_ZONE } from '@bbigbang/protocol';
3
+ import { allocateNextChannelMessageSeq } from './channelMessageSequences.js';
4
+ export const TASK_LIFECYCLE_SOURCE = 'task_lifecycle';
5
+ const taskLifecycleTimeFormatter = new Intl.DateTimeFormat('en-US', {
6
+ timeZone: BEIJING_TIME_ZONE,
7
+ hour: '2-digit',
8
+ minute: '2-digit',
9
+ hour12: true,
10
+ });
11
+ const taskLifecycleDateFormatter = new Intl.DateTimeFormat('en-US', {
12
+ timeZone: BEIJING_TIME_ZONE,
13
+ month: '2-digit',
14
+ day: '2-digit',
15
+ });
16
+ const taskLifecycleYearFormatter = new Intl.DateTimeFormat('en-US', {
17
+ timeZone: BEIJING_TIME_ZONE,
18
+ year: 'numeric',
19
+ });
20
+ export function deriveTaskLifecycleKindFromStatusChange(fromStatus, toStatus) {
21
+ if (fromStatus === 'todo' && toStatus === 'in_progress')
22
+ return 'started';
23
+ if (toStatus === 'in_progress')
24
+ return 'in_progress';
25
+ if (toStatus === 'in_review')
26
+ return 'in_review';
27
+ if (toStatus === 'done')
28
+ return 'done';
29
+ return null;
30
+ }
31
+ export function inferTaskLifecycleKindFromText(content, taskStatus) {
32
+ const normalized = content.toLowerCase();
33
+ if (normalized.includes('could not start its task thread automatically'))
34
+ return 'handoff_failed';
35
+ if (normalized.includes('moved to in review'))
36
+ return 'in_review';
37
+ if (normalized.includes('marked done'))
38
+ return 'done';
39
+ if (normalized.includes('moved back to in progress')
40
+ || normalized.includes('moved to in progress')) {
41
+ return 'in_progress';
42
+ }
43
+ if (normalized.startsWith('started '))
44
+ return 'started';
45
+ if (taskStatus === 'in_review')
46
+ return 'in_review';
47
+ if (taskStatus === 'done')
48
+ return 'done';
49
+ if (taskStatus === 'in_progress')
50
+ return 'in_progress';
51
+ return null;
52
+ }
53
+ export function extractTaskLifecycleTitle(content) {
54
+ const matched = content.match(/#(\d+)\s+"([^"]+)"/u);
55
+ if (!matched) {
56
+ return {
57
+ taskNumber: null,
58
+ title: null,
59
+ };
60
+ }
61
+ return {
62
+ taskNumber: Number(matched[1]),
63
+ title: matched[2] ?? null,
64
+ };
65
+ }
66
+ export function buildTaskLifecycleText(params) {
67
+ const label = `#${params.taskNumber} "${params.title}"`;
68
+ switch (params.kind) {
69
+ case 'started':
70
+ return `Started ${label}. Detailed work continues in the task thread.`;
71
+ case 'in_progress':
72
+ return params.fromStatus === 'in_review'
73
+ ? `${label} moved back to in progress.`
74
+ : `${label} moved to in progress.`;
75
+ case 'done':
76
+ return `${label} marked done.`;
77
+ case 'handoff_failed':
78
+ return `${label} could not start its task thread automatically.`;
79
+ case 'in_review':
80
+ default:
81
+ return `${label} moved to in review.`;
82
+ }
83
+ }
84
+ export function extractTaskLifecycleStatus(content) {
85
+ const normalized = content.toLowerCase();
86
+ if (normalized.includes('marked done.'))
87
+ return 'done';
88
+ if (normalized.includes('moved to in review.'))
89
+ return 'in_review';
90
+ if (normalized.includes('moved back to in progress.')
91
+ || normalized.includes('moved to in progress.')) {
92
+ return 'in_progress';
93
+ }
94
+ return null;
95
+ }
96
+ export function formatTaskLifecycleLead(params) {
97
+ const actor = normalizeInline(params.actorName) || 'System';
98
+ const taskLabel = formatDisplayTaskLabel(params.taskNumber, params.title);
99
+ switch (params.kind) {
100
+ case 'started':
101
+ return `๐Ÿš€ ${actor} started ${taskLabel}`;
102
+ case 'in_progress':
103
+ return `๐Ÿ”„ ${actor} moved ${taskLabel} to In Progress`;
104
+ case 'in_review':
105
+ return `๐Ÿ‘€ ${actor} moved ${taskLabel} to In Review`;
106
+ case 'done':
107
+ return `โœ… ${actor} moved ${taskLabel} to Done`;
108
+ case 'handoff_failed':
109
+ return `โš ๏ธ ${actor} failed to start ${taskLabel}`;
110
+ default:
111
+ return `${actor} updated ${taskLabel}`;
112
+ }
113
+ }
114
+ export function formatTaskLifecyclePromptLine(params) {
115
+ return `${formatTaskLifecycleTimestamp(params.createdAt)} ยท ${formatTaskLifecycleLead(params)}`;
116
+ }
117
+ export function insertTaskLifecycleMessage(db, params) {
118
+ const createdAt = Date.now();
119
+ const messageId = randomUUID();
120
+ const seq = allocateNextChannelMessageSeq(db, params.channelId);
121
+ const content = buildTaskLifecycleText(params);
122
+ const senderName = params.actorName?.trim() || 'system';
123
+ db.prepare(`INSERT INTO channel_messages(
124
+ message_id, channel_id, sender_id, sender_name, sender_type, target, content,
125
+ seq, created_at, thread_root_id, message_kind, message_source
126
+ )
127
+ VALUES(?, ?, 'system', ?, 'system', ?, ?, ?, ?, NULL, 'task_event', ?)`).run(messageId, params.channelId, senderName, params.primaryTarget, content, seq, createdAt, TASK_LIFECYCLE_SOURCE);
128
+ return {
129
+ messageId,
130
+ content,
131
+ seq,
132
+ createdAt,
133
+ senderName,
134
+ messageSource: TASK_LIFECYCLE_SOURCE,
135
+ taskNumber: params.taskNumber,
136
+ taskStatus: params.taskStatus,
137
+ taskAssigneeName: params.taskAssigneeName ?? null,
138
+ };
139
+ }
140
+ function formatDisplayTaskLabel(taskNumber, title) {
141
+ const normalizedTitle = normalizeInline(title);
142
+ if (normalizedTitle) {
143
+ return `#${taskNumber ?? '?'} "${normalizedTitle.replaceAll('"', '\\"')}"`;
144
+ }
145
+ return taskNumber != null ? `#${taskNumber}` : 'task';
146
+ }
147
+ function getBeijingDayStamp(timestamp) {
148
+ const parts = taskLifecycleDateFormatter.formatToParts(timestamp);
149
+ const year = Number(taskLifecycleYearFormatter.format(timestamp));
150
+ const month = Number(parts.find((part) => part.type === 'month')?.value ?? '1');
151
+ const day = Number(parts.find((part) => part.type === 'day')?.value ?? '1');
152
+ return Date.UTC(year, month - 1, day);
153
+ }
154
+ function formatTaskLifecycleTimestamp(timestamp) {
155
+ const dayDiff = Math.round((getBeijingDayStamp(Date.now()) - getBeijingDayStamp(timestamp)) / 86_400_000);
156
+ const timeText = taskLifecycleTimeFormatter.format(timestamp);
157
+ if (dayDiff === 0)
158
+ return `Today ${timeText}`;
159
+ if (dayDiff === 1)
160
+ return `Yesterday ${timeText}`;
161
+ return `${taskLifecycleDateFormatter.format(timestamp)} ${timeText}`;
162
+ }
163
+ function normalizeInline(value) {
164
+ return (value ?? '').replace(/\s+/gu, ' ').trim();
165
+ }