@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.
- package/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- 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
|
+
}
|