@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,224 @@
1
+ import { buildThreadShortId, formatBeijingPromptTimestamp, } from '@bbigbang/protocol';
2
+ import { channelMemoryNotePath } from './channelMemoryNotes.js';
3
+ const WORK_LOG_PATH = 'notes/work-log/legacy.md';
4
+ const WORK_LOG_INSTRUCTION = 'Append important completed work, decisions, and follow-ups here.';
5
+ const CHANNEL_NOTE_INSTRUCTION = 'Durable notes and reset markers for this channel.';
6
+ const RECENT_DURABLE_OUTCOMES_START = '<!-- recent-durable-outcomes:start -->';
7
+ const RECENT_DURABLE_OUTCOMES_END = '<!-- recent-durable-outcomes:end -->';
8
+ const TASK_MEMORY_AUDIT_PATH = '.bigbang/task-memory-sync-audit.md';
9
+ const TASK_MEMORY_AUDIT_HEADER = '# Task Memory Sync Audit';
10
+ const TASK_MEMORY_AUDIT_INSTRUCTION = 'Framework-observed task status changes are recorded here for audit/debug only. Agents remain responsible for MEMORY.md and task notes.';
11
+ const TASK_MEMORY_SYNC_MODE_ENV = 'BIGBANG_TASK_MEMORY_SYNC_MODE';
12
+ function isNotFoundError(error) {
13
+ const message = String(error?.message ?? error);
14
+ return message.startsWith('not_found:');
15
+ }
16
+ function normalizeInlineNoteText(value, fallback) {
17
+ const normalized = (value ?? '')
18
+ .replace(/\s+/g, ' ')
19
+ .trim();
20
+ return normalized || fallback;
21
+ }
22
+ function fallbackTaskRef(taskId) {
23
+ return `task_${taskId.replace(/[^a-zA-Z0-9]/g, '').toLowerCase().slice(0, 12) || 'unknown'}`;
24
+ }
25
+ function normalizeManagedNoteSource(content) {
26
+ return content.replace(/\r\n/g, '\n');
27
+ }
28
+ function stripLeadingManagedHeader(params) {
29
+ let remaining = normalizeManagedNoteSource(params.content).trim();
30
+ if (!remaining.startsWith(params.titleLine))
31
+ return remaining;
32
+ remaining = remaining.slice(params.titleLine.length);
33
+ while (true) {
34
+ remaining = remaining.replace(/^(?:[ \t]*\n)+/u, '');
35
+ if (!params.introLine || !remaining.startsWith(params.introLine))
36
+ break;
37
+ remaining = remaining.slice(params.introLine.length);
38
+ }
39
+ return remaining.trim();
40
+ }
41
+ function stripStandaloneInstructionLines(content, instruction) {
42
+ const normalized = normalizeManagedNoteSource(content).trim();
43
+ if (!normalized)
44
+ return '';
45
+ return normalized
46
+ .replace(new RegExp(`(?:^|\\n\\n)${escapeRegex(instruction)}(?=\\n\\n|$)`, 'g'), '')
47
+ .replace(/\n{3,}/g, '\n\n')
48
+ .trim();
49
+ }
50
+ function buildWorkLogEntry(snapshot) {
51
+ return [
52
+ `## ${formatBeijingPromptTimestamp(snapshot.updatedAt)} — ${snapshot.taskRef}`,
53
+ `- target: ${normalizeInlineNoteText(snapshot.targetLabel, snapshot.channelId)}`,
54
+ `- root_message: ${normalizeInlineNoteText(snapshot.rootMessage, 'No root message recorded.')}`,
55
+ `- key decision: Task moved to ${snapshot.status}.`,
56
+ `- risk / follow-up: ${normalizeInlineNoteText(snapshot.residualRisks, 'None recorded.')}`,
57
+ '',
58
+ ].join('\n');
59
+ }
60
+ function appendWorkLog(existingContent, snapshot) {
61
+ const header = '# Work Log';
62
+ const body = stripLeadingManagedHeader({
63
+ content: existingContent,
64
+ titleLine: header,
65
+ introLine: WORK_LOG_INSTRUCTION,
66
+ });
67
+ const entry = buildWorkLogEntry(snapshot).trimEnd();
68
+ const dedupedBody = body.replace(new RegExp(`(?:^|\\n\\n)${escapeRegex(entry)}(?=\\n\\n|$)`, 'g'), '').trim();
69
+ const normalizedBody = stripStandaloneInstructionLines(dedupedBody, WORK_LOG_INSTRUCTION);
70
+ const nextBody = [normalizedBody, entry].filter(Boolean).join('\n\n');
71
+ return `${header}\n\n${WORK_LOG_INSTRUCTION}\n\n${nextBody}\n`;
72
+ }
73
+ function upsertChannelOutcomeSection(existingContent, channelName, snapshot) {
74
+ const titleLine = `# Channel: #${channelName}`;
75
+ const header = [
76
+ titleLine,
77
+ '',
78
+ CHANNEL_NOTE_INSTRUCTION,
79
+ ].join('\n');
80
+ const content = normalizeManagedNoteSource(existingContent).trim();
81
+ const body = stripLeadingManagedHeader({
82
+ content,
83
+ titleLine,
84
+ introLine: CHANNEL_NOTE_INSTRUCTION,
85
+ });
86
+ const managedPattern = new RegExp(`${escapeRegex(RECENT_DURABLE_OUTCOMES_START)}[\\s\\S]*?${escapeRegex(RECENT_DURABLE_OUTCOMES_END)}\\n*`, 'g');
87
+ const remainder = body.replace(managedPattern, '').trim();
88
+ const entry = `- ${formatBeijingPromptTimestamp(snapshot.updatedAt)} ${snapshot.taskRef} [${snapshot.status}] — ${normalizeInlineNoteText(snapshot.title, 'Untitled task')}`;
89
+ const existingManaged = content.match(new RegExp(`${escapeRegex(RECENT_DURABLE_OUTCOMES_START)}([\\s\\S]*?)${escapeRegex(RECENT_DURABLE_OUTCOMES_END)}`));
90
+ const priorLines = (existingManaged?.[1] ?? '')
91
+ .split('\n')
92
+ .map((line) => line.trim())
93
+ .filter((line) => line.startsWith('- '))
94
+ .filter((line) => !line.includes(`${snapshot.taskRef} [`));
95
+ const managedLines = [
96
+ '## Recent Durable Outcomes',
97
+ RECENT_DURABLE_OUTCOMES_START,
98
+ entry,
99
+ ...priorLines.slice(0, 4),
100
+ RECENT_DURABLE_OUTCOMES_END,
101
+ ];
102
+ const nextBody = [managedLines.join('\n'), remainder].filter(Boolean).join('\n\n');
103
+ return `${header}\n\n${nextBody}\n`;
104
+ }
105
+ function buildTaskSyncAuditEntry(snapshot) {
106
+ return [
107
+ `## ${formatBeijingPromptTimestamp(snapshot.updatedAt)} — ${snapshot.taskRef}`,
108
+ `- status: ${normalizeInlineNoteText(snapshot.status, 'unknown')}`,
109
+ `- target: ${normalizeInlineNoteText(snapshot.targetLabel, snapshot.channelId)}`,
110
+ `- root_message: ${normalizeInlineNoteText(snapshot.rootMessage, 'No root message recorded.')}`,
111
+ '- note: Framework observed this task status change in audit-only mode. The agent remains responsible for MEMORY.md, notes/tasks.md, and notes/work-log/<week>.md updates.',
112
+ '',
113
+ ].join('\n');
114
+ }
115
+ function appendTaskSyncAudit(existingContent, snapshot) {
116
+ const body = stripLeadingManagedHeader({
117
+ content: existingContent,
118
+ titleLine: TASK_MEMORY_AUDIT_HEADER,
119
+ introLine: TASK_MEMORY_AUDIT_INSTRUCTION,
120
+ });
121
+ const entry = buildTaskSyncAuditEntry(snapshot).trimEnd();
122
+ const dedupedBody = body.replace(new RegExp(`(?:^|\\n\\n)${escapeRegex(entry)}(?=\\n\\n|$)`, 'g'), '').trim();
123
+ const normalizedBody = stripStandaloneInstructionLines(dedupedBody, TASK_MEMORY_AUDIT_INSTRUCTION);
124
+ const nextBody = [normalizedBody, entry].filter(Boolean).join('\n\n');
125
+ return `${TASK_MEMORY_AUDIT_HEADER}\n\n${TASK_MEMORY_AUDIT_INSTRUCTION}\n\n${nextBody}\n`;
126
+ }
127
+ function normalizeTaskDurableNoteSyncMode(value) {
128
+ const normalized = (value ?? '').trim().toLowerCase();
129
+ if (!normalized)
130
+ return null;
131
+ if (normalized === 'legacy_memory' || normalized === 'legacy' || normalized === 'memory') {
132
+ return 'legacy_memory';
133
+ }
134
+ if (normalized === 'audit_only' || normalized === 'audit') {
135
+ return 'audit_only';
136
+ }
137
+ if (normalized === 'disabled' || normalized === 'off' || normalized === 'none') {
138
+ return 'disabled';
139
+ }
140
+ return null;
141
+ }
142
+ export function resolveTaskDurableNoteSyncMode(value) {
143
+ return normalizeTaskDurableNoteSyncMode(value) ?? 'audit_only';
144
+ }
145
+ function escapeRegex(text) {
146
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
147
+ }
148
+ async function readOptionalFile(broker, agent, relativePath, options) {
149
+ if (!agent.nodeId || !agent.workspacePath)
150
+ return '';
151
+ try {
152
+ const existing = await broker.readFile(agent.nodeId, agent.workspacePath, relativePath, options);
153
+ return existing.content;
154
+ }
155
+ catch (error) {
156
+ if (isNotFoundError(error))
157
+ return '';
158
+ throw error;
159
+ }
160
+ }
161
+ function loadTaskMemorySnapshot(db, taskId) {
162
+ const row = db.prepare(`SELECT t.task_id as taskId,
163
+ t.agent_task_ref as taskRef,
164
+ t.title,
165
+ t.status,
166
+ t.channel_id as channelId,
167
+ t.updated_at as updatedAt,
168
+ t.message_id as messageId,
169
+ COALESCE(t.dm_target, cm.target) as messageTarget,
170
+ cm.content as rootMessageContent,
171
+ c.name as channelName
172
+ FROM tasks t
173
+ LEFT JOIN channel_messages cm ON cm.message_id = t.message_id
174
+ LEFT JOIN channels c ON c.channel_id = t.channel_id
175
+ WHERE t.task_id = ?
176
+ LIMIT 1`).get(taskId);
177
+ if (!row)
178
+ return null;
179
+ const targetLabel = row.messageId && row.messageTarget
180
+ ? `${row.messageTarget}:${buildThreadShortId(row.messageId)}`
181
+ : row.messageTarget ?? (row.channelName ? `#${row.channelName}` : row.channelId);
182
+ return {
183
+ taskId: row.taskId,
184
+ taskRef: row.taskRef ?? fallbackTaskRef(row.taskId),
185
+ title: row.title,
186
+ status: row.status,
187
+ channelId: row.channelId,
188
+ channelName: row.channelName,
189
+ messageId: row.messageId,
190
+ messageTarget: row.messageTarget,
191
+ targetLabel,
192
+ updatedAt: row.updatedAt,
193
+ rootMessage: row.rootMessageContent?.trim() || null,
194
+ residualRisks: row.status === 'in_review' ? 'Awaiting human review or approval.' : 'None recorded.',
195
+ };
196
+ }
197
+ export async function syncTaskDurableNotesForAgent(params) {
198
+ const { db, broker, agent, taskId } = params;
199
+ if (!agent?.nodeId || !agent.workspacePath)
200
+ return;
201
+ const snapshot = loadTaskMemorySnapshot(db, taskId);
202
+ if (!snapshot)
203
+ return;
204
+ const mode = resolveTaskDurableNoteSyncMode(params.mode ?? process.env[TASK_MEMORY_SYNC_MODE_ENV]);
205
+ if (mode === 'disabled')
206
+ return;
207
+ if (mode === 'audit_only') {
208
+ const auditContent = await readOptionalFile(broker, agent, TASK_MEMORY_AUDIT_PATH, { scaffold: false });
209
+ await broker.writeFile(agent.nodeId, agent.workspacePath, TASK_MEMORY_AUDIT_PATH, appendTaskSyncAudit(auditContent, snapshot), 'overwrite', { scaffold: false });
210
+ return;
211
+ }
212
+ // legacy_memory keeps the previous framework-managed durable-note behavior
213
+ // as an optional fallback during migration. The agent remains responsible for
214
+ // MEMORY.md and notes/tasks.md in all modes.
215
+ if (snapshot.status === 'done') {
216
+ const workLogContent = await readOptionalFile(broker, agent, WORK_LOG_PATH);
217
+ await broker.writeFile(agent.nodeId, agent.workspacePath, WORK_LOG_PATH, appendWorkLog(workLogContent, snapshot), 'overwrite');
218
+ }
219
+ if (snapshot.channelName && !snapshot.channelId.startsWith('dm:')) {
220
+ const channelNotePath = channelMemoryNotePath(snapshot.channelName);
221
+ const channelContent = await readOptionalFile(broker, agent, channelNotePath);
222
+ await broker.writeFile(agent.nodeId, agent.workspacePath, channelNotePath, upsertChannelOutcomeSection(channelContent, snapshot.channelName, snapshot), 'overwrite');
223
+ }
224
+ }
@@ -0,0 +1,16 @@
1
+ export function allocateNextTaskNumber(db, channelId) {
2
+ db.prepare(`INSERT INTO channel_task_sequences(channel_id, next_task_number)
3
+ VALUES(
4
+ ?,
5
+ COALESCE((SELECT MAX(task_number) FROM tasks WHERE channel_id = ?), 0) + 1
6
+ )
7
+ ON CONFLICT(channel_id) DO NOTHING`).run(channelId, channelId);
8
+ const row = db.prepare(`UPDATE channel_task_sequences
9
+ SET next_task_number = next_task_number + 1
10
+ WHERE channel_id = ?
11
+ RETURNING next_task_number - 1 as taskNumber`).get(channelId);
12
+ if (!row) {
13
+ throw new Error(`Failed to allocate task number for channel ${channelId}`);
14
+ }
15
+ return row.taskNumber;
16
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 返回 task 的当前 owner agent id。
3
+ * 权威源为 `task_participants.participant_role='owner'`;若缺则回落到 `tasks.claimed_by_agent_id`。
4
+ */
5
+ export function loadTaskOwnerAgentId(db, taskId) {
6
+ if (!taskId)
7
+ return null;
8
+ const participantRow = db
9
+ .prepare(`SELECT agent_id AS agentId FROM task_participants
10
+ WHERE task_id = ? AND participant_role = 'owner'
11
+ LIMIT 1`)
12
+ .get(taskId);
13
+ if (participantRow?.agentId)
14
+ return participantRow.agentId;
15
+ const taskRow = db
16
+ .prepare(`SELECT claimed_by_agent_id AS claimedByAgentId FROM tasks WHERE task_id = ? LIMIT 1`)
17
+ .get(taskId);
18
+ const fallback = taskRow?.claimedByAgentId?.trim();
19
+ return fallback ? fallback : null;
20
+ }
21
+ export function isAgentTaskOwner(db, taskId, agentId) {
22
+ if (!taskId || !agentId)
23
+ return false;
24
+ return loadTaskOwnerAgentId(db, taskId) === agentId;
25
+ }
26
+ /**
27
+ * 构造 agent 端 "非 owner 不得修改 task" 拒绝响应。
28
+ * 调用方:reply.code(denial.status); return denial.body;
29
+ */
30
+ export function buildAgentTaskOwnerDenial(params) {
31
+ const taskNumber = typeof params.taskNumber === 'number' && Number.isFinite(params.taskNumber)
32
+ ? params.taskNumber
33
+ : null;
34
+ const label = taskNumber !== null ? `#${taskNumber}` : `task ${params.taskId}`;
35
+ return {
36
+ status: 403,
37
+ body: {
38
+ error: `Only the task owner can modify ${label}. Ask the owner or user to change the task instead.`,
39
+ error_code: 'task_owner_required',
40
+ required_action: 'ask_owner_or_user',
41
+ source_target: params.sourceTarget ?? null,
42
+ requested_target: {
43
+ type: 'task',
44
+ taskId: params.taskId,
45
+ taskNumber,
46
+ },
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,42 @@
1
+ import { MAX_TASK_PARTICIPANTS, replaceTaskParticipants, } from './taskParticipants.js';
2
+ /**
3
+ * Single entry point for all task participant writes.
4
+ *
5
+ * Enforces MAX_TASK_PARTICIPANTS (1 owner + bounded collaborators).
6
+ * Handles owner change preservation, collaborator merging, deduplication,
7
+ * and empty-list clearing.
8
+ */
9
+ export function resolveTaskParticipants(db, params) {
10
+ const ownerId = params.ownerAgentId?.trim() || null;
11
+ // Normalize and deduplicate incoming collaborator IDs, excluding owner
12
+ const incomingCollaboratorIds = Array.from(new Set((params.collaboratorAgentIds ?? [])
13
+ .map((id) => (typeof id === 'string' ? id.trim() : ''))
14
+ .filter((id) => id.length > 0 && id !== ownerId)));
15
+ // Load existing participants when preserving
16
+ let existingCollaboratorIds = [];
17
+ if (params.preserveExistingCollaborators) {
18
+ const rows = db.prepare(`SELECT agent_id as agentId
19
+ FROM task_participants
20
+ WHERE task_id = ?
21
+ AND participant_role = 'collaborator'`).all(params.taskId);
22
+ existingCollaboratorIds = rows
23
+ .map((r) => r.agentId)
24
+ .filter((id) => id && id !== ownerId);
25
+ }
26
+ // Merge: existing + incoming, deduplicated
27
+ const collaboratorAgentIds = params.preserveExistingCollaborators
28
+ ? Array.from(new Set([...existingCollaboratorIds, ...incomingCollaboratorIds]))
29
+ : incomingCollaboratorIds;
30
+ // M9 enforcement: bounded collaborators plus owner.
31
+ if (collaboratorAgentIds.length > MAX_TASK_PARTICIPANTS - 1) {
32
+ throw new Error(`Too many collaborators (max ${MAX_TASK_PARTICIPANTS - 1}, plus owner = ${MAX_TASK_PARTICIPANTS} total)`);
33
+ }
34
+ // Write to DB via replaceTaskParticipants
35
+ replaceTaskParticipants(db, {
36
+ taskId: params.taskId,
37
+ ownerAgentId: ownerId,
38
+ collaboratorAgentIds,
39
+ updatedAt: params.updatedAt,
40
+ });
41
+ return { ownerId, collaboratorAgentIds };
42
+ }
@@ -0,0 +1,97 @@
1
+ /** Maximum number of task participants (1 owner + up to 19 collaborators = 20 total). */
2
+ export const MAX_TASK_PARTICIPANTS = 20;
3
+ export function upsertTaskParticipant(db, params) {
4
+ const agentId = params.agentId?.trim();
5
+ if (!agentId)
6
+ return;
7
+ const updatedAt = params.updatedAt ?? Date.now();
8
+ db.prepare(`INSERT INTO task_participants(
9
+ task_id,
10
+ agent_id,
11
+ participant_role,
12
+ joined_at,
13
+ last_updated_at
14
+ )
15
+ VALUES(?, ?, ?, ?, ?)
16
+ ON CONFLICT(task_id, agent_id) DO UPDATE SET
17
+ participant_role = CASE
18
+ WHEN excluded.participant_role = 'owner' THEN 'owner'
19
+ ELSE task_participants.participant_role
20
+ END,
21
+ joined_at = MIN(task_participants.joined_at, excluded.joined_at),
22
+ last_updated_at = MAX(task_participants.last_updated_at, excluded.last_updated_at)`).run(params.taskId, agentId, params.role, updatedAt, updatedAt);
23
+ }
24
+ export function listTaskCollaboratorAgentIds(db, params) {
25
+ const rows = db.prepare(`SELECT agent_id as agentId
26
+ FROM task_participants
27
+ WHERE task_id = ?
28
+ AND participant_role = 'collaborator'
29
+ ORDER BY last_updated_at ASC, agent_id ASC`).all(params.taskId);
30
+ return rows
31
+ .map((row) => row.agentId?.trim() ?? '')
32
+ .filter((agentId) => agentId.length > 0);
33
+ }
34
+ export function captureTaskParticipantSnapshot(db, params) {
35
+ const row = db.prepare(`SELECT participant_role as participantRole,
36
+ joined_at as joinedAt,
37
+ last_updated_at as lastUpdatedAt
38
+ FROM task_participants
39
+ WHERE task_id = ? AND agent_id = ?
40
+ LIMIT 1`).get(params.taskId, params.agentId);
41
+ return row ? { exists: true, ...row } : { exists: false };
42
+ }
43
+ export function restoreTaskParticipantSnapshot(db, params) {
44
+ if (!params.snapshot.exists) {
45
+ db.prepare(`DELETE FROM task_participants
46
+ WHERE task_id = ? AND agent_id = ?`).run(params.taskId, params.agentId);
47
+ return;
48
+ }
49
+ db.prepare(`INSERT INTO task_participants(
50
+ task_id,
51
+ agent_id,
52
+ participant_role,
53
+ joined_at,
54
+ last_updated_at
55
+ )
56
+ VALUES(?, ?, ?, ?, ?)
57
+ ON CONFLICT(task_id, agent_id) DO UPDATE SET
58
+ participant_role = excluded.participant_role,
59
+ joined_at = excluded.joined_at,
60
+ last_updated_at = excluded.last_updated_at`).run(params.taskId, params.agentId, params.snapshot.participantRole, params.snapshot.joinedAt, params.snapshot.lastUpdatedAt);
61
+ }
62
+ export function clearTaskParticipants(db, params) {
63
+ db.prepare(`DELETE FROM task_participants WHERE task_id = ?`).run(params.taskId);
64
+ }
65
+ export function removeTaskParticipant(db, params) {
66
+ const agentId = params.agentId?.trim();
67
+ if (!agentId)
68
+ return;
69
+ // Shared-task unclaim only removes the releasing owner. Collaborators stay visible
70
+ // so the shared task can still be discovered and potentially re-owned.
71
+ db.prepare(`DELETE FROM task_participants
72
+ WHERE task_id = ? AND agent_id = ?`).run(params.taskId, agentId);
73
+ }
74
+ export function replaceTaskParticipants(db, params) {
75
+ clearTaskParticipants(db, { taskId: params.taskId });
76
+ const ownerAgentId = params.ownerAgentId?.trim() ?? null;
77
+ const collaboratorAgentIds = Array.from(new Set((params.collaboratorAgentIds ?? [])
78
+ .map((agentId) => agentId?.trim() ?? '')
79
+ .filter((agentId) => agentId && agentId !== ownerAgentId)));
80
+ const updatedAt = params.updatedAt ?? Date.now();
81
+ if (ownerAgentId) {
82
+ upsertTaskParticipant(db, {
83
+ taskId: params.taskId,
84
+ agentId: ownerAgentId,
85
+ role: 'owner',
86
+ updatedAt,
87
+ });
88
+ }
89
+ for (const collaboratorAgentId of collaboratorAgentIds) {
90
+ upsertTaskParticipant(db, {
91
+ taskId: params.taskId,
92
+ agentId: collaboratorAgentId,
93
+ role: 'collaborator',
94
+ updatedAt,
95
+ });
96
+ }
97
+ }
@@ -0,0 +1,20 @@
1
+ const SOURCE_MESSAGE_SECTION_LABEL = 'Source message:';
2
+ function normalizeForSourceComparison(value) {
3
+ return (value ?? '').replace(/\s+/g, ' ').trim();
4
+ }
5
+ export function appendTaskSourceMessageToDescription(params) {
6
+ const description = params.description.trim();
7
+ const sourceMessage = params.sourceMessage?.trim();
8
+ if (!sourceMessage)
9
+ return description;
10
+ const normalizedSource = normalizeForSourceComparison(sourceMessage);
11
+ if (!normalizedSource)
12
+ return description;
13
+ if (normalizeForSourceComparison(params.title) === normalizedSource)
14
+ return description;
15
+ if (normalizeForSourceComparison(description).includes(normalizedSource))
16
+ return description;
17
+ return description
18
+ ? `${description}\n\n${SOURCE_MESSAGE_SECTION_LABEL}\n${sourceMessage}`
19
+ : `${SOURCE_MESSAGE_SECTION_LABEL}\n${sourceMessage}`;
20
+ }
@@ -0,0 +1,210 @@
1
+ import { buildThreadShortId } from '@bbigbang/protocol';
2
+ import { isClearedTaskRootTarget } from './clearedTaskRoots.js';
3
+ export function listVisibleTasksForAgent(db, agentId, options) {
4
+ const requestedStatus = options?.status ?? 'all';
5
+ const requestedScope = options?.scope ?? 'all';
6
+ const params = [agentId, agentId, agentId];
7
+ const conditions = ['(t.claimed_by_agent_id = ? OR tp.agent_id = ?)'];
8
+ if (requestedStatus !== 'all') {
9
+ conditions.push('t.status = ?');
10
+ params.push(requestedStatus);
11
+ }
12
+ if (requestedScope === 'dm') {
13
+ conditions.push(`t.channel_id LIKE 'dm:%'`);
14
+ }
15
+ else if (requestedScope === 'channel') {
16
+ conditions.push(`t.channel_id NOT LIKE 'dm:%'`);
17
+ }
18
+ const rows = db.prepare(`${VISIBLE_TASK_SELECT}
19
+ ${VISIBLE_TASK_FROM}
20
+ WHERE ${conditions.join(' AND ')}
21
+ ORDER BY t.updated_at DESC, t.created_at DESC`).all(...params);
22
+ return rows.map(toVisibleTaskView);
23
+ }
24
+ export function getVisibleTaskLookupByRef(db, agentId, taskRef) {
25
+ const row = db.prepare(`${VISIBLE_TASK_SELECT}
26
+ ${VISIBLE_TASK_FROM}
27
+ WHERE t.agent_task_ref = ?
28
+ AND (t.claimed_by_agent_id = ? OR tp.agent_id = ?)
29
+ LIMIT 1`).get(agentId, taskRef, agentId, agentId);
30
+ if (!row)
31
+ return null;
32
+ return {
33
+ record: row,
34
+ task: toVisibleTaskView(row),
35
+ };
36
+ }
37
+ export function getDeletedTaskLookupByRef(db, agentId, taskRef) {
38
+ const row = db.prepare(`SELECT dt.task_id as taskId,
39
+ dt.agent_task_ref as agentTaskRef,
40
+ dt.channel_id as channelId,
41
+ dt.channel_name as channelName,
42
+ dt.task_number as taskNumber,
43
+ dt.title,
44
+ dt.description,
45
+ dt.status,
46
+ dt.claimed_by_name as claimedByName,
47
+ dt.created_by_name as createdByName,
48
+ dt.message_id as messageId,
49
+ dt.source_target as sourceTarget,
50
+ dt.thread_root_id as threadRootId,
51
+ dt.deleted_by_name as deletedByName,
52
+ dt.created_at as createdAt,
53
+ dt.updated_at as updatedAt,
54
+ dt.deleted_at as deletedAt
55
+ FROM deleted_tasks dt
56
+ WHERE dt.agent_task_ref = ?
57
+ AND (
58
+ dt.claimed_by_agent_id = ?
59
+ OR EXISTS (
60
+ SELECT 1
61
+ FROM deleted_task_participants dtp
62
+ WHERE dtp.task_id = dt.task_id
63
+ AND dtp.agent_id = ?
64
+ )
65
+ )
66
+ LIMIT 1`).get(taskRef, agentId, agentId);
67
+ if (!row)
68
+ return null;
69
+ return {
70
+ record: row,
71
+ deletedTask: toDeletedTaskView(row),
72
+ };
73
+ }
74
+ export function listTaskHistoryEventViews(db, taskId, limit) {
75
+ const rows = db.prepare(`SELECT event_id as eventId,
76
+ event_type as eventType,
77
+ actor_type as actorType,
78
+ actor_id as actorId,
79
+ actor_name as actorName,
80
+ from_status as fromStatus,
81
+ to_status as toStatus,
82
+ claimed_by_agent_id_after as claimedByAgentIdAfter,
83
+ claimed_by_name_after as claimedByNameAfter,
84
+ message_id as messageId,
85
+ thread_target as threadTarget,
86
+ created_at as createdAt
87
+ FROM task_events
88
+ WHERE task_id = ?
89
+ ORDER BY created_at DESC
90
+ LIMIT ?`).all(taskId, limit);
91
+ return rows.map((row) => ({
92
+ eventId: row.eventId,
93
+ type: row.eventType,
94
+ actorType: row.actorType,
95
+ actorId: row.actorId,
96
+ actorName: row.actorName,
97
+ fromStatus: row.fromStatus,
98
+ toStatus: row.toStatus,
99
+ claimedByAgentIdAfter: row.claimedByAgentIdAfter,
100
+ claimedByNameAfter: row.claimedByNameAfter,
101
+ messageId: row.messageId,
102
+ threadTarget: row.threadTarget,
103
+ createdAt: new Date(row.createdAt).toISOString(),
104
+ }));
105
+ }
106
+ export function buildTaskSourceTarget(channelId, sourceTarget, channelName) {
107
+ if (isClearedTaskRootTarget(sourceTarget))
108
+ return channelId.startsWith('dm:') ? null : `#${channelName ?? channelId}`;
109
+ if (sourceTarget?.trim())
110
+ return sourceTarget.trim();
111
+ if (channelId.startsWith('dm:'))
112
+ return null;
113
+ return `#${channelName ?? channelId}`;
114
+ }
115
+ export function buildTaskSourceLabel(channelId, sourceTarget, channelName) {
116
+ if (sourceTarget?.startsWith('dm:@'))
117
+ return sourceTarget;
118
+ if (channelName)
119
+ return `#${channelName}`;
120
+ if (channelId.startsWith('dm:'))
121
+ return 'DM';
122
+ if (sourceTarget?.trim())
123
+ return sourceTarget.trim();
124
+ return channelId;
125
+ }
126
+ export function buildTaskThreadTarget(sourceTarget, messageId) {
127
+ if (!sourceTarget || !messageId)
128
+ return null;
129
+ const normalizedTarget = sourceTarget.trim();
130
+ if (!(normalizedTarget.startsWith('dm:@') || normalizedTarget.startsWith('#')))
131
+ return null;
132
+ if (isThreadTargetValue(normalizedTarget))
133
+ return normalizedTarget;
134
+ return `${normalizedTarget}:${buildThreadShortId(messageId)}`;
135
+ }
136
+ const VISIBLE_TASK_SELECT = `SELECT t.task_id as taskId,
137
+ t.agent_task_ref as agentTaskRef,
138
+ t.channel_id as channelId,
139
+ t.task_number as taskNumber,
140
+ t.title,
141
+ t.description,
142
+ t.status,
143
+ t.claimed_by_agent_id as claimedByAgentId,
144
+ t.claimed_by_name as claimedByName,
145
+ t.created_by_agent_id as createdByAgentId,
146
+ t.created_by_name as createdByName,
147
+ t.created_at as createdAt,
148
+ t.updated_at as updatedAt,
149
+ t.message_id as messageId,
150
+ COALESCE(t.dm_target, cm.target) as sourceTarget,
151
+ ch.name as channelName`;
152
+ const VISIBLE_TASK_FROM = `FROM tasks t
153
+ LEFT JOIN task_participants tp
154
+ ON tp.task_id = t.task_id
155
+ AND tp.agent_id = ?
156
+ LEFT JOIN channel_messages cm
157
+ ON cm.message_id = t.message_id
158
+ LEFT JOIN channels ch
159
+ ON ch.channel_id = t.channel_id`;
160
+ function toVisibleTaskView(row) {
161
+ const sourceTarget = buildTaskSourceTarget(row.channelId, row.sourceTarget, row.channelName);
162
+ return {
163
+ taskId: row.taskId,
164
+ agentTaskRef: row.agentTaskRef,
165
+ channelId: row.channelId,
166
+ taskNumber: row.taskNumber,
167
+ title: row.title,
168
+ description: row.description ?? null,
169
+ status: row.status,
170
+ claimedByName: row.claimedByName ?? null,
171
+ createdByName: row.createdByName ?? null,
172
+ messageId: row.messageId ?? null,
173
+ sourceTarget,
174
+ sourceLabel: buildTaskSourceLabel(row.channelId, sourceTarget, row.channelName),
175
+ threadTarget: buildTaskThreadTarget(sourceTarget, row.messageId ?? null),
176
+ createdAt: new Date(row.createdAt).toISOString(),
177
+ updatedAt: new Date(row.updatedAt).toISOString(),
178
+ };
179
+ }
180
+ function toDeletedTaskView(row) {
181
+ const sourceTarget = buildTaskSourceTarget(row.channelId, row.sourceTarget, row.channelName);
182
+ const threadTarget = row.threadRootId && sourceTarget ? `${sourceTarget}:${row.threadRootId}` : null;
183
+ return {
184
+ deleted: true,
185
+ taskId: row.taskId,
186
+ agentTaskRef: row.agentTaskRef,
187
+ channelId: row.channelId,
188
+ taskNumber: row.taskNumber,
189
+ title: row.title,
190
+ description: row.description ?? null,
191
+ status: row.status,
192
+ claimedByName: row.claimedByName ?? null,
193
+ createdByName: row.createdByName ?? null,
194
+ messageId: row.messageId ?? null,
195
+ sourceTarget,
196
+ sourceLabel: buildTaskSourceLabel(row.channelId, sourceTarget, row.channelName),
197
+ threadTarget,
198
+ deletedByName: row.deletedByName ?? null,
199
+ createdAt: new Date(row.createdAt).toISOString(),
200
+ updatedAt: new Date(row.updatedAt).toISOString(),
201
+ deletedAt: new Date(row.deletedAt).toISOString(),
202
+ };
203
+ }
204
+ function isThreadTargetValue(target) {
205
+ if (target.startsWith('dm:@'))
206
+ return target.split(':').length >= 3;
207
+ if (target.startsWith('#'))
208
+ return target.includes(':');
209
+ return false;
210
+ }
@@ -0,0 +1,9 @@
1
+ const VALID_TRANSITIONS = {
2
+ todo: ['in_progress'],
3
+ in_progress: ['in_review'],
4
+ in_review: ['done', 'in_progress'],
5
+ done: [],
6
+ };
7
+ export function isValidTransition(from, to) {
8
+ return VALID_TRANSITIONS[from]?.includes(to) ?? false;
9
+ }