@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,1061 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { bumpAgentMessageCheckpoint } from './messageCheckpoints.js';
3
+ import { findThreadRootMessageId } from './threadRoots.js';
4
+ export const NOTIFICATION_REASON_MASK = {
5
+ participant_heads_up: 1 << 0,
6
+ explicit_agent_mention: 1 << 1,
7
+ human_prompt: 1 << 2,
8
+ task_control_notice: 1 << 3,
9
+ };
10
+ export const NOTIFICATION_PRIORITY = {
11
+ participant_heads_up: 1,
12
+ explicit_agent_mention: 2,
13
+ human_prompt: 3,
14
+ task_control_notice: 4,
15
+ };
16
+ const REDACTED_NOTIFICATION_PROMPT = '[notification delivery payload redacted]';
17
+ export function normalizeNotificationSurfaceThreadKey(threadRootId) {
18
+ return threadRootId?.trim() ?? '';
19
+ }
20
+ export function notificationSurfaceKey(channelId, threadRootId) {
21
+ return `${channelId}:${normalizeNotificationSurfaceThreadKey(threadRootId)}`;
22
+ }
23
+ function parsePayload(payloadJson) {
24
+ const parsed = JSON.parse(payloadJson);
25
+ if (!parsed || typeof parsed.promptText !== 'string' || parsed.promptText.trim() === '') {
26
+ throw new Error('Notification round has an invalid delivery payload.');
27
+ }
28
+ return parsed;
29
+ }
30
+ export function redactNotificationDeliveryPayloadJson(payloadJson) {
31
+ let activationMetadata;
32
+ try {
33
+ const parsed = JSON.parse(payloadJson ?? '{}');
34
+ activationMetadata = redactNotificationActivationMetadata(parsed?.activationMetadata);
35
+ }
36
+ catch {
37
+ activationMetadata = undefined;
38
+ }
39
+ return JSON.stringify({
40
+ promptText: REDACTED_NOTIFICATION_PROMPT,
41
+ ...(activationMetadata ? { activationMetadata } : {}),
42
+ });
43
+ }
44
+ function redactNotificationActivationMetadata(value) {
45
+ if (!value || typeof value !== 'object' || Array.isArray(value))
46
+ return undefined;
47
+ const redacted = { ...value };
48
+ delete redacted.promptContextSections;
49
+ delete redacted.slashCommand;
50
+ delete redacted.attachments;
51
+ delete redacted.attachmentIds;
52
+ return Object.keys(redacted).length > 0 ? redacted : undefined;
53
+ }
54
+ function getMutableReadyRound(db, params) {
55
+ return db.prepare(`SELECT round_id as roundId
56
+ FROM agent_notification_rounds
57
+ WHERE target_agent_id = ?
58
+ AND channel_id = ?
59
+ AND surface_thread_key = ?
60
+ AND status = 'ready'
61
+ AND leased_until IS NULL
62
+ ORDER BY created_at ASC
63
+ LIMIT 1`).get(params.targetAgentId, params.channelId, params.surfaceThreadKey);
64
+ }
65
+ export function getAgentSurfaceNotificationState(db, params) {
66
+ const surfaceThreadKey = normalizeNotificationSurfaceThreadKey(params.threadRootId);
67
+ const row = db.prepare(`SELECT covered_through_seq as coveredThroughSeq,
68
+ acked_through_seq as ackedThroughSeq
69
+ FROM agent_surface_notification_state
70
+ WHERE target_agent_id = ?
71
+ AND channel_id = ?
72
+ AND surface_thread_key = ?
73
+ LIMIT 1`).get(params.targetAgentId, params.channelId, surfaceThreadKey);
74
+ return row;
75
+ }
76
+ export function getAgentSurfaceNotificationAckedThroughSeq(db, params) {
77
+ return getAgentSurfaceNotificationState(db, params)?.ackedThroughSeq ?? 0;
78
+ }
79
+ export function advanceAgentSurfaceNotificationAck(db, params) {
80
+ const now = params.now ?? Date.now();
81
+ const ackedThroughSeq = Math.max(0, Math.floor(params.ackedThroughSeq));
82
+ if (ackedThroughSeq <= 0)
83
+ return;
84
+ const surfaceThreadKey = normalizeNotificationSurfaceThreadKey(params.threadRootId);
85
+ db.transaction(() => {
86
+ getOrCreateSurfaceState(db, {
87
+ targetAgentId: params.targetAgentId,
88
+ channelId: params.channelId,
89
+ surfaceThreadKey,
90
+ now,
91
+ });
92
+ db.prepare(`UPDATE agent_surface_notification_state
93
+ SET covered_through_seq = MAX(covered_through_seq, ?),
94
+ acked_through_seq = MAX(acked_through_seq, ?),
95
+ updated_at = ?
96
+ WHERE target_agent_id = ?
97
+ AND channel_id = ?
98
+ AND surface_thread_key = ?`).run(ackedThroughSeq, ackedThroughSeq, now, params.targetAgentId, params.channelId, surfaceThreadKey);
99
+ bumpAgentMessageCheckpoint(db, params.targetAgentId, params.channelId, ackedThroughSeq, surfaceThreadKey || null);
100
+ const excludeRoundClause = params.excludeRoundId ? 'AND round_id <> ?' : '';
101
+ const supersedeArgs = [
102
+ now,
103
+ now,
104
+ params.targetAgentId,
105
+ params.channelId,
106
+ surfaceThreadKey,
107
+ ...(params.excludeRoundId ? [params.excludeRoundId] : []),
108
+ ackedThroughSeq,
109
+ ];
110
+ db.prepare(`UPDATE agent_notification_rounds
111
+ SET status = 'superseded',
112
+ superseded_at = ?,
113
+ leased_until = NULL,
114
+ attempt_token = NULL,
115
+ delivery_id = NULL,
116
+ delivery_mode = NULL,
117
+ updated_at = ?
118
+ WHERE target_agent_id = ?
119
+ AND channel_id = ?
120
+ AND surface_thread_key = ?
121
+ ${excludeRoundClause}
122
+ AND status IN ('ready', 'leased', 'delivered')
123
+ AND acked_at IS NULL
124
+ AND to_seq_inclusive <= ?`).run(...supersedeArgs);
125
+ })();
126
+ }
127
+ function getOrCreateSurfaceState(db, params) {
128
+ const existing = db.prepare(`SELECT covered_through_seq as coveredThroughSeq,
129
+ acked_through_seq as ackedThroughSeq
130
+ FROM agent_surface_notification_state
131
+ WHERE target_agent_id = ?
132
+ AND channel_id = ?
133
+ AND surface_thread_key = ?
134
+ LIMIT 1`).get(params.targetAgentId, params.channelId, params.surfaceThreadKey);
135
+ if (existing)
136
+ return existing;
137
+ db.prepare(`INSERT INTO agent_surface_notification_state(
138
+ target_agent_id, channel_id, surface_thread_key, covered_through_seq, acked_through_seq, created_at, updated_at
139
+ ) VALUES(?, ?, ?, 0, 0, ?, ?)`).run(params.targetAgentId, params.channelId, params.surfaceThreadKey, params.now, params.now);
140
+ return { coveredThroughSeq: 0, ackedThroughSeq: 0 };
141
+ }
142
+ export function listAgentSurfaceRosterParticipants(db, params) {
143
+ const now = params.now ?? Date.now();
144
+ const surfaceThreadKey = normalizeNotificationSurfaceThreadKey(params.threadRootId);
145
+ const sanitizedLimit = Number.isInteger(params.limit) && params.limit > 0 ? params.limit : undefined;
146
+ const limitClause = sanitizedLimit !== undefined ? `LIMIT ${sanitizedLimit}` : '';
147
+ return db.prepare(`SELECT r.target_agent_id as agentId,
148
+ a.name as name,
149
+ CASE
150
+ WHEN MAX(CASE WHEN r.source = 'surface_owner' THEN 1 ELSE 0 END) = 1 THEN 'owner'
151
+ ELSE 'participant'
152
+ END as role,
153
+ MIN(r.created_at) as joinedAt,
154
+ MAX(r.updated_at) as lastActiveAt
155
+ FROM agent_surface_roster r
156
+ JOIN agents a ON a.agent_id = r.target_agent_id
157
+ WHERE r.channel_id = ?
158
+ AND r.surface_thread_key = ?
159
+ AND (? != '' OR r.source != 'surface_owner')
160
+ AND (r.expires_at IS NULL OR r.expires_at > ?)
161
+ GROUP BY r.target_agent_id, a.name
162
+ ORDER BY
163
+ CASE
164
+ WHEN MAX(CASE WHEN r.source = 'surface_owner' THEN 1 ELSE 0 END) = 1 THEN 0
165
+ ELSE 1
166
+ END ASC,
167
+ MAX(r.updated_at) DESC,
168
+ a.name ASC
169
+ ${limitClause}`).all(params.channelId, surfaceThreadKey, surfaceThreadKey, now);
170
+ }
171
+ export function ensureAgentSurfaceOwner(db, params) {
172
+ const now = params.now ?? Date.now();
173
+ const surfaceThreadKey = normalizeNotificationSurfaceThreadKey(params.threadRootId);
174
+ if (!surfaceThreadKey) {
175
+ return;
176
+ }
177
+ const existingOwner = db.prepare(`SELECT 1
178
+ FROM agent_surface_roster
179
+ WHERE channel_id = ?
180
+ AND surface_thread_key = ?
181
+ AND source = 'surface_owner'
182
+ LIMIT 1`).get(params.channelId, surfaceThreadKey);
183
+ if (existingOwner)
184
+ return;
185
+ upsertAgentSurfaceRoster(db, {
186
+ targetAgentId: params.targetAgentId,
187
+ channelId: params.channelId,
188
+ threadRootId: surfaceThreadKey,
189
+ source: 'surface_owner',
190
+ sourceKey: 'owner',
191
+ sourceAgentId: params.targetAgentId,
192
+ expiresAt: null,
193
+ now,
194
+ });
195
+ }
196
+ export function syncTaskBindingSurfaceRoster(db, params) {
197
+ db.transaction(() => _syncTaskBindingSurfaceRoster(db, params))();
198
+ }
199
+ function _syncTaskBindingSurfaceRoster(db, params) {
200
+ const now = params.now ?? Date.now();
201
+ const entries = new Set();
202
+ const ownerAgentId = params.ownerAgentId?.trim();
203
+ if (ownerAgentId)
204
+ entries.add(ownerAgentId);
205
+ for (const collaboratorAgentId of params.collaboratorAgentIds ?? []) {
206
+ const normalized = collaboratorAgentId.trim();
207
+ if (normalized)
208
+ entries.add(normalized);
209
+ }
210
+ const surfaceThreadKey = normalizeNotificationSurfaceThreadKey(params.threadRootId);
211
+ const entryList = [...entries];
212
+ if (entryList.length > 0) {
213
+ const placeholders = entryList.map(() => '?').join(', ');
214
+ db.prepare(`DELETE FROM target_participants
215
+ WHERE channel_id = ?
216
+ AND thread_root_id = ?
217
+ AND agent_id NOT IN (${placeholders})`).run(params.channelId, surfaceThreadKey, ...entryList);
218
+ }
219
+ else {
220
+ db.prepare(`DELETE FROM target_participants
221
+ WHERE channel_id = ?
222
+ AND thread_root_id = ?`).run(params.channelId, surfaceThreadKey);
223
+ }
224
+ if (params.collaboratorAgentIds !== undefined) {
225
+ if (entryList.length > 0) {
226
+ const placeholders = entryList.map(() => '?').join(', ');
227
+ db.prepare(`DELETE FROM agent_surface_roster
228
+ WHERE channel_id = ?
229
+ AND surface_thread_key = ?
230
+ AND source = 'task_binding_current'
231
+ AND source_key = ?
232
+ AND target_agent_id NOT IN (${placeholders})`).run(params.channelId, surfaceThreadKey, params.taskId, ...entryList);
233
+ }
234
+ else {
235
+ db.prepare(`DELETE FROM agent_surface_roster
236
+ WHERE channel_id = ?
237
+ AND surface_thread_key = ?
238
+ AND source = 'task_binding_current'
239
+ AND source_key = ?`).run(params.channelId, surfaceThreadKey, params.taskId);
240
+ }
241
+ }
242
+ db.prepare(`DELETE FROM agent_surface_roster
243
+ WHERE channel_id = ?
244
+ AND surface_thread_key = ?
245
+ AND source = 'surface_owner'`).run(params.channelId, surfaceThreadKey);
246
+ for (const agentId of entries) {
247
+ upsertAgentSurfaceRoster(db, {
248
+ targetAgentId: agentId,
249
+ channelId: params.channelId,
250
+ threadRootId: params.threadRootId,
251
+ source: 'task_binding_current',
252
+ sourceKey: params.taskId,
253
+ sourceAgentId: params.ownerAgentId ?? null,
254
+ expiresAt: null,
255
+ now,
256
+ });
257
+ if (ownerAgentId && agentId === ownerAgentId) {
258
+ upsertAgentSurfaceRoster(db, {
259
+ targetAgentId: agentId,
260
+ channelId: params.channelId,
261
+ threadRootId: params.threadRootId,
262
+ source: 'surface_owner',
263
+ sourceKey: params.taskId,
264
+ sourceAgentId: agentId,
265
+ expiresAt: null,
266
+ now,
267
+ });
268
+ }
269
+ }
270
+ }
271
+ function loadTaskPolicySnapshotForSurface(db, params) {
272
+ if (!params.surfaceThreadKey)
273
+ return null;
274
+ const rootMessageId = findThreadRootMessageId(db, params.channelId, params.surfaceThreadKey);
275
+ if (!rootMessageId)
276
+ return null;
277
+ const row = db.prepare(`SELECT t.task_id as taskId,
278
+ t.status,
279
+ t.updated_at as updatedAt,
280
+ t.claimed_by_agent_id as assigneeAgentId,
281
+ tp.participant_role as targetParticipantRole
282
+ FROM tasks t
283
+ LEFT JOIN task_participants tp
284
+ ON tp.task_id = t.task_id
285
+ AND tp.agent_id = ?
286
+ WHERE t.channel_id = ?
287
+ AND t.message_id = ?
288
+ LIMIT 1`).get(params.targetAgentId, params.channelId, rootMessageId);
289
+ if (!row)
290
+ return null;
291
+ return {
292
+ taskId: row.taskId,
293
+ status: row.status,
294
+ updatedAt: row.updatedAt,
295
+ assigneeAgentId: row.assigneeAgentId,
296
+ targetParticipantRole: row.targetParticipantRole
297
+ ?? (row.assigneeAgentId === params.targetAgentId ? 'owner' : null),
298
+ };
299
+ }
300
+ function parseTaskPolicySnapshot(raw) {
301
+ if (!raw)
302
+ return null;
303
+ try {
304
+ const parsed = JSON.parse(raw);
305
+ if (!parsed
306
+ || typeof parsed.taskId !== 'string'
307
+ || typeof parsed.status !== 'string'
308
+ || typeof parsed.updatedAt !== 'number') {
309
+ return null;
310
+ }
311
+ return {
312
+ taskId: parsed.taskId,
313
+ status: parsed.status,
314
+ updatedAt: parsed.updatedAt,
315
+ assigneeAgentId: typeof parsed.assigneeAgentId === 'string' ? parsed.assigneeAgentId : null,
316
+ targetParticipantRole: parsed.targetParticipantRole === 'owner' || parsed.targetParticipantRole === 'collaborator'
317
+ ? parsed.targetParticipantRole
318
+ : null,
319
+ };
320
+ }
321
+ catch {
322
+ return null;
323
+ }
324
+ }
325
+ export function upsertAgentSurfaceRoster(db, params) {
326
+ const now = params.now ?? Date.now();
327
+ const surfaceThreadKey = normalizeNotificationSurfaceThreadKey(params.threadRootId);
328
+ if (!surfaceThreadKey && params.source === 'surface_owner') {
329
+ return;
330
+ }
331
+ db.prepare(`INSERT INTO agent_surface_roster(
332
+ target_agent_id, channel_id, surface_thread_key, source, source_key, source_agent_id, expires_at, created_at, updated_at
333
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
334
+ ON CONFLICT(target_agent_id, channel_id, surface_thread_key, source, source_key) DO UPDATE SET
335
+ source_agent_id = excluded.source_agent_id,
336
+ expires_at = excluded.expires_at,
337
+ updated_at = excluded.updated_at`).run(params.targetAgentId, params.channelId, surfaceThreadKey, params.source, params.sourceKey?.trim() ?? '', params.sourceAgentId ?? null, params.expiresAt ?? null, now, now);
338
+ }
339
+ export function createOrMergeNotificationRound(db, params) {
340
+ const now = params.now ?? Date.now();
341
+ const surfaceThreadKey = normalizeNotificationSurfaceThreadKey(params.threadRootId);
342
+ const toSeqInclusive = Math.max(0, Math.floor(params.toSeqInclusive));
343
+ if (toSeqInclusive <= 0) {
344
+ return { roundId: '', created: false, skipped: true, acceptedPayload: false };
345
+ }
346
+ return db.transaction(() => {
347
+ const state = getOrCreateSurfaceState(db, {
348
+ targetAgentId: params.targetAgentId,
349
+ channelId: params.channelId,
350
+ surfaceThreadKey,
351
+ now,
352
+ });
353
+ if (toSeqInclusive <= state.coveredThroughSeq) {
354
+ const existing = getMutableReadyRound(db, {
355
+ targetAgentId: params.targetAgentId,
356
+ channelId: params.channelId,
357
+ surfaceThreadKey,
358
+ });
359
+ return {
360
+ roundId: existing?.roundId ?? '',
361
+ created: false,
362
+ skipped: !existing,
363
+ acceptedPayload: false,
364
+ };
365
+ }
366
+ const reasonMask = NOTIFICATION_REASON_MASK[params.reason];
367
+ const priority = NOTIFICATION_PRIORITY[params.reason];
368
+ const payloadJson = JSON.stringify(params.deliveryPayload);
369
+ const resolvedTaskPolicySnapshot = params.taskPolicySnapshot
370
+ ?? loadTaskPolicySnapshotForSurface(db, {
371
+ targetAgentId: params.targetAgentId,
372
+ channelId: params.channelId,
373
+ surfaceThreadKey,
374
+ });
375
+ const taskPolicySnapshotJson = resolvedTaskPolicySnapshot == null
376
+ ? null
377
+ : JSON.stringify(resolvedTaskPolicySnapshot);
378
+ const existing = getMutableReadyRound(db, {
379
+ targetAgentId: params.targetAgentId,
380
+ channelId: params.channelId,
381
+ surfaceThreadKey,
382
+ });
383
+ db.prepare(`UPDATE agent_surface_notification_state
384
+ SET covered_through_seq = MAX(covered_through_seq, ?),
385
+ last_human_activity_at = CASE
386
+ WHEN ? THEN ?
387
+ ELSE last_human_activity_at
388
+ END,
389
+ last_target_activity_at = CASE
390
+ WHEN ? THEN ?
391
+ ELSE last_target_activity_at
392
+ END,
393
+ updated_at = ?
394
+ WHERE target_agent_id = ?
395
+ AND channel_id = ?
396
+ AND surface_thread_key = ?`).run(toSeqInclusive, params.fromAgentId?.startsWith('user:') ? 1 : 0, now, params.fromAgentId === params.targetAgentId ? 1 : 0, now, now, params.targetAgentId, params.channelId, surfaceThreadKey);
397
+ if (existing) {
398
+ db.prepare(`UPDATE agent_notification_rounds
399
+ SET conversation_id = ?,
400
+ reason_mask = reason_mask | ?,
401
+ priority = MAX(priority, ?),
402
+ to_seq_inclusive = MAX(to_seq_inclusive, ?),
403
+ from_agent_id = COALESCE(?, from_agent_id),
404
+ sender_name = COALESCE(?, sender_name),
405
+ last_message_id = COALESCE(?, last_message_id),
406
+ message_count = message_count + ?,
407
+ delivery_payload_json = ?,
408
+ task_policy_snapshot_json = COALESCE(?, task_policy_snapshot_json),
409
+ updated_at = ?
410
+ WHERE round_id = ?`).run(params.conversationId, reasonMask, priority, toSeqInclusive, params.fromAgentId ?? null, params.senderName ?? null, params.lastMessageId ?? null, Math.max(1, params.messageCount ?? 1), payloadJson, taskPolicySnapshotJson, now, existing.roundId);
411
+ return { roundId: existing.roundId, created: false, skipped: false, acceptedPayload: true };
412
+ }
413
+ const roundId = randomUUID();
414
+ db.prepare(`INSERT INTO agent_notification_rounds(
415
+ round_id, target_agent_id, channel_id, surface_thread_key, conversation_id,
416
+ reason_mask, priority, from_seq_exclusive, to_seq_inclusive, status, not_before,
417
+ from_agent_id, sender_name, first_message_id, last_message_id, message_count,
418
+ delivery_payload_json, task_policy_snapshot_json, created_at, updated_at
419
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, 'ready', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(roundId, params.targetAgentId, params.channelId, surfaceThreadKey, params.conversationId, reasonMask, priority, state.ackedThroughSeq, toSeqInclusive, params.fromAgentId ?? null, params.senderName ?? null, params.firstMessageId ?? null, params.lastMessageId ?? null, Math.max(1, params.messageCount ?? 1), payloadJson, taskPolicySnapshotJson, now, now);
420
+ return { roundId, created: true, skipped: false, acceptedPayload: true };
421
+ })();
422
+ }
423
+ export function leaseReadyNotificationRound(db, params) {
424
+ const now = params?.now ?? Date.now();
425
+ const leaseMs = Math.max(1_000, params?.leaseMs ?? 30_000);
426
+ const ackTimeoutMs = Math.max(1_000, params?.ackTimeoutMs ?? 30_000);
427
+ const ackDeadline = now - ackTimeoutMs;
428
+ const roundIdClause = params?.roundId ? 'AND round_id = ?' : '';
429
+ const values = params?.roundId
430
+ ? [now, now, ackDeadline, params.roundId]
431
+ : [now, now, ackDeadline];
432
+ const row = db.prepare(`SELECT r.round_id as roundId,
433
+ r.target_agent_id as targetAgentId,
434
+ r.channel_id as channelId,
435
+ r.surface_thread_key as surfaceThreadKey,
436
+ r.conversation_id as conversationId,
437
+ r.reason_mask as reasonMask,
438
+ r.priority,
439
+ r.from_seq_exclusive as fromSeqExclusive,
440
+ r.to_seq_inclusive as toSeqInclusive,
441
+ r.status,
442
+ r.delivery_id as deliveryId,
443
+ r.delivery_payload_json as deliveryPayloadJson,
444
+ r.task_policy_snapshot_json as taskPolicySnapshotJson,
445
+ r.attempt_count as attemptCount
446
+ FROM agent_notification_rounds r
447
+ WHERE r.status IN ('ready', 'leased', 'delivered')
448
+ AND r.not_before <= ?
449
+ AND NOT EXISTS (
450
+ SELECT 1
451
+ FROM agent_surface_notification_state s
452
+ WHERE s.target_agent_id = r.target_agent_id
453
+ AND s.channel_id = r.channel_id
454
+ AND s.surface_thread_key = r.surface_thread_key
455
+ AND s.acked_through_seq >= r.to_seq_inclusive
456
+ )
457
+ AND (
458
+ r.status = 'ready'
459
+ OR (r.status = 'leased' AND COALESCE(r.leased_until, 0) <= ?)
460
+ OR (r.status = 'delivered' AND r.acked_at IS NULL AND COALESCE(r.delivered_at, 0) <= ?)
461
+ )
462
+ ${roundIdClause}
463
+ ORDER BY r.priority DESC, r.created_at ASC
464
+ LIMIT 1`).get(...values);
465
+ if (!row)
466
+ return null;
467
+ const deliveryId = randomUUID();
468
+ const attemptToken = randomUUID();
469
+ const result = db.prepare(`UPDATE agent_notification_rounds
470
+ SET status = 'leased',
471
+ leased_until = ?,
472
+ attempt_token = ?,
473
+ delivery_id = ?,
474
+ attempt_count = attempt_count + 1,
475
+ updated_at = ?
476
+ WHERE round_id = ?
477
+ AND status IN ('ready', 'leased', 'delivered')
478
+ AND NOT EXISTS (
479
+ SELECT 1
480
+ FROM agent_surface_notification_state s
481
+ WHERE s.target_agent_id = agent_notification_rounds.target_agent_id
482
+ AND s.channel_id = agent_notification_rounds.channel_id
483
+ AND s.surface_thread_key = agent_notification_rounds.surface_thread_key
484
+ AND s.acked_through_seq >= agent_notification_rounds.to_seq_inclusive
485
+ )
486
+ AND (
487
+ status = 'ready'
488
+ OR (status = 'leased' AND COALESCE(leased_until, 0) <= ?)
489
+ OR (status = 'delivered' AND acked_at IS NULL AND COALESCE(delivered_at, 0) <= ?)
490
+ )`).run(now + leaseMs, attemptToken, deliveryId, now, row.roundId, now, ackDeadline);
491
+ if (result.changes !== 1)
492
+ return null;
493
+ const delivery = {
494
+ deliveryId,
495
+ roundId: row.roundId,
496
+ ackedThroughSeq: row.toSeqInclusive,
497
+ surfaceKey: notificationSurfaceKey(row.channelId, row.surfaceThreadKey),
498
+ };
499
+ return {
500
+ ...row,
501
+ status: 'leased',
502
+ deliveryId,
503
+ delivery,
504
+ };
505
+ }
506
+ export function markNotificationRoundDelivered(db, params) {
507
+ const now = params.now ?? Date.now();
508
+ db.prepare(`UPDATE agent_notification_rounds
509
+ SET status = 'delivered',
510
+ delivery_mode = ?,
511
+ delivered_at = ?,
512
+ updated_at = ?
513
+ WHERE round_id = ?
514
+ AND delivery_id = ?
515
+ AND status = 'leased'`).run(params.mode, now, now, params.roundId, params.deliveryId);
516
+ }
517
+ export function releaseNotificationRoundForRetry(db, params) {
518
+ const now = params.now ?? Date.now();
519
+ const retryMs = Math.max(1_000, params.retryMs ?? 10_000);
520
+ const retryAt = now + retryMs;
521
+ const errorText = params.error?.slice(0, 1000) ?? null;
522
+ db.transaction(() => {
523
+ const row = db.prepare(`SELECT round_id as roundId,
524
+ target_agent_id as targetAgentId,
525
+ channel_id as channelId,
526
+ surface_thread_key as surfaceThreadKey,
527
+ reason_mask as reasonMask,
528
+ priority,
529
+ to_seq_inclusive as toSeqInclusive,
530
+ message_count as messageCount,
531
+ delivery_payload_json as deliveryPayloadJson,
532
+ task_policy_snapshot_json as taskPolicySnapshotJson
533
+ FROM agent_notification_rounds
534
+ WHERE round_id = ?
535
+ AND delivery_id = ?
536
+ AND status = 'leased'
537
+ LIMIT 1`).get(params.roundId, params.deliveryId);
538
+ if (!row)
539
+ return;
540
+ const existingReady = db.prepare(`SELECT round_id as roundId
541
+ FROM agent_notification_rounds
542
+ WHERE target_agent_id = ?
543
+ AND channel_id = ?
544
+ AND surface_thread_key = ?
545
+ AND status = 'ready'
546
+ AND leased_until IS NULL
547
+ AND round_id != ?
548
+ ORDER BY created_at ASC
549
+ LIMIT 1`).get(row.targetAgentId, row.channelId, row.surfaceThreadKey, row.roundId);
550
+ if (existingReady) {
551
+ db.prepare(`UPDATE agent_notification_rounds
552
+ SET reason_mask = reason_mask | ?,
553
+ priority = MAX(priority, ?),
554
+ to_seq_inclusive = MAX(to_seq_inclusive, ?),
555
+ message_count = message_count + ?,
556
+ not_before = MAX(not_before, ?),
557
+ task_policy_snapshot_json = COALESCE(task_policy_snapshot_json, ?),
558
+ failed_at = ?,
559
+ last_error = COALESCE(?, last_error),
560
+ updated_at = ?
561
+ WHERE round_id = ?`).run(row.reasonMask, row.priority, row.toSeqInclusive, Math.max(1, row.messageCount), retryAt, row.taskPolicySnapshotJson, now, errorText, now, existingReady.roundId);
562
+ db.prepare(`UPDATE agent_notification_rounds
563
+ SET status = 'superseded',
564
+ superseded_at = ?,
565
+ leased_until = NULL,
566
+ attempt_token = NULL,
567
+ delivery_id = NULL,
568
+ delivery_mode = NULL,
569
+ delivery_payload_json = ?,
570
+ failed_at = ?,
571
+ last_error = COALESCE(?, last_error),
572
+ updated_at = ?
573
+ WHERE round_id = ?`).run(now, redactNotificationDeliveryPayloadJson(row.deliveryPayloadJson), now, errorText, now, row.roundId);
574
+ return;
575
+ }
576
+ db.prepare(`UPDATE agent_notification_rounds
577
+ SET status = 'ready',
578
+ not_before = ?,
579
+ leased_until = NULL,
580
+ attempt_token = NULL,
581
+ delivery_id = NULL,
582
+ delivery_mode = NULL,
583
+ failed_at = ?,
584
+ last_error = ?,
585
+ updated_at = ?
586
+ WHERE round_id = ?
587
+ AND delivery_id = ?
588
+ AND status = 'leased'`).run(retryAt, now, errorText, now, params.roundId, params.deliveryId);
589
+ })();
590
+ }
591
+ export function ackNotificationDelivery(db, params) {
592
+ const now = params.now ?? Date.now();
593
+ return db.transaction(() => {
594
+ const row = db.prepare(`SELECT round_id as roundId,
595
+ target_agent_id as targetAgentId,
596
+ channel_id as channelId,
597
+ surface_thread_key as surfaceThreadKey,
598
+ delivery_id as deliveryId,
599
+ status,
600
+ to_seq_inclusive as toSeqInclusive,
601
+ delivery_payload_json as deliveryPayloadJson
602
+ FROM agent_notification_rounds
603
+ WHERE round_id = ?
604
+ LIMIT 1`).get(params.roundId);
605
+ if (!row
606
+ || row.deliveryId !== params.deliveryId
607
+ || !['leased', 'delivered'].includes(row.status)
608
+ || Math.max(0, Math.floor(params.ackedThroughSeq)) !== row.toSeqInclusive) {
609
+ return false;
610
+ }
611
+ const ackedThroughSeq = row.toSeqInclusive;
612
+ advanceAgentSurfaceNotificationAck(db, {
613
+ targetAgentId: row.targetAgentId,
614
+ channelId: row.channelId,
615
+ threadRootId: row.surfaceThreadKey || null,
616
+ ackedThroughSeq,
617
+ now,
618
+ excludeRoundId: params.roundId,
619
+ });
620
+ db.prepare(`UPDATE agent_notification_rounds
621
+ SET status = 'acknowledged',
622
+ delivery_mode = ?,
623
+ acked_at = COALESCE(acked_at, ?),
624
+ delivered_at = COALESCE(delivered_at, ?),
625
+ delivery_payload_json = ?,
626
+ leased_until = NULL,
627
+ updated_at = ?
628
+ WHERE round_id = ?
629
+ AND delivery_id = ?`).run(params.mode, now, now, redactNotificationDeliveryPayloadJson(row.deliveryPayloadJson), now, params.roundId, params.deliveryId);
630
+ return true;
631
+ })();
632
+ }
633
+ export function deleteAgentNotificationV2State(db, agentId) {
634
+ db.prepare(`DELETE FROM agent_surface_notification_state WHERE target_agent_id = ?`).run(agentId);
635
+ db.prepare(`DELETE FROM agent_notification_rounds
636
+ WHERE target_agent_id = ? OR from_agent_id = ?`).run(agentId, agentId);
637
+ db.prepare(`DELETE FROM agent_surface_roster
638
+ WHERE target_agent_id = ? OR source_agent_id = ?`).run(agentId, agentId);
639
+ }
640
+ export function deleteNotificationV2StateForChannel(db, channelId) {
641
+ db.prepare(`DELETE FROM agent_surface_notification_state WHERE channel_id = ?`).run(channelId);
642
+ db.prepare(`DELETE FROM agent_notification_rounds WHERE channel_id = ?`).run(channelId);
643
+ db.prepare(`DELETE FROM agent_surface_roster WHERE channel_id = ?`).run(channelId);
644
+ }
645
+ export function deleteNotificationV2StateForThreadRoots(db, params) {
646
+ const threadRootIds = Array.from(new Set(params.threadRootIds
647
+ .map((threadRootId) => normalizeNotificationSurfaceThreadKey(threadRootId))
648
+ .filter(Boolean)));
649
+ if (threadRootIds.length === 0)
650
+ return;
651
+ const placeholders = threadRootIds.map(() => '?').join(', ');
652
+ db.prepare(`DELETE FROM agent_surface_notification_state
653
+ WHERE channel_id = ?
654
+ AND surface_thread_key IN (${placeholders})`).run(params.channelId, ...threadRootIds);
655
+ db.prepare(`DELETE FROM agent_notification_rounds
656
+ WHERE channel_id = ?
657
+ AND surface_thread_key IN (${placeholders})`).run(params.channelId, ...threadRootIds);
658
+ db.prepare(`DELETE FROM agent_surface_roster
659
+ WHERE channel_id = ?
660
+ AND surface_thread_key IN (${placeholders})`).run(params.channelId, ...threadRootIds);
661
+ }
662
+ export function supersedeNotificationRoundsForThreadRoots(db, params) {
663
+ const now = params.now ?? Date.now();
664
+ const threadRootIds = Array.from(new Set(params.threadRootIds
665
+ .map((threadRootId) => normalizeNotificationSurfaceThreadKey(threadRootId))
666
+ .filter(Boolean)));
667
+ if (threadRootIds.length === 0)
668
+ return 0;
669
+ const placeholders = threadRootIds.map(() => '?').join(', ');
670
+ const chatReasonClause = params.chatOnly ?? true
671
+ ? `AND (reason_mask & ?) != 0
672
+ AND (reason_mask & ?) = 0`
673
+ : '';
674
+ const rows = db.prepare(`SELECT round_id as roundId,
675
+ target_agent_id as targetAgentId,
676
+ channel_id as channelId,
677
+ surface_thread_key as surfaceThreadKey,
678
+ conversation_id as conversationId,
679
+ reason_mask as reasonMask,
680
+ priority,
681
+ from_seq_exclusive as fromSeqExclusive,
682
+ to_seq_inclusive as toSeqInclusive,
683
+ status,
684
+ delivery_id as deliveryId,
685
+ delivery_payload_json as deliveryPayloadJson,
686
+ task_policy_snapshot_json as taskPolicySnapshotJson,
687
+ attempt_count as attemptCount
688
+ FROM agent_notification_rounds
689
+ WHERE channel_id = ?
690
+ AND surface_thread_key IN (${placeholders})
691
+ AND status IN ('ready', 'leased', 'delivered')
692
+ AND acked_at IS NULL
693
+ ${chatReasonClause}`).all(params.channelId, ...threadRootIds, ...(params.chatOnly ?? true
694
+ ? [
695
+ NOTIFICATION_REASON_MASK.participant_heads_up | NOTIFICATION_REASON_MASK.explicit_agent_mention,
696
+ NOTIFICATION_REASON_MASK.human_prompt | NOTIFICATION_REASON_MASK.task_control_notice,
697
+ ]
698
+ : []));
699
+ let superseded = 0;
700
+ for (const row of rows) {
701
+ let payload = null;
702
+ try {
703
+ payload = parsePayload(row.deliveryPayloadJson);
704
+ }
705
+ catch {
706
+ payload = null;
707
+ }
708
+ const current = loadTaskPolicySnapshotForSurface(db, {
709
+ targetAgentId: row.targetAgentId,
710
+ channelId: row.channelId,
711
+ surfaceThreadKey: row.surfaceThreadKey,
712
+ });
713
+ if (payload && current && canDeliverClosedTaskPeerInboxRound(db, row, payload, current)) {
714
+ continue;
715
+ }
716
+ supersedeNotificationRound(db, {
717
+ roundId: row.roundId,
718
+ now,
719
+ error: params.reason,
720
+ });
721
+ superseded += 1;
722
+ }
723
+ return superseded;
724
+ }
725
+ function hasQueuedHumanPrompt(db, conversationId) {
726
+ const row = db.prepare(`SELECT 1
727
+ FROM conversation_prompt_queue
728
+ WHERE conversation_id = ?
729
+ AND record_as_user_message != 0
730
+ AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
731
+ LIMIT 1`).get(conversationId);
732
+ return Boolean(row);
733
+ }
734
+ function getConversationRunState(db, conversationId) {
735
+ const row = db.prepare(`SELECT c.status,
736
+ r.run_id as runId
737
+ FROM conversations c
738
+ LEFT JOIN runs r
739
+ ON r.session_key = c.session_key
740
+ AND r.ended_at IS NULL
741
+ WHERE c.id = ?
742
+ ORDER BY r.started_at DESC
743
+ LIMIT 1`).get(conversationId);
744
+ return row ?? null;
745
+ }
746
+ function canSteerReason(row, payload) {
747
+ if (payload.allowActiveSteer === false)
748
+ return false;
749
+ return (row.reasonMask & NOTIFICATION_REASON_MASK.task_control_notice) !== 0
750
+ || (row.reasonMask & NOTIFICATION_REASON_MASK.human_prompt) !== 0
751
+ || (row.reasonMask & NOTIFICATION_REASON_MASK.explicit_agent_mention) !== 0
752
+ || (payload.allowActiveSteer === true
753
+ && (row.reasonMask & NOTIFICATION_REASON_MASK.participant_heads_up) !== 0);
754
+ }
755
+ function isChatNotificationRound(row) {
756
+ return (row.reasonMask & (NOTIFICATION_REASON_MASK.participant_heads_up
757
+ | NOTIFICATION_REASON_MASK.explicit_agent_mention)) !== 0
758
+ && (row.reasonMask & (NOTIFICATION_REASON_MASK.human_prompt
759
+ | NOTIFICATION_REASON_MASK.task_control_notice)) === 0;
760
+ }
761
+ export function isClosedTaskStatus(status) {
762
+ return status === 'in_review' || status === 'done';
763
+ }
764
+ function isPeerInboxAggregatePayload(payload) {
765
+ const aggregate = payload.activationMetadata?.peerInboxAggregate;
766
+ return Boolean(aggregate
767
+ && typeof aggregate.aggregateId === 'string'
768
+ && Number.isFinite(aggregate.firstSeq)
769
+ && Number.isFinite(aggregate.lastSeq)
770
+ && Number.isFinite(aggregate.messageCount));
771
+ }
772
+ export function getTaskCloseEventAt(db, params) {
773
+ if (!isClosedTaskStatus(params.status))
774
+ return null;
775
+ const row = db.prepare(`SELECT MAX(created_at) as closedAt
776
+ FROM task_events
777
+ WHERE task_id = ?
778
+ AND event_type = 'status_changed'
779
+ AND to_status = ?`).get(params.taskId, params.status);
780
+ return typeof row?.closedAt === 'number' ? row.closedAt : null;
781
+ }
782
+ export function peerInboxMessagesWereCreatedBeforeTaskClose(db, params) {
783
+ const closedAt = getTaskCloseEventAt(db, {
784
+ taskId: params.taskId,
785
+ status: params.taskStatus,
786
+ });
787
+ if (closedAt == null)
788
+ return false;
789
+ const firstSeq = Math.max(1, Math.floor(params.firstSeq));
790
+ const lastSeq = Math.max(firstSeq, Math.floor(params.lastSeq));
791
+ const messageWindow = db.prepare(`SELECT COUNT(*) as messageCount,
792
+ MAX(created_at) as maxCreatedAt
793
+ FROM channel_messages
794
+ WHERE channel_id = ?
795
+ AND COALESCE(thread_root_id, '') = ?
796
+ AND seq BETWEEN ? AND ?`).get(params.channelId, params.surfaceThreadKey, firstSeq, lastSeq);
797
+ return Boolean(messageWindow
798
+ && messageWindow.messageCount > 0
799
+ && typeof messageWindow.maxCreatedAt === 'number'
800
+ && messageWindow.maxCreatedAt <= closedAt);
801
+ }
802
+ function canDeliverClosedTaskPeerInboxRound(db, row, payload, current) {
803
+ const aggregate = payload.activationMetadata?.peerInboxAggregate;
804
+ return isClosedTaskStatus(current.status)
805
+ && isChatNotificationRound(row)
806
+ && isPeerInboxAggregatePayload(payload)
807
+ && ((row.reasonMask & NOTIFICATION_REASON_MASK.participant_heads_up) === 0
808
+ || Boolean(current.targetParticipantRole))
809
+ && Boolean(aggregate)
810
+ && peerInboxMessagesWereCreatedBeforeTaskClose(db, {
811
+ channelId: row.channelId,
812
+ surfaceThreadKey: row.surfaceThreadKey,
813
+ firstSeq: aggregate?.firstSeq ?? 0,
814
+ lastSeq: aggregate?.lastSeq ?? 0,
815
+ taskId: current.taskId,
816
+ taskStatus: current.status,
817
+ });
818
+ }
819
+ function supersedeNotificationRound(db, params) {
820
+ const now = params.now ?? Date.now();
821
+ const deliveryClause = params.deliveryId ? 'AND delivery_id = ?' : '';
822
+ const row = db.prepare(`SELECT delivery_payload_json as deliveryPayloadJson
823
+ FROM agent_notification_rounds
824
+ WHERE round_id = ?
825
+ ${deliveryClause}
826
+ AND status IN ('ready', 'leased', 'delivered')
827
+ LIMIT 1`).get(...(params.deliveryId ? [params.roundId, params.deliveryId] : [params.roundId]));
828
+ if (!row)
829
+ return;
830
+ const args = params.deliveryId
831
+ ? [now, redactNotificationDeliveryPayloadJson(row.deliveryPayloadJson), params.error?.slice(0, 1000) ?? null, now, params.roundId, params.deliveryId]
832
+ : [now, redactNotificationDeliveryPayloadJson(row.deliveryPayloadJson), params.error?.slice(0, 1000) ?? null, now, params.roundId];
833
+ db.prepare(`UPDATE agent_notification_rounds
834
+ SET status = 'superseded',
835
+ superseded_at = ?,
836
+ leased_until = NULL,
837
+ attempt_token = NULL,
838
+ delivery_id = NULL,
839
+ delivery_mode = NULL,
840
+ delivery_payload_json = ?,
841
+ last_error = COALESCE(?, last_error),
842
+ updated_at = ?
843
+ WHERE round_id = ?
844
+ ${deliveryClause}
845
+ AND status IN ('ready', 'leased', 'delivered')`).run(...args);
846
+ }
847
+ function validateNotificationRoundTaskPolicy(db, row, payload) {
848
+ const current = loadTaskPolicySnapshotForSurface(db, {
849
+ targetAgentId: row.targetAgentId,
850
+ channelId: row.channelId,
851
+ surfaceThreadKey: row.surfaceThreadKey,
852
+ });
853
+ const snapshot = parseTaskPolicySnapshot(row.taskPolicySnapshotJson);
854
+ if (!current && !snapshot)
855
+ return { ok: true };
856
+ if (!current)
857
+ return { ok: false, reason: 'Task binding no longer exists' };
858
+ if (!snapshot) {
859
+ if ((row.reasonMask & NOTIFICATION_REASON_MASK.participant_heads_up) !== 0
860
+ && !current.targetParticipantRole) {
861
+ return { ok: false, reason: 'Target is no longer a current task participant' };
862
+ }
863
+ if (isClosedTaskStatus(current.status) && isChatNotificationRound(row)) {
864
+ return canDeliverClosedTaskPeerInboxRound(db, row, payload, current)
865
+ ? { ok: true }
866
+ : { ok: false, reason: `Task thread is ${current.status}` };
867
+ }
868
+ return { ok: true };
869
+ }
870
+ if ((row.reasonMask & NOTIFICATION_REASON_MASK.participant_heads_up) !== 0
871
+ && !current.targetParticipantRole) {
872
+ return { ok: false, reason: 'Target is no longer a current task participant' };
873
+ }
874
+ if (current.taskId !== snapshot.taskId) {
875
+ return { ok: false, reason: 'Task binding changed' };
876
+ }
877
+ if (isClosedTaskStatus(current.status) && isChatNotificationRound(row)) {
878
+ if (canDeliverClosedTaskPeerInboxRound(db, row, payload, current)) {
879
+ return { ok: true };
880
+ }
881
+ return current.updatedAt !== snapshot.updatedAt
882
+ ? { ok: false, reason: 'Task policy snapshot is stale' }
883
+ : { ok: false, reason: `Task thread is ${current.status}` };
884
+ }
885
+ if (current.updatedAt !== snapshot.updatedAt) {
886
+ return { ok: false, reason: 'Task policy snapshot is stale' };
887
+ }
888
+ return { ok: true };
889
+ }
890
+ export async function processReadyNotificationRounds(params) {
891
+ const now = params.now ?? Date.now();
892
+ const limit = Math.max(1, params.limit ?? 10);
893
+ let delivered = 0;
894
+ let deferred = 0;
895
+ for (let index = 0; index < limit; index += 1) {
896
+ const round = leaseReadyNotificationRound(params.db, {
897
+ now,
898
+ roundId: params.roundId,
899
+ });
900
+ if (!round)
901
+ break;
902
+ let payload;
903
+ try {
904
+ payload = parsePayload(round.deliveryPayloadJson);
905
+ }
906
+ catch (error) {
907
+ releaseNotificationRoundForRetry(params.db, {
908
+ roundId: round.roundId,
909
+ deliveryId: round.deliveryId,
910
+ error: String(error?.message ?? error),
911
+ now,
912
+ });
913
+ deferred += 1;
914
+ continue;
915
+ }
916
+ const runState = getConversationRunState(params.db, round.conversationId);
917
+ if (!runState) {
918
+ releaseNotificationRoundForRetry(params.db, {
919
+ roundId: round.roundId,
920
+ deliveryId: round.deliveryId,
921
+ error: 'Conversation not found',
922
+ now,
923
+ retryMs: 60_000,
924
+ });
925
+ deferred += 1;
926
+ continue;
927
+ }
928
+ const taskPolicy = validateNotificationRoundTaskPolicy(params.db, round, payload);
929
+ if (!taskPolicy.ok) {
930
+ supersedeNotificationRound(params.db, {
931
+ roundId: round.roundId,
932
+ deliveryId: round.deliveryId,
933
+ error: taskPolicy.reason,
934
+ now,
935
+ });
936
+ deferred += 1;
937
+ continue;
938
+ }
939
+ const queuedHumanPromptBlocksRound = hasQueuedHumanPrompt(params.db, round.conversationId)
940
+ && (round.reasonMask & NOTIFICATION_REASON_MASK.human_prompt) === 0;
941
+ if (queuedHumanPromptBlocksRound) {
942
+ releaseNotificationRoundForRetry(params.db, {
943
+ roundId: round.roundId,
944
+ deliveryId: round.deliveryId,
945
+ error: 'Blocked by queued human prompt',
946
+ now,
947
+ });
948
+ deferred += 1;
949
+ continue;
950
+ }
951
+ const steerAllowed = canSteerReason(round, payload);
952
+ if (runState.runId
953
+ && !steerAllowed) {
954
+ releaseNotificationRoundForRetry(params.db, {
955
+ roundId: round.roundId,
956
+ deliveryId: round.deliveryId,
957
+ error: 'Active run is not steerable for this notification reason',
958
+ now,
959
+ });
960
+ deferred += 1;
961
+ continue;
962
+ }
963
+ try {
964
+ const result = await params.conversationManager.submitPrompt(round.conversationId, payload.promptText, {
965
+ recordAsUserMessage: false,
966
+ activationContextText: payload.activationContextText,
967
+ activationContextSections: payload.activationContextSections,
968
+ activationContextMode: payload.activationContextMode ?? 'resume_only',
969
+ resumeContextText: payload.resumeContextText,
970
+ resumeContextSections: payload.resumeContextSections,
971
+ replayOverlapRecentMessages: payload.replayOverlapRecentMessages,
972
+ activationMetadata: payload.activationMetadata,
973
+ attachments: payload.attachments,
974
+ attachmentIds: payload.attachmentIds,
975
+ notificationDelivery: round.delivery,
976
+ allowActiveSteer: steerAllowed,
977
+ allowQueue: false,
978
+ ...(payload.suppressReplyContract === true ? { suppressReplyContract: true } : {}),
979
+ });
980
+ if (result.queued && !result.runId) {
981
+ releaseNotificationRoundForRetry(params.db, {
982
+ roundId: round.roundId,
983
+ deliveryId: round.deliveryId,
984
+ error: 'Notification delivery was deferred',
985
+ now,
986
+ });
987
+ deferred += 1;
988
+ continue;
989
+ }
990
+ markNotificationRoundDelivered(params.db, {
991
+ roundId: round.roundId,
992
+ deliveryId: round.deliveryId,
993
+ mode: result.steered ? 'steer' : 'dispatch',
994
+ now,
995
+ });
996
+ delivered += 1;
997
+ }
998
+ catch (error) {
999
+ releaseNotificationRoundForRetry(params.db, {
1000
+ roundId: round.roundId,
1001
+ deliveryId: round.deliveryId,
1002
+ error: String(error?.message ?? error),
1003
+ now,
1004
+ });
1005
+ deferred += 1;
1006
+ }
1007
+ }
1008
+ return { delivered, deferred };
1009
+ }
1010
+ export async function enqueueAndDeliverNotificationRound(params) {
1011
+ const round = createOrMergeNotificationRound(params.db, params);
1012
+ if (round.skipped || !round.roundId || !round.acceptedPayload) {
1013
+ return { roundId: round.roundId, delivered: 0, deferred: 0, skipped: true, acceptedPayload: round.acceptedPayload };
1014
+ }
1015
+ const result = await processReadyNotificationRounds({
1016
+ db: params.db,
1017
+ conversationManager: params.conversationManager,
1018
+ roundId: round.roundId,
1019
+ now: params.now,
1020
+ limit: 1,
1021
+ });
1022
+ return {
1023
+ roundId: round.roundId,
1024
+ delivered: result.delivered,
1025
+ deferred: result.deferred,
1026
+ skipped: false,
1027
+ acceptedPayload: round.acceptedPayload,
1028
+ };
1029
+ }
1030
+ export function startNotificationDeliveryService(params) {
1031
+ const intervalMs = Math.max(1_000, params.intervalMs ?? 1_000);
1032
+ let timer = null;
1033
+ let running = false;
1034
+ const tick = async () => {
1035
+ if (running)
1036
+ return;
1037
+ running = true;
1038
+ try {
1039
+ await processReadyNotificationRounds({
1040
+ db: params.db,
1041
+ conversationManager: params.conversationManager,
1042
+ });
1043
+ }
1044
+ finally {
1045
+ running = false;
1046
+ }
1047
+ };
1048
+ timer = setInterval(() => {
1049
+ void tick();
1050
+ }, intervalMs);
1051
+ timer.unref?.();
1052
+ return {
1053
+ tick,
1054
+ stop: () => {
1055
+ if (!timer)
1056
+ return;
1057
+ clearInterval(timer);
1058
+ timer = null;
1059
+ },
1060
+ };
1061
+ }