@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,1668 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export const AGENT_REMINDER_TIMEZONE = 'Asia/Shanghai';
3
+ export const AGENT_REMINDER_POLL_INTERVAL_MS = 5_000;
4
+ const OPEN_REMINDER_OCCURRENCE_STATUSES = ['pending', 'dispatched', 'awaiting_review', 'failed', 'snoozed'];
5
+ function intervalMs(unit, value) {
6
+ if (!unit || !value || value <= 0)
7
+ return null;
8
+ switch (unit) {
9
+ case 'hour':
10
+ return value * 60 * 60 * 1000;
11
+ case 'day':
12
+ return value * 24 * 60 * 60 * 1000;
13
+ case 'week':
14
+ return value * 7 * 24 * 60 * 60 * 1000;
15
+ }
16
+ }
17
+ function countReminderOccurrences(db, reminderId) {
18
+ const row = db.prepare(`SELECT COUNT(*) as count
19
+ FROM agent_reminder_occurrences
20
+ WHERE reminder_id = ?
21
+ AND COALESCE(trigger_kind, 'scheduled') = 'scheduled'`).get(reminderId);
22
+ return row?.count ?? 0;
23
+ }
24
+ function countOpenReminderOccurrences(db, reminderId) {
25
+ const row = db.prepare(`SELECT COUNT(*) as count
26
+ FROM agent_reminder_occurrences
27
+ WHERE reminder_id = ?
28
+ AND status IN ('pending', 'dispatched', 'awaiting_review', 'failed', 'snoozed')`).get(reminderId);
29
+ return row?.count ?? 0;
30
+ }
31
+ export function isOpenReminderOccurrenceStatus(status) {
32
+ return OPEN_REMINDER_OCCURRENCE_STATUSES.includes(status);
33
+ }
34
+ function normalizePositiveInt(value) {
35
+ if (value == null)
36
+ return null;
37
+ const parsed = Number(value);
38
+ if (!Number.isFinite(parsed) || parsed <= 0)
39
+ return null;
40
+ return Math.floor(parsed);
41
+ }
42
+ function normalizeNullableTimestamp(value) {
43
+ if (value == null)
44
+ return null;
45
+ const parsed = Number(value);
46
+ if (!Number.isFinite(parsed) || parsed <= 0)
47
+ return null;
48
+ return Math.floor(parsed);
49
+ }
50
+ function normalizeRequiredText(value, field) {
51
+ if (typeof value !== 'string' || !value.trim()) {
52
+ throw new Error(`${field} is required`);
53
+ }
54
+ return value.trim();
55
+ }
56
+ function normalizeReminderUpsertInput(request, existing) {
57
+ const title = 'title' in request && request.title !== undefined
58
+ ? normalizeRequiredText(request.title, 'title')
59
+ : existing?.title;
60
+ const promptText = 'promptText' in request && request.promptText !== undefined
61
+ ? normalizeRequiredText(request.promptText, 'promptText')
62
+ : existing?.promptText;
63
+ const scheduleKind = 'scheduleKind' in request && request.scheduleKind !== undefined
64
+ ? request.scheduleKind
65
+ : existing?.scheduleKind;
66
+ const startAt = 'startAt' in request && request.startAt !== undefined
67
+ ? normalizeNullableTimestamp(request.startAt)
68
+ : existing?.startAt ?? null;
69
+ const intervalUnit = 'intervalUnit' in request
70
+ ? (request.intervalUnit ?? null)
71
+ : (existing?.intervalUnit ?? null);
72
+ const intervalValue = 'intervalValue' in request
73
+ ? normalizePositiveInt(request.intervalValue)
74
+ : (existing?.intervalValue ?? null);
75
+ const endAt = 'endAt' in request
76
+ ? normalizeNullableTimestamp(request.endAt)
77
+ : (existing?.endAt ?? null);
78
+ const maxOccurrences = 'maxOccurrences' in request
79
+ ? normalizePositiveInt(request.maxOccurrences)
80
+ : (existing?.maxOccurrences ?? null);
81
+ if (!title)
82
+ throw new Error('title is required');
83
+ if (!promptText)
84
+ throw new Error('promptText is required');
85
+ if (scheduleKind !== 'one_time' && scheduleKind !== 'recurring') {
86
+ throw new Error('scheduleKind must be one_time or recurring');
87
+ }
88
+ if (!startAt)
89
+ throw new Error('startAt is required');
90
+ if (endAt != null && endAt < startAt) {
91
+ throw new Error('endAt must be greater than or equal to startAt');
92
+ }
93
+ if (scheduleKind === 'recurring') {
94
+ if (intervalUnit !== 'hour' && intervalUnit !== 'day' && intervalUnit !== 'week') {
95
+ throw new Error('intervalUnit is required for recurring reminders');
96
+ }
97
+ if (!intervalValue) {
98
+ throw new Error('intervalValue is required for recurring reminders');
99
+ }
100
+ }
101
+ return {
102
+ title,
103
+ promptText,
104
+ scheduleKind,
105
+ startAt,
106
+ intervalUnit: scheduleKind === 'recurring' ? intervalUnit : null,
107
+ intervalValue: scheduleKind === 'recurring' ? intervalValue : null,
108
+ endAt,
109
+ maxOccurrences: scheduleKind === 'recurring' ? maxOccurrences : null,
110
+ };
111
+ }
112
+ function computeNextRunAt(params) {
113
+ if (params.maxOccurrences != null && params.existingOccurrenceCount >= params.maxOccurrences) {
114
+ return null;
115
+ }
116
+ if (params.scheduleKind === 'one_time') {
117
+ if (params.existingOccurrenceCount > 0 || params.lastScheduledAt != null)
118
+ return null;
119
+ const candidate = params.startAt;
120
+ if (params.endAt != null && candidate > params.endAt)
121
+ return null;
122
+ return candidate;
123
+ }
124
+ const interval = intervalMs(params.intervalUnit, params.intervalValue);
125
+ if (!interval)
126
+ return null;
127
+ let candidate = params.lastScheduledAt != null
128
+ ? params.lastScheduledAt + interval
129
+ : params.startAt;
130
+ if (params.lastScheduledAt == null && candidate < params.now) {
131
+ const delta = params.now - params.startAt;
132
+ const steps = Math.ceil(delta / interval);
133
+ candidate = params.startAt + Math.max(0, steps) * interval;
134
+ }
135
+ if (params.endAt != null && candidate > params.endAt)
136
+ return null;
137
+ return candidate;
138
+ }
139
+ function mapOccurrenceRow(row) {
140
+ return {
141
+ occurrenceId: row.occurrenceId,
142
+ reminderId: row.reminderId,
143
+ scheduledFor: row.scheduledFor,
144
+ startedAt: row.startedAt,
145
+ completedAt: row.completedAt,
146
+ status: row.status,
147
+ taskId: row.taskId,
148
+ conversationId: row.conversationId,
149
+ threadRootId: row.threadRootId,
150
+ taskNumber: row.taskNumber,
151
+ taskTitle: row.taskTitle,
152
+ taskStatus: row.taskStatus,
153
+ error: row.error,
154
+ createdAt: row.createdAt,
155
+ updatedAt: row.updatedAt,
156
+ };
157
+ }
158
+ function hasReminderTriggered(db, row) {
159
+ if (row.scheduleKind !== 'one_time')
160
+ return false;
161
+ if (row.lastScheduledAt != null)
162
+ return true;
163
+ return countReminderOccurrences(db, row.reminderId) > 0;
164
+ }
165
+ function isRetryableFailedOneTimeOccurrence(occurrence) {
166
+ return Boolean(occurrence
167
+ && occurrence.status === 'failed'
168
+ && !occurrence.taskId
169
+ && !occurrence.conversationId
170
+ && !occurrence.threadRootId);
171
+ }
172
+ function assertOneTimeScheduleStillMutable(db, row, request) {
173
+ if (!hasReminderTriggered(db, row))
174
+ return;
175
+ const scheduleChanged = (request.scheduleKind !== row.scheduleKind
176
+ || request.startAt !== row.startAt
177
+ || request.intervalUnit !== row.intervalUnit
178
+ || request.intervalValue !== row.intervalValue
179
+ || request.endAt !== row.endAt
180
+ || request.maxOccurrences !== row.maxOccurrences);
181
+ if (scheduleChanged) {
182
+ throw new Error('One-time reminder schedule cannot change after its first occurrence');
183
+ }
184
+ }
185
+ function ensureOneTimeOccurrenceSupportsReview(reminder) {
186
+ if (reminder.scheduleKind !== 'one_time')
187
+ return;
188
+ throw new Error('One-time reminder occurrences must use bigbang reminder submit-for-review before user approval');
189
+ }
190
+ function ensureOneTimeFollowUpActionAllowed(reminder, action) {
191
+ if (reminder.scheduleKind !== 'one_time')
192
+ return;
193
+ throw new Error(`One-time reminder occurrences do not support ${action}; edit the brief or abandon the reminder instead`);
194
+ }
195
+ function mapReminderRow(row, recentOccurrences = []) {
196
+ return {
197
+ reminderId: row.reminderId,
198
+ agentId: row.agentId,
199
+ targetUserId: row.targetUserId,
200
+ title: row.title,
201
+ promptText: row.promptText,
202
+ scheduleKind: row.scheduleKind,
203
+ intervalUnit: row.intervalUnit,
204
+ intervalValue: row.intervalValue,
205
+ startAt: row.startAt,
206
+ endAt: row.endAt,
207
+ maxOccurrences: row.maxOccurrences,
208
+ status: row.status,
209
+ timezone: row.timezone,
210
+ lastScheduledAt: row.lastScheduledAt,
211
+ nextRunAt: row.nextRunAt,
212
+ createdByUserId: row.createdByUserId,
213
+ createdByAgentId: row.createdByAgentId,
214
+ createdAt: row.createdAt,
215
+ updatedAt: row.updatedAt,
216
+ recentOccurrences,
217
+ };
218
+ }
219
+ function appendReminderEvent(db, params) {
220
+ db.prepare(`INSERT INTO agent_reminder_events(
221
+ event_id, reminder_id, occurrence_id, event_type, actor_type, actor_id, actor_name, payload_json, created_at
222
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(randomUUID(), params.reminderId, params.occurrenceId ?? null, params.eventType, params.actorType, params.actorId ?? null, params.actorName ?? null, params.payload ? JSON.stringify(params.payload) : null, params.createdAt ?? Date.now());
223
+ }
224
+ function getReminderRowById(db, reminderId) {
225
+ const row = db.prepare(`SELECT reminder_id as reminderId,
226
+ agent_id as agentId,
227
+ target_user_id as targetUserId,
228
+ title,
229
+ prompt_text as promptText,
230
+ schedule_kind as scheduleKind,
231
+ interval_unit as intervalUnit,
232
+ interval_value as intervalValue,
233
+ start_at as startAt,
234
+ end_at as endAt,
235
+ max_occurrences as maxOccurrences,
236
+ status,
237
+ timezone,
238
+ last_scheduled_at as lastScheduledAt,
239
+ next_run_at as nextRunAt,
240
+ created_by_user_id as createdByUserId,
241
+ created_by_agent_id as createdByAgentId,
242
+ created_at as createdAt,
243
+ updated_at as updatedAt
244
+ FROM agent_reminders
245
+ WHERE reminder_id = ?
246
+ LIMIT 1`).get(reminderId);
247
+ return row ?? null;
248
+ }
249
+ function getReminderOccurrenceRowById(db, occurrenceId) {
250
+ const row = db.prepare(`SELECT aro.occurrence_id as occurrenceId,
251
+ aro.reminder_id as reminderId,
252
+ aro.scheduled_for as scheduledFor,
253
+ aro.started_at as startedAt,
254
+ aro.completed_at as completedAt,
255
+ aro.status,
256
+ aro.task_id as taskId,
257
+ aro.conversation_id as conversationId,
258
+ aro.thread_root_id as threadRootId,
259
+ aro.error,
260
+ aro.created_at as createdAt,
261
+ aro.updated_at as updatedAt,
262
+ t.task_number as taskNumber,
263
+ t.title as taskTitle,
264
+ t.status as taskStatus
265
+ FROM agent_reminder_occurrences aro
266
+ LEFT JOIN tasks t ON t.task_id = aro.task_id
267
+ WHERE aro.occurrence_id = ?
268
+ LIMIT 1`).get(occurrenceId);
269
+ return row ?? null;
270
+ }
271
+ function getLatestReminderOccurrenceRowByReminderId(db, reminderId) {
272
+ const row = db.prepare(`SELECT aro.occurrence_id as occurrenceId,
273
+ aro.reminder_id as reminderId,
274
+ aro.scheduled_for as scheduledFor,
275
+ aro.started_at as startedAt,
276
+ aro.completed_at as completedAt,
277
+ aro.status,
278
+ aro.task_id as taskId,
279
+ aro.conversation_id as conversationId,
280
+ aro.thread_root_id as threadRootId,
281
+ aro.error,
282
+ aro.created_at as createdAt,
283
+ aro.updated_at as updatedAt,
284
+ t.task_number as taskNumber,
285
+ t.title as taskTitle,
286
+ t.status as taskStatus
287
+ FROM agent_reminder_occurrences aro
288
+ LEFT JOIN tasks t ON t.task_id = aro.task_id
289
+ WHERE aro.reminder_id = ?
290
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC
291
+ LIMIT 1`).get(reminderId);
292
+ return row ?? null;
293
+ }
294
+ function getCurrentReminderOccurrenceRowByReminderId(db, reminderId) {
295
+ const row = db.prepare(`SELECT aro.occurrence_id as occurrenceId,
296
+ aro.reminder_id as reminderId,
297
+ aro.scheduled_for as scheduledFor,
298
+ aro.started_at as startedAt,
299
+ aro.completed_at as completedAt,
300
+ aro.status,
301
+ aro.task_id as taskId,
302
+ aro.conversation_id as conversationId,
303
+ aro.thread_root_id as threadRootId,
304
+ aro.error,
305
+ aro.created_at as createdAt,
306
+ aro.updated_at as updatedAt,
307
+ t.task_number as taskNumber,
308
+ t.title as taskTitle,
309
+ t.status as taskStatus
310
+ FROM agent_reminder_occurrences aro
311
+ LEFT JOIN tasks t ON t.task_id = aro.task_id
312
+ WHERE aro.reminder_id = ?
313
+ AND aro.status IN ('pending', 'dispatched', 'awaiting_review', 'failed', 'snoozed')
314
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC
315
+ LIMIT 1`).get(reminderId);
316
+ return row ?? null;
317
+ }
318
+ function hasReminderOccurrenceAtTime(db, reminderId, scheduledFor, excludeOccurrenceId) {
319
+ const row = excludeOccurrenceId
320
+ ? db.prepare(`SELECT 1 as found
321
+ FROM agent_reminder_occurrences
322
+ WHERE reminder_id = ?
323
+ AND scheduled_for = ?
324
+ AND occurrence_id != ?
325
+ LIMIT 1`).get(reminderId, scheduledFor, excludeOccurrenceId)
326
+ : db.prepare(`SELECT 1 as found
327
+ FROM agent_reminder_occurrences
328
+ WHERE reminder_id = ?
329
+ AND scheduled_for = ?
330
+ LIMIT 1`).get(reminderId, scheduledFor);
331
+ return !!row;
332
+ }
333
+ function allocateOccurrenceScheduledFor(db, reminderId, desiredScheduledFor, excludeOccurrenceId) {
334
+ let scheduledFor = Math.max(1, Math.floor(desiredScheduledFor));
335
+ while (hasReminderOccurrenceAtTime(db, reminderId, scheduledFor, excludeOccurrenceId)) {
336
+ scheduledFor += 1;
337
+ }
338
+ return scheduledFor;
339
+ }
340
+ function listRecentOccurrencesByReminderIds(db, reminderIds, limitPerReminder = 5) {
341
+ const uniqueReminderIds = [...new Set(reminderIds.filter(Boolean))];
342
+ const result = new Map();
343
+ if (uniqueReminderIds.length === 0)
344
+ return result;
345
+ const placeholders = uniqueReminderIds.map(() => '?').join(', ');
346
+ const rows = db.prepare(`SELECT aro.occurrence_id as occurrenceId,
347
+ aro.reminder_id as reminderId,
348
+ aro.scheduled_for as scheduledFor,
349
+ aro.started_at as startedAt,
350
+ aro.completed_at as completedAt,
351
+ aro.status,
352
+ aro.task_id as taskId,
353
+ aro.conversation_id as conversationId,
354
+ aro.thread_root_id as threadRootId,
355
+ aro.error,
356
+ aro.created_at as createdAt,
357
+ aro.updated_at as updatedAt,
358
+ t.task_number as taskNumber,
359
+ t.title as taskTitle,
360
+ t.status as taskStatus
361
+ FROM agent_reminder_occurrences aro
362
+ LEFT JOIN tasks t ON t.task_id = aro.task_id
363
+ WHERE aro.reminder_id IN (${placeholders})
364
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC`).all(...uniqueReminderIds);
365
+ for (const row of rows) {
366
+ const list = result.get(row.reminderId) ?? [];
367
+ if (list.length >= limitPerReminder)
368
+ continue;
369
+ list.push(mapOccurrenceRow(row));
370
+ result.set(row.reminderId, list);
371
+ }
372
+ return result;
373
+ }
374
+ function listReminderRowsForAgentUser(db, agentId, targetUserId) {
375
+ const rows = targetUserId == null
376
+ ? db.prepare(`SELECT reminder_id as reminderId,
377
+ agent_id as agentId,
378
+ target_user_id as targetUserId,
379
+ title,
380
+ prompt_text as promptText,
381
+ schedule_kind as scheduleKind,
382
+ interval_unit as intervalUnit,
383
+ interval_value as intervalValue,
384
+ start_at as startAt,
385
+ end_at as endAt,
386
+ max_occurrences as maxOccurrences,
387
+ status,
388
+ timezone,
389
+ last_scheduled_at as lastScheduledAt,
390
+ next_run_at as nextRunAt,
391
+ created_by_user_id as createdByUserId,
392
+ created_by_agent_id as createdByAgentId,
393
+ created_at as createdAt,
394
+ updated_at as updatedAt
395
+ FROM agent_reminders
396
+ WHERE agent_id = ?
397
+ AND target_user_id IS NULL
398
+ ORDER BY CASE status
399
+ WHEN 'needs_attention' THEN 0
400
+ WHEN 'awaiting_review' THEN 1
401
+ WHEN 'in_progress' THEN 2
402
+ WHEN 'active' THEN 3
403
+ WHEN 'paused' THEN 4
404
+ WHEN 'completed' THEN 5
405
+ WHEN 'cancelled' THEN 6
406
+ ELSE 7
407
+ END,
408
+ COALESCE(next_run_at, updated_at) ASC,
409
+ updated_at DESC`).all(agentId)
410
+ : db.prepare(`SELECT reminder_id as reminderId,
411
+ agent_id as agentId,
412
+ target_user_id as targetUserId,
413
+ title,
414
+ prompt_text as promptText,
415
+ schedule_kind as scheduleKind,
416
+ interval_unit as intervalUnit,
417
+ interval_value as intervalValue,
418
+ start_at as startAt,
419
+ end_at as endAt,
420
+ max_occurrences as maxOccurrences,
421
+ status,
422
+ timezone,
423
+ last_scheduled_at as lastScheduledAt,
424
+ next_run_at as nextRunAt,
425
+ created_by_user_id as createdByUserId,
426
+ created_by_agent_id as createdByAgentId,
427
+ created_at as createdAt,
428
+ updated_at as updatedAt
429
+ FROM agent_reminders
430
+ WHERE agent_id = ?
431
+ AND target_user_id = ?
432
+ ORDER BY CASE status
433
+ WHEN 'needs_attention' THEN 0
434
+ WHEN 'awaiting_review' THEN 1
435
+ WHEN 'in_progress' THEN 2
436
+ WHEN 'active' THEN 3
437
+ WHEN 'paused' THEN 4
438
+ WHEN 'completed' THEN 5
439
+ WHEN 'cancelled' THEN 6
440
+ ELSE 7
441
+ END,
442
+ COALESCE(next_run_at, updated_at) ASC,
443
+ updated_at DESC`).all(agentId, targetUserId);
444
+ return rows;
445
+ }
446
+ function findReminderRowForAgentUser(db, agentId, reminderId, targetUserId) {
447
+ const row = targetUserId == null
448
+ ? db.prepare(`SELECT reminder_id as reminderId,
449
+ agent_id as agentId,
450
+ target_user_id as targetUserId,
451
+ title,
452
+ prompt_text as promptText,
453
+ schedule_kind as scheduleKind,
454
+ interval_unit as intervalUnit,
455
+ interval_value as intervalValue,
456
+ start_at as startAt,
457
+ end_at as endAt,
458
+ max_occurrences as maxOccurrences,
459
+ status,
460
+ timezone,
461
+ last_scheduled_at as lastScheduledAt,
462
+ next_run_at as nextRunAt,
463
+ created_by_user_id as createdByUserId,
464
+ created_by_agent_id as createdByAgentId,
465
+ created_at as createdAt,
466
+ updated_at as updatedAt
467
+ FROM agent_reminders
468
+ WHERE reminder_id = ?
469
+ AND agent_id = ?
470
+ AND target_user_id IS NULL
471
+ LIMIT 1`).get(reminderId, agentId)
472
+ : db.prepare(`SELECT reminder_id as reminderId,
473
+ agent_id as agentId,
474
+ target_user_id as targetUserId,
475
+ title,
476
+ prompt_text as promptText,
477
+ schedule_kind as scheduleKind,
478
+ interval_unit as intervalUnit,
479
+ interval_value as intervalValue,
480
+ start_at as startAt,
481
+ end_at as endAt,
482
+ max_occurrences as maxOccurrences,
483
+ status,
484
+ timezone,
485
+ last_scheduled_at as lastScheduledAt,
486
+ next_run_at as nextRunAt,
487
+ created_by_user_id as createdByUserId,
488
+ created_by_agent_id as createdByAgentId,
489
+ created_at as createdAt,
490
+ updated_at as updatedAt
491
+ FROM agent_reminders
492
+ WHERE reminder_id = ?
493
+ AND agent_id = ?
494
+ AND target_user_id = ?
495
+ LIMIT 1`).get(reminderId, agentId, targetUserId);
496
+ return row ?? null;
497
+ }
498
+ function buildReminderInfoForRow(db, row) {
499
+ const recentOccurrences = listRecentOccurrencesByReminderIds(db, [row.reminderId]).get(row.reminderId) ?? [];
500
+ return mapReminderRow(row, recentOccurrences);
501
+ }
502
+ function buildReminderCurrentOccurrenceResult(db, reminderRow) {
503
+ const currentOccurrence = getCurrentReminderOccurrenceRowByReminderId(db, reminderRow.reminderId);
504
+ const latestOccurrence = currentOccurrence ?? getLatestReminderOccurrenceRowByReminderId(db, reminderRow.reminderId);
505
+ return {
506
+ reminder: buildReminderInfoForRow(db, reminderRow),
507
+ currentOccurrence: currentOccurrence ? mapOccurrenceRow(currentOccurrence) : null,
508
+ latestOccurrence: latestOccurrence ? mapOccurrenceRow(latestOccurrence) : null,
509
+ };
510
+ }
511
+ export function listAgentRemindersForUser(db, agentId, targetUserId) {
512
+ const rows = listReminderRowsForAgentUser(db, agentId, targetUserId);
513
+ const recentOccurrences = listRecentOccurrencesByReminderIds(db, rows.map((row) => row.reminderId));
514
+ return rows.map((row) => mapReminderRow(row, recentOccurrences.get(row.reminderId) ?? []));
515
+ }
516
+ export function getAgentReminderForUser(db, agentId, reminderId, targetUserId) {
517
+ const row = findReminderRowForAgentUser(db, agentId, reminderId, targetUserId);
518
+ return row ? buildReminderInfoForRow(db, row) : null;
519
+ }
520
+ export function listAgentReminderOccurrencesForUser(db, agentId, reminderId, targetUserId, limit = 50) {
521
+ const row = findReminderRowForAgentUser(db, agentId, reminderId, targetUserId);
522
+ if (!row)
523
+ return null;
524
+ const occurrences = db.prepare(`SELECT aro.occurrence_id as occurrenceId,
525
+ aro.reminder_id as reminderId,
526
+ aro.scheduled_for as scheduledFor,
527
+ aro.started_at as startedAt,
528
+ aro.completed_at as completedAt,
529
+ aro.status,
530
+ aro.task_id as taskId,
531
+ aro.conversation_id as conversationId,
532
+ aro.thread_root_id as threadRootId,
533
+ aro.error,
534
+ aro.created_at as createdAt,
535
+ aro.updated_at as updatedAt,
536
+ t.task_number as taskNumber,
537
+ t.title as taskTitle,
538
+ t.status as taskStatus
539
+ FROM agent_reminder_occurrences aro
540
+ LEFT JOIN tasks t ON t.task_id = aro.task_id
541
+ WHERE aro.reminder_id = ?
542
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC
543
+ LIMIT ?`).all(reminderId, limit);
544
+ return {
545
+ reminder: buildReminderInfoForRow(db, row),
546
+ occurrences: occurrences.map(mapOccurrenceRow),
547
+ };
548
+ }
549
+ export function getAgentReminderOccurrenceForUser(db, params) {
550
+ const occurrence = getReminderOccurrenceRowById(db, params.occurrenceId);
551
+ if (!occurrence)
552
+ return null;
553
+ const reminder = findReminderRowForAgentUser(db, params.agentId, occurrence.reminderId, params.targetUserId);
554
+ if (!reminder)
555
+ return null;
556
+ return {
557
+ reminder: buildReminderInfoForRow(db, reminder),
558
+ occurrence: mapOccurrenceRow(occurrence),
559
+ };
560
+ }
561
+ export function getAgentReminderCurrentOccurrenceForUser(db, params) {
562
+ const reminder = findReminderRowForAgentUser(db, params.agentId, params.reminderId, params.targetUserId);
563
+ if (!reminder)
564
+ return null;
565
+ return buildReminderCurrentOccurrenceResult(db, reminder);
566
+ }
567
+ export function createAgentReminder(db, params) {
568
+ const normalized = normalizeReminderUpsertInput(params.request);
569
+ const now = params.now ?? Date.now();
570
+ const reminderId = randomUUID();
571
+ const nextRunAt = computeNextRunAt({
572
+ scheduleKind: normalized.scheduleKind,
573
+ startAt: normalized.startAt,
574
+ intervalUnit: normalized.intervalUnit,
575
+ intervalValue: normalized.intervalValue,
576
+ endAt: normalized.endAt,
577
+ maxOccurrences: normalized.maxOccurrences,
578
+ lastScheduledAt: null,
579
+ existingOccurrenceCount: 0,
580
+ now,
581
+ });
582
+ const status = nextRunAt == null ? 'completed' : 'active';
583
+ db.prepare(`INSERT INTO agent_reminders(
584
+ reminder_id, agent_id, target_user_id, title, prompt_text, schedule_kind,
585
+ interval_unit, interval_value, start_at, end_at, max_occurrences, status,
586
+ timezone, last_scheduled_at, next_run_at, created_by_user_id, created_by_agent_id,
587
+ created_at, updated_at
588
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)`).run(reminderId, params.agentId, params.targetUserId, normalized.title, normalized.promptText, normalized.scheduleKind, normalized.intervalUnit, normalized.intervalValue, normalized.startAt, normalized.endAt, normalized.maxOccurrences, status, AGENT_REMINDER_TIMEZONE, nextRunAt, params.createdByUserId ?? null, params.createdByAgentId ?? null, now, now);
589
+ appendReminderEvent(db, {
590
+ reminderId,
591
+ eventType: 'created',
592
+ actorType: params.createdByAgentId ? 'agent' : 'user',
593
+ actorId: params.createdByAgentId ?? params.createdByUserId ?? null,
594
+ payload: {
595
+ title: normalized.title,
596
+ scheduleKind: normalized.scheduleKind,
597
+ nextRunAt,
598
+ status,
599
+ },
600
+ createdAt: now,
601
+ });
602
+ const row = getReminderRowById(db, reminderId);
603
+ if (!row)
604
+ throw new Error('Failed to create reminder');
605
+ return buildReminderInfoForRow(db, row);
606
+ }
607
+ export function updateAgentReminder(db, params) {
608
+ const existingRow = getReminderRowById(db, params.reminderId);
609
+ if (!existingRow)
610
+ return null;
611
+ if (existingRow.status === 'cancelled' || existingRow.status === 'completed') {
612
+ throw new Error(`Cannot update a ${existingRow.status} reminder`);
613
+ }
614
+ const existing = {
615
+ title: existingRow.title,
616
+ promptText: existingRow.promptText,
617
+ scheduleKind: existingRow.scheduleKind,
618
+ startAt: existingRow.startAt,
619
+ intervalUnit: existingRow.intervalUnit,
620
+ intervalValue: existingRow.intervalValue,
621
+ endAt: existingRow.endAt,
622
+ maxOccurrences: existingRow.maxOccurrences,
623
+ };
624
+ const normalized = normalizeReminderUpsertInput(params.request, existing);
625
+ assertOneTimeScheduleStillMutable(db, existingRow, normalized);
626
+ const now = params.now ?? Date.now();
627
+ const existingOccurrenceCount = countReminderOccurrences(db, params.reminderId);
628
+ const nextRunAt = existingRow.status === 'paused'
629
+ ? existingRow.nextRunAt
630
+ : computeNextRunAt({
631
+ scheduleKind: normalized.scheduleKind,
632
+ startAt: normalized.startAt,
633
+ intervalUnit: normalized.intervalUnit,
634
+ intervalValue: normalized.intervalValue,
635
+ endAt: normalized.endAt,
636
+ maxOccurrences: normalized.maxOccurrences,
637
+ lastScheduledAt: existingRow.lastScheduledAt,
638
+ existingOccurrenceCount,
639
+ now,
640
+ });
641
+ const openOccurrences = countOpenReminderOccurrences(db, params.reminderId);
642
+ const status = nextRunAt == null && openOccurrences === 0
643
+ ? 'completed'
644
+ : existingRow.status;
645
+ db.prepare(`UPDATE agent_reminders
646
+ SET title = ?,
647
+ prompt_text = ?,
648
+ schedule_kind = ?,
649
+ interval_unit = ?,
650
+ interval_value = ?,
651
+ start_at = ?,
652
+ end_at = ?,
653
+ max_occurrences = ?,
654
+ next_run_at = ?,
655
+ status = ?,
656
+ updated_at = ?
657
+ WHERE reminder_id = ?`).run(normalized.title, normalized.promptText, normalized.scheduleKind, normalized.intervalUnit, normalized.intervalValue, normalized.startAt, normalized.endAt, normalized.maxOccurrences, nextRunAt, status, now, params.reminderId);
658
+ appendReminderEvent(db, {
659
+ reminderId: params.reminderId,
660
+ eventType: 'updated',
661
+ actorType: params.actor.actorType,
662
+ actorId: params.actor.actorId ?? null,
663
+ actorName: params.actor.actorName ?? null,
664
+ payload: {
665
+ nextRunAt,
666
+ status,
667
+ },
668
+ createdAt: now,
669
+ });
670
+ const row = getReminderRowById(db, params.reminderId);
671
+ return row ? buildReminderInfoForRow(db, row) : null;
672
+ }
673
+ function transitionReminderStatus(db, reminderId, nextStatus, now, actor) {
674
+ const row = getReminderRowById(db, reminderId);
675
+ if (!row)
676
+ return null;
677
+ if (row.status === 'cancelled' || row.status === 'completed') {
678
+ throw new Error(`Cannot change a ${row.status} reminder`);
679
+ }
680
+ if (hasReminderTriggered(db, row)) {
681
+ if (nextStatus === 'paused' || nextStatus === 'active') {
682
+ throw new Error('One-time reminders cannot pause or resume after their first occurrence');
683
+ }
684
+ if (nextStatus === 'cancelled') {
685
+ throw new Error('Triggered one-time reminders must be abandoned instead of cancelled');
686
+ }
687
+ }
688
+ let updatedStatus = nextStatus;
689
+ let nextRunAt = row.nextRunAt;
690
+ if (nextStatus === 'paused') {
691
+ nextRunAt = null;
692
+ }
693
+ else if (nextStatus === 'active') {
694
+ nextRunAt = computeNextRunAt({
695
+ scheduleKind: row.scheduleKind,
696
+ startAt: row.startAt,
697
+ intervalUnit: row.intervalUnit,
698
+ intervalValue: row.intervalValue,
699
+ endAt: row.endAt,
700
+ maxOccurrences: row.maxOccurrences,
701
+ lastScheduledAt: row.lastScheduledAt,
702
+ existingOccurrenceCount: countReminderOccurrences(db, reminderId),
703
+ now,
704
+ });
705
+ if (nextRunAt == null && countOpenReminderOccurrences(db, reminderId) === 0) {
706
+ updatedStatus = 'completed';
707
+ }
708
+ }
709
+ else if (nextStatus === 'cancelled') {
710
+ nextRunAt = null;
711
+ }
712
+ db.prepare(`UPDATE agent_reminders
713
+ SET status = ?, next_run_at = ?, updated_at = ?
714
+ WHERE reminder_id = ?`).run(updatedStatus, nextRunAt, now, reminderId);
715
+ appendReminderEvent(db, {
716
+ reminderId,
717
+ eventType: updatedStatus === 'paused'
718
+ ? 'paused'
719
+ : updatedStatus === 'active'
720
+ ? 'resumed'
721
+ : 'cancelled',
722
+ actorType: actor.actorType,
723
+ actorId: actor.actorId ?? null,
724
+ actorName: actor.actorName ?? null,
725
+ payload: { nextRunAt, status: updatedStatus },
726
+ createdAt: now,
727
+ });
728
+ const updated = getReminderRowById(db, reminderId);
729
+ return updated ? buildReminderInfoForRow(db, updated) : null;
730
+ }
731
+ export function pauseAgentReminder(db, reminderId, actor) {
732
+ return transitionReminderStatus(db, reminderId, 'paused', Date.now(), actor);
733
+ }
734
+ export function resumeAgentReminder(db, reminderId, actor) {
735
+ return transitionReminderStatus(db, reminderId, 'active', Date.now(), actor);
736
+ }
737
+ export function cancelAgentReminder(db, reminderId, actor) {
738
+ return transitionReminderStatus(db, reminderId, 'cancelled', Date.now(), actor);
739
+ }
740
+ function maybeCompleteReminderAfterOccurrence(db, reminderId, now) {
741
+ const row = getReminderRowById(db, reminderId);
742
+ if (!row)
743
+ return;
744
+ if (row.status === 'cancelled' || row.status === 'completed')
745
+ return;
746
+ if (row.scheduleKind !== 'one_time' && row.status !== 'active')
747
+ return;
748
+ if (row.nextRunAt != null)
749
+ return;
750
+ if (countOpenReminderOccurrences(db, reminderId) > 0)
751
+ return;
752
+ db.prepare(`UPDATE agent_reminders
753
+ SET status = 'completed', updated_at = ?
754
+ WHERE reminder_id = ?`).run(now, reminderId);
755
+ }
756
+ export function runAgentReminderNowForUser(db, params) {
757
+ const reminder = findReminderRowForAgentUser(db, params.agentId, params.reminderId, params.targetUserId);
758
+ if (!reminder)
759
+ return null;
760
+ if (reminder.status === 'cancelled' || reminder.status === 'completed') {
761
+ throw new Error(`Cannot run a ${reminder.status} reminder`);
762
+ }
763
+ const currentOpenOccurrence = getCurrentReminderOccurrenceRowByReminderId(db, reminder.reminderId);
764
+ const retryableFailedOccurrence = reminder.scheduleKind === 'one_time' && isRetryableFailedOneTimeOccurrence(currentOpenOccurrence)
765
+ ? currentOpenOccurrence
766
+ : null;
767
+ if (reminder.scheduleKind === 'one_time' && hasReminderTriggered(db, reminder) && !retryableFailedOccurrence) {
768
+ throw new Error('One-time reminders cannot run again after their first occurrence');
769
+ }
770
+ if (countOpenReminderOccurrences(db, reminder.reminderId) > 0 && !retryableFailedOccurrence) {
771
+ throw new Error('Reminder already has an open occurrence');
772
+ }
773
+ const now = params.now ?? Date.now();
774
+ if (retryableFailedOccurrence) {
775
+ const scheduledFor = allocateOccurrenceScheduledFor(db, reminder.reminderId, now, retryableFailedOccurrence.occurrenceId);
776
+ db.transaction(() => {
777
+ db.prepare(`UPDATE agent_reminder_occurrences
778
+ SET status = 'pending',
779
+ scheduled_for = ?,
780
+ started_at = NULL,
781
+ completed_at = NULL,
782
+ error = NULL,
783
+ updated_at = ?
784
+ WHERE occurrence_id = ?`).run(scheduledFor, now, retryableFailedOccurrence.occurrenceId);
785
+ db.prepare(`UPDATE agent_reminders
786
+ SET status = 'in_progress',
787
+ updated_at = ?
788
+ WHERE reminder_id = ?`).run(now, reminder.reminderId);
789
+ appendReminderEvent(db, {
790
+ reminderId: reminder.reminderId,
791
+ occurrenceId: retryableFailedOccurrence.occurrenceId,
792
+ eventType: 'scheduled',
793
+ actorType: params.actor.actorType,
794
+ actorId: params.actor.actorId ?? null,
795
+ actorName: params.actor.actorName ?? null,
796
+ payload: {
797
+ scheduledFor,
798
+ kind: 'run_now_retry',
799
+ },
800
+ createdAt: now,
801
+ });
802
+ })();
803
+ const updatedReminder = getReminderRowById(db, reminder.reminderId);
804
+ const updatedOccurrence = getReminderOccurrenceRowById(db, retryableFailedOccurrence.occurrenceId);
805
+ if (!updatedReminder || !updatedOccurrence) {
806
+ throw new Error('Failed to retry reminder occurrence');
807
+ }
808
+ return {
809
+ reminder: buildReminderInfoForRow(db, updatedReminder),
810
+ occurrence: mapOccurrenceRow(updatedOccurrence),
811
+ };
812
+ }
813
+ const occurrenceId = randomUUID();
814
+ const scheduledFor = allocateOccurrenceScheduledFor(db, reminder.reminderId, now);
815
+ const consumesOneTimeSlot = reminder.scheduleKind === 'one_time'
816
+ && reminder.lastScheduledAt == null
817
+ && countReminderOccurrences(db, reminder.reminderId) === 0;
818
+ db.transaction(() => {
819
+ db.prepare(`INSERT INTO agent_reminder_occurrences(
820
+ occurrence_id, reminder_id, scheduled_for, trigger_kind, status, created_at, updated_at
821
+ ) VALUES(?, ?, ?, ?, 'pending', ?, ?)`).run(occurrenceId, reminder.reminderId, scheduledFor, consumesOneTimeSlot ? 'scheduled' : 'manual_run_now', now, now);
822
+ db.prepare(`UPDATE agent_reminders
823
+ SET status = CASE WHEN schedule_kind = 'one_time' THEN 'in_progress' ELSE status END,
824
+ last_scheduled_at = CASE WHEN ? THEN ? ELSE last_scheduled_at END,
825
+ next_run_at = CASE WHEN ? THEN NULL ELSE next_run_at END,
826
+ updated_at = ?
827
+ WHERE reminder_id = ?`).run(consumesOneTimeSlot ? 1 : 0, scheduledFor, consumesOneTimeSlot ? 1 : 0, now, reminder.reminderId);
828
+ appendReminderEvent(db, {
829
+ reminderId: reminder.reminderId,
830
+ occurrenceId,
831
+ eventType: 'scheduled',
832
+ actorType: params.actor.actorType,
833
+ actorId: params.actor.actorId ?? null,
834
+ actorName: params.actor.actorName ?? null,
835
+ payload: {
836
+ scheduledFor,
837
+ kind: 'run_now',
838
+ },
839
+ createdAt: now,
840
+ });
841
+ })();
842
+ const updatedReminder = getReminderRowById(db, reminder.reminderId);
843
+ const updatedOccurrence = getReminderOccurrenceRowById(db, occurrenceId);
844
+ if (!updatedReminder || !updatedOccurrence) {
845
+ throw new Error('Failed to create reminder occurrence');
846
+ }
847
+ return {
848
+ reminder: buildReminderInfoForRow(db, updatedReminder),
849
+ occurrence: mapOccurrenceRow(updatedOccurrence),
850
+ };
851
+ }
852
+ export function skipAgentReminderOccurrenceForUser(db, params) {
853
+ const scoped = getAgentReminderOccurrenceForUser(db, {
854
+ agentId: params.agentId,
855
+ occurrenceId: params.occurrenceId,
856
+ targetUserId: params.targetUserId,
857
+ });
858
+ if (!scoped)
859
+ return null;
860
+ ensureOneTimeFollowUpActionAllowed(scoped.reminder, 'skip');
861
+ if (!isOpenReminderOccurrenceStatus(scoped.occurrence.status)) {
862
+ throw new Error('Only open reminder occurrences can be skipped');
863
+ }
864
+ if (scoped.occurrence.status === 'dispatched'
865
+ && (scoped.occurrence.conversationId || scoped.occurrence.taskId || scoped.occurrence.threadRootId)) {
866
+ if (!scoped.occurrence.conversationId || !params.cancelActiveRun) {
867
+ throw new Error('Cannot skip a dispatched reminder occurrence without task-thread cancellation support');
868
+ }
869
+ params.cancelActiveRun(scoped.occurrence.conversationId);
870
+ }
871
+ const now = params.now ?? Date.now();
872
+ db.transaction(() => {
873
+ db.prepare(`UPDATE agent_reminder_occurrences
874
+ SET status = 'skipped',
875
+ completed_at = COALESCE(completed_at, ?),
876
+ updated_at = ?
877
+ WHERE occurrence_id = ?`).run(now, now, params.occurrenceId);
878
+ appendReminderEvent(db, {
879
+ reminderId: scoped.reminder.reminderId,
880
+ occurrenceId: params.occurrenceId,
881
+ eventType: 'skipped',
882
+ actorType: params.actor.actorType,
883
+ actorId: params.actor.actorId ?? null,
884
+ actorName: params.actor.actorName ?? null,
885
+ payload: params.reason ? { reason: params.reason } : null,
886
+ createdAt: now,
887
+ });
888
+ maybeCompleteReminderAfterOccurrence(db, scoped.reminder.reminderId, now);
889
+ })();
890
+ const updatedReminder = getReminderRowById(db, scoped.reminder.reminderId);
891
+ const updatedOccurrence = getReminderOccurrenceRowById(db, params.occurrenceId);
892
+ if (!updatedReminder || !updatedOccurrence)
893
+ return null;
894
+ return {
895
+ ok: true,
896
+ reminder: buildReminderInfoForRow(db, updatedReminder),
897
+ occurrence: mapOccurrenceRow(updatedOccurrence),
898
+ task: null,
899
+ };
900
+ }
901
+ export function snoozeAgentReminderOccurrenceForUser(db, params) {
902
+ const scoped = getAgentReminderOccurrenceForUser(db, {
903
+ agentId: params.agentId,
904
+ occurrenceId: params.occurrenceId,
905
+ targetUserId: params.targetUserId,
906
+ });
907
+ if (!scoped)
908
+ return null;
909
+ ensureOneTimeFollowUpActionAllowed(scoped.reminder, 'snooze');
910
+ if (!isOpenReminderOccurrenceStatus(scoped.occurrence.status)) {
911
+ throw new Error('Only open reminder occurrences can be snoozed');
912
+ }
913
+ if (scoped.occurrence.status === 'dispatched'
914
+ && (scoped.occurrence.conversationId || scoped.occurrence.taskId || scoped.occurrence.threadRootId)) {
915
+ if (!scoped.occurrence.conversationId || !params.cancelActiveRun) {
916
+ throw new Error('Cannot snooze a dispatched reminder occurrence without task-thread cancellation support');
917
+ }
918
+ params.cancelActiveRun(scoped.occurrence.conversationId);
919
+ }
920
+ const now = params.now ?? Date.now();
921
+ const until = normalizeNullableTimestamp(params.until);
922
+ if (!until || until <= now) {
923
+ throw new Error('until must be a future timestamp');
924
+ }
925
+ const scheduledFor = allocateOccurrenceScheduledFor(db, scoped.reminder.reminderId, until, params.occurrenceId);
926
+ db.transaction(() => {
927
+ db.prepare(`UPDATE agent_reminder_occurrences
928
+ SET status = 'snoozed',
929
+ scheduled_for = ?,
930
+ error = NULL,
931
+ updated_at = ?
932
+ WHERE occurrence_id = ?`).run(scheduledFor, now, params.occurrenceId);
933
+ appendReminderEvent(db, {
934
+ reminderId: scoped.reminder.reminderId,
935
+ occurrenceId: params.occurrenceId,
936
+ eventType: 'snoozed',
937
+ actorType: params.actor.actorType,
938
+ actorId: params.actor.actorId ?? null,
939
+ actorName: params.actor.actorName ?? null,
940
+ payload: {
941
+ until: scheduledFor,
942
+ },
943
+ createdAt: now,
944
+ });
945
+ db.prepare(`UPDATE agent_reminders
946
+ SET updated_at = ?
947
+ WHERE reminder_id = ?`).run(now, scoped.reminder.reminderId);
948
+ })();
949
+ const updatedReminder = getReminderRowById(db, scoped.reminder.reminderId);
950
+ const updatedOccurrence = getReminderOccurrenceRowById(db, params.occurrenceId);
951
+ if (!updatedReminder || !updatedOccurrence)
952
+ return null;
953
+ return {
954
+ ok: true,
955
+ reminder: buildReminderInfoForRow(db, updatedReminder),
956
+ occurrence: mapOccurrenceRow(updatedOccurrence),
957
+ task: null,
958
+ };
959
+ }
960
+ export function completeAgentReminderOccurrence(db, params) {
961
+ const occurrence = getReminderOccurrenceRowById(db, params.occurrenceId);
962
+ if (!occurrence)
963
+ return null;
964
+ const reminder = getReminderRowById(db, occurrence.reminderId);
965
+ if (!reminder)
966
+ return null;
967
+ ensureOneTimeOccurrenceSupportsReview(reminder);
968
+ if (occurrence.status !== 'completed' && !isOpenReminderOccurrenceStatus(occurrence.status)) {
969
+ throw new Error('Only open reminder occurrences can be completed');
970
+ }
971
+ const now = params.now ?? Date.now();
972
+ let taskResult = null;
973
+ if (occurrence.status !== 'completed' && occurrence.taskId && params.ensureTaskDone) {
974
+ taskResult = params.ensureTaskDone(occurrence.taskId);
975
+ }
976
+ else if (occurrence.taskId && occurrence.taskNumber != null) {
977
+ taskResult = {
978
+ taskId: occurrence.taskId,
979
+ status: 'done',
980
+ taskNumber: occurrence.taskNumber,
981
+ };
982
+ }
983
+ if (occurrence.status !== 'completed') {
984
+ db.transaction(() => {
985
+ db.prepare(`UPDATE agent_reminder_occurrences
986
+ SET status = 'completed',
987
+ completed_at = COALESCE(completed_at, ?),
988
+ updated_at = ?
989
+ WHERE occurrence_id = ?`).run(now, now, params.occurrenceId);
990
+ appendReminderEvent(db, {
991
+ reminderId: occurrence.reminderId,
992
+ occurrenceId: occurrence.occurrenceId,
993
+ eventType: 'completed',
994
+ actorType: params.actor.actorType,
995
+ actorId: params.actor.actorId ?? null,
996
+ actorName: params.actor.actorName ?? null,
997
+ payload: taskResult ? { taskId: taskResult.taskId, taskNumber: taskResult.taskNumber } : null,
998
+ createdAt: now,
999
+ });
1000
+ if (taskResult) {
1001
+ appendReminderEvent(db, {
1002
+ reminderId: occurrence.reminderId,
1003
+ occurrenceId: occurrence.occurrenceId,
1004
+ eventType: 'completed_task_marked_done',
1005
+ actorType: params.actor.actorType,
1006
+ actorId: params.actor.actorId ?? null,
1007
+ actorName: params.actor.actorName ?? null,
1008
+ payload: {
1009
+ taskId: taskResult.taskId,
1010
+ taskNumber: taskResult.taskNumber,
1011
+ status: taskResult.status,
1012
+ },
1013
+ createdAt: now,
1014
+ });
1015
+ }
1016
+ maybeCompleteReminderAfterOccurrence(db, occurrence.reminderId, now);
1017
+ })();
1018
+ }
1019
+ const updatedReminder = getReminderRowById(db, occurrence.reminderId);
1020
+ const updatedOccurrence = getReminderOccurrenceRowById(db, params.occurrenceId);
1021
+ if (!updatedReminder || !updatedOccurrence)
1022
+ return null;
1023
+ return {
1024
+ ok: true,
1025
+ reminder: buildReminderInfoForRow(db, updatedReminder),
1026
+ occurrence: mapOccurrenceRow(updatedOccurrence),
1027
+ task: taskResult,
1028
+ };
1029
+ }
1030
+ export function submitAgentReminderOccurrenceForReview(db, params) {
1031
+ const occurrence = getReminderOccurrenceRowById(db, params.occurrenceId);
1032
+ if (!occurrence)
1033
+ return null;
1034
+ const reminder = getReminderRowById(db, occurrence.reminderId);
1035
+ if (!reminder)
1036
+ return null;
1037
+ if (reminder.scheduleKind !== 'one_time') {
1038
+ throw new Error('bigbang reminder submit-for-review is only valid for one-time reminders');
1039
+ }
1040
+ const allowedStatuses = params.allowFailedLinkedOccurrence
1041
+ ? ['dispatched', 'failed']
1042
+ : ['dispatched'];
1043
+ if (!allowedStatuses.includes(occurrence.status)) {
1044
+ throw new Error('Only dispatched one-time reminder occurrences can be submitted for review');
1045
+ }
1046
+ if (!occurrence.taskId) {
1047
+ throw new Error('One-time reminder occurrences need a linked task before they can be submitted for review');
1048
+ }
1049
+ const now = params.now ?? Date.now();
1050
+ let taskResult = null;
1051
+ if (params.ensureTaskInReview) {
1052
+ taskResult = params.ensureTaskInReview(occurrence.taskId);
1053
+ }
1054
+ else if (occurrence.taskNumber != null && occurrence.taskStatus === 'in_review') {
1055
+ taskResult = {
1056
+ taskId: occurrence.taskId,
1057
+ status: 'in_review',
1058
+ taskNumber: occurrence.taskNumber,
1059
+ };
1060
+ }
1061
+ if (!taskResult) {
1062
+ throw new Error('One-time reminder occurrences must move their linked task into in_review before submission');
1063
+ }
1064
+ db.transaction(() => {
1065
+ db.prepare(`UPDATE agent_reminder_occurrences
1066
+ SET status = 'awaiting_review',
1067
+ error = NULL,
1068
+ updated_at = ?
1069
+ WHERE occurrence_id = ?`).run(now, params.occurrenceId);
1070
+ db.prepare(`UPDATE agent_reminders
1071
+ SET status = 'awaiting_review',
1072
+ updated_at = ?
1073
+ WHERE reminder_id = ?`).run(now, reminder.reminderId);
1074
+ appendReminderEvent(db, {
1075
+ reminderId: reminder.reminderId,
1076
+ occurrenceId: occurrence.occurrenceId,
1077
+ eventType: 'submitted_for_review',
1078
+ actorType: params.actor.actorType,
1079
+ actorId: params.actor.actorId ?? null,
1080
+ actorName: params.actor.actorName ?? null,
1081
+ payload: taskResult ? { taskId: taskResult.taskId, taskNumber: taskResult.taskNumber } : null,
1082
+ createdAt: now,
1083
+ });
1084
+ if (taskResult) {
1085
+ appendReminderEvent(db, {
1086
+ reminderId: reminder.reminderId,
1087
+ occurrenceId: occurrence.occurrenceId,
1088
+ eventType: 'submitted_task_marked_in_review',
1089
+ actorType: params.actor.actorType,
1090
+ actorId: params.actor.actorId ?? null,
1091
+ actorName: params.actor.actorName ?? null,
1092
+ payload: {
1093
+ taskId: taskResult.taskId,
1094
+ taskNumber: taskResult.taskNumber,
1095
+ status: taskResult.status,
1096
+ },
1097
+ createdAt: now,
1098
+ });
1099
+ }
1100
+ })();
1101
+ const updatedReminder = getReminderRowById(db, reminder.reminderId);
1102
+ const updatedOccurrence = getReminderOccurrenceRowById(db, occurrence.occurrenceId);
1103
+ if (!updatedReminder || !updatedOccurrence)
1104
+ return null;
1105
+ return {
1106
+ ok: true,
1107
+ reminder: buildReminderInfoForRow(db, updatedReminder),
1108
+ occurrence: mapOccurrenceRow(updatedOccurrence),
1109
+ task: taskResult,
1110
+ };
1111
+ }
1112
+ export function completeCurrentReminderOccurrenceForUser(db, params) {
1113
+ const reminder = findReminderRowForAgentUser(db, params.agentId, params.reminderId, params.targetUserId);
1114
+ if (!reminder)
1115
+ return null;
1116
+ const occurrence = db.prepare(`SELECT aro.occurrence_id as occurrenceId,
1117
+ aro.reminder_id as reminderId,
1118
+ aro.scheduled_for as scheduledFor,
1119
+ aro.started_at as startedAt,
1120
+ aro.completed_at as completedAt,
1121
+ aro.status,
1122
+ aro.task_id as taskId,
1123
+ aro.conversation_id as conversationId,
1124
+ aro.thread_root_id as threadRootId,
1125
+ aro.error,
1126
+ aro.created_at as createdAt,
1127
+ aro.updated_at as updatedAt,
1128
+ t.task_number as taskNumber,
1129
+ t.title as taskTitle,
1130
+ t.status as taskStatus
1131
+ FROM agent_reminder_occurrences aro
1132
+ LEFT JOIN tasks t ON t.task_id = aro.task_id
1133
+ WHERE aro.reminder_id = ?
1134
+ AND aro.status IN ('pending', 'dispatched', 'awaiting_review', 'failed', 'snoozed')
1135
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC
1136
+ LIMIT 1`).get(params.reminderId);
1137
+ if (!occurrence)
1138
+ return null;
1139
+ return completeAgentReminderOccurrence(db, {
1140
+ occurrenceId: occurrence.occurrenceId,
1141
+ now: params.now,
1142
+ actor: params.actor,
1143
+ ensureTaskDone: params.ensureTaskDone,
1144
+ });
1145
+ }
1146
+ export function submitCurrentReminderOccurrenceForReviewForUser(db, params) {
1147
+ const reminder = findReminderRowForAgentUser(db, params.agentId, params.reminderId, params.targetUserId);
1148
+ if (!reminder)
1149
+ return null;
1150
+ const occurrence = getCurrentReminderOccurrenceRowByReminderId(db, params.reminderId);
1151
+ if (!occurrence)
1152
+ return null;
1153
+ return submitAgentReminderOccurrenceForReview(db, {
1154
+ occurrenceId: occurrence.occurrenceId,
1155
+ now: params.now,
1156
+ actor: params.actor,
1157
+ ensureTaskInReview: params.ensureTaskInReview,
1158
+ });
1159
+ }
1160
+ export function syncReminderOccurrenceForTaskInReview(db, params) {
1161
+ const row = db.prepare(`SELECT aro.occurrence_id as occurrenceId
1162
+ FROM agent_reminder_occurrences aro
1163
+ INNER JOIN agent_reminders ar
1164
+ ON ar.reminder_id = aro.reminder_id
1165
+ WHERE aro.task_id = ?
1166
+ AND aro.status IN ('dispatched', 'failed')
1167
+ AND ar.schedule_kind = 'one_time'
1168
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC
1169
+ LIMIT 1`).get(params.taskId);
1170
+ if (!row)
1171
+ return null;
1172
+ return submitAgentReminderOccurrenceForReview(db, {
1173
+ occurrenceId: row.occurrenceId,
1174
+ now: params.now,
1175
+ actor: params.actor,
1176
+ allowFailedLinkedOccurrence: true,
1177
+ });
1178
+ }
1179
+ export function autoCompleteReminderOccurrenceForTaskDone(db, params) {
1180
+ const row = db.prepare(`SELECT aro.occurrence_id as occurrenceId
1181
+ FROM agent_reminder_occurrences aro
1182
+ WHERE aro.task_id = ?
1183
+ AND aro.status IN ('pending', 'dispatched', 'awaiting_review', 'failed')
1184
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC
1185
+ LIMIT 1`).get(params.taskId);
1186
+ if (!row)
1187
+ return null;
1188
+ const occurrence = getReminderOccurrenceRowById(db, row.occurrenceId);
1189
+ if (!occurrence)
1190
+ return null;
1191
+ const reminder = getReminderRowById(db, occurrence.reminderId);
1192
+ if (!reminder)
1193
+ return null;
1194
+ const now = params.now ?? Date.now();
1195
+ if (occurrence.status !== 'completed') {
1196
+ db.transaction(() => {
1197
+ db.prepare(`UPDATE agent_reminder_occurrences
1198
+ SET status = 'completed',
1199
+ completed_at = COALESCE(completed_at, ?),
1200
+ updated_at = ?
1201
+ WHERE occurrence_id = ?`).run(now, now, occurrence.occurrenceId);
1202
+ if (reminder.scheduleKind === 'one_time') {
1203
+ db.prepare(`UPDATE agent_reminders
1204
+ SET status = 'completed',
1205
+ updated_at = ?
1206
+ WHERE reminder_id = ?`).run(now, occurrence.reminderId);
1207
+ }
1208
+ appendReminderEvent(db, {
1209
+ reminderId: occurrence.reminderId,
1210
+ occurrenceId: occurrence.occurrenceId,
1211
+ eventType: 'auto_completed_from_task_done',
1212
+ actorType: 'system',
1213
+ actorName: 'system',
1214
+ payload: { taskId: params.taskId },
1215
+ createdAt: now,
1216
+ });
1217
+ maybeCompleteReminderAfterOccurrence(db, occurrence.reminderId, now);
1218
+ })();
1219
+ }
1220
+ const updatedReminder = getReminderRowById(db, occurrence.reminderId);
1221
+ const updatedOccurrence = getReminderOccurrenceRowById(db, occurrence.occurrenceId);
1222
+ if (!updatedReminder || !updatedOccurrence)
1223
+ return null;
1224
+ return {
1225
+ ok: true,
1226
+ reminder: buildReminderInfoForRow(db, updatedReminder),
1227
+ occurrence: mapOccurrenceRow(updatedOccurrence),
1228
+ task: occurrence.taskId && occurrence.taskNumber != null
1229
+ ? { taskId: occurrence.taskId, status: 'done', taskNumber: occurrence.taskNumber }
1230
+ : null,
1231
+ };
1232
+ }
1233
+ export function reopenReminderOccurrenceForTaskInProgress(db, params) {
1234
+ const row = db.prepare(`SELECT aro.occurrence_id as occurrenceId
1235
+ FROM agent_reminder_occurrences aro
1236
+ INNER JOIN agent_reminders ar
1237
+ ON ar.reminder_id = aro.reminder_id
1238
+ WHERE aro.task_id = ?
1239
+ AND aro.status = 'awaiting_review'
1240
+ AND ar.schedule_kind = 'one_time'
1241
+ ORDER BY aro.scheduled_for DESC, aro.created_at DESC
1242
+ LIMIT 1`).get(params.taskId);
1243
+ if (!row)
1244
+ return null;
1245
+ const occurrence = getReminderOccurrenceRowById(db, row.occurrenceId);
1246
+ if (!occurrence)
1247
+ return null;
1248
+ const reminder = getReminderRowById(db, occurrence.reminderId);
1249
+ if (!reminder)
1250
+ return null;
1251
+ const now = params.now ?? Date.now();
1252
+ db.transaction(() => {
1253
+ db.prepare(`UPDATE agent_reminder_occurrences
1254
+ SET status = 'dispatched',
1255
+ completed_at = NULL,
1256
+ updated_at = ?
1257
+ WHERE occurrence_id = ?`).run(now, occurrence.occurrenceId);
1258
+ db.prepare(`UPDATE agent_reminders
1259
+ SET status = 'in_progress',
1260
+ updated_at = ?
1261
+ WHERE reminder_id = ?`).run(now, occurrence.reminderId);
1262
+ appendReminderEvent(db, {
1263
+ reminderId: occurrence.reminderId,
1264
+ occurrenceId: occurrence.occurrenceId,
1265
+ eventType: 'reopened_for_rework',
1266
+ actorType: 'system',
1267
+ actorName: 'system',
1268
+ payload: { taskId: params.taskId },
1269
+ createdAt: now,
1270
+ });
1271
+ })();
1272
+ const updatedReminder = getReminderRowById(db, occurrence.reminderId);
1273
+ const updatedOccurrence = getReminderOccurrenceRowById(db, occurrence.occurrenceId);
1274
+ if (!updatedReminder || !updatedOccurrence)
1275
+ return null;
1276
+ return {
1277
+ ok: true,
1278
+ reminder: buildReminderInfoForRow(db, updatedReminder),
1279
+ occurrence: mapOccurrenceRow(updatedOccurrence),
1280
+ task: occurrence.taskId && occurrence.taskNumber != null
1281
+ ? { taskId: occurrence.taskId, status: 'in_progress', taskNumber: occurrence.taskNumber }
1282
+ : null,
1283
+ };
1284
+ }
1285
+ export function abandonOneTimeReminderForUser(db, params) {
1286
+ const scopedReminder = findReminderRowForAgentUser(db, params.agentId, params.reminderId, params.targetUserId);
1287
+ if (!scopedReminder)
1288
+ return null;
1289
+ if (scopedReminder.scheduleKind !== 'one_time' || !hasReminderTriggered(db, scopedReminder)) {
1290
+ throw new Error('Only triggered one-time reminders can be abandoned');
1291
+ }
1292
+ const occurrence = getCurrentReminderOccurrenceRowByReminderId(db, scopedReminder.reminderId);
1293
+ if (!occurrence) {
1294
+ throw new Error('Triggered one-time reminder has no open occurrence to abandon');
1295
+ }
1296
+ if (occurrence.status === 'dispatched' && occurrence.conversationId && params.cancelActiveRun) {
1297
+ params.cancelActiveRun(occurrence.conversationId);
1298
+ }
1299
+ const now = params.now ?? Date.now();
1300
+ const taskResult = occurrence.taskId && params.closeLinkedTaskThread
1301
+ ? params.closeLinkedTaskThread(occurrence.taskId)
1302
+ : occurrence.taskId && occurrence.taskNumber != null && occurrence.taskStatus != null
1303
+ ? { taskId: occurrence.taskId, status: occurrence.taskStatus, taskNumber: occurrence.taskNumber }
1304
+ : null;
1305
+ db.transaction(() => {
1306
+ db.prepare(`UPDATE agent_reminder_occurrences
1307
+ SET status = 'skipped',
1308
+ completed_at = COALESCE(completed_at, ?),
1309
+ updated_at = ?
1310
+ WHERE occurrence_id = ?`).run(now, now, occurrence.occurrenceId);
1311
+ db.prepare(`UPDATE agent_reminders
1312
+ SET status = 'cancelled',
1313
+ next_run_at = NULL,
1314
+ updated_at = ?
1315
+ WHERE reminder_id = ?`).run(now, scopedReminder.reminderId);
1316
+ appendReminderEvent(db, {
1317
+ reminderId: scopedReminder.reminderId,
1318
+ occurrenceId: occurrence.occurrenceId,
1319
+ eventType: 'abandoned',
1320
+ actorType: params.actor.actorType,
1321
+ actorId: params.actor.actorId ?? null,
1322
+ actorName: params.actor.actorName ?? null,
1323
+ payload: taskResult ? { taskId: taskResult.taskId, taskNumber: taskResult.taskNumber } : null,
1324
+ createdAt: now,
1325
+ });
1326
+ })();
1327
+ const updatedReminder = getReminderRowById(db, scopedReminder.reminderId);
1328
+ const updatedOccurrence = getReminderOccurrenceRowById(db, occurrence.occurrenceId);
1329
+ if (!updatedReminder || !updatedOccurrence)
1330
+ return null;
1331
+ return {
1332
+ ok: true,
1333
+ reminder: buildReminderInfoForRow(db, updatedReminder),
1334
+ occurrence: mapOccurrenceRow(updatedOccurrence),
1335
+ task: taskResult,
1336
+ };
1337
+ }
1338
+ function scheduleDueReminderOccurrence(db, reminderId, now) {
1339
+ const scheduled = db.transaction(() => {
1340
+ const row = getReminderRowById(db, reminderId);
1341
+ if (!row)
1342
+ return null;
1343
+ if (row.status !== 'active' || row.nextRunAt == null || row.nextRunAt > now)
1344
+ return null;
1345
+ const scheduledFor = row.nextRunAt;
1346
+ const occurrenceCount = countReminderOccurrences(db, reminderId);
1347
+ const nextRunAt = computeNextRunAt({
1348
+ scheduleKind: row.scheduleKind,
1349
+ startAt: row.startAt,
1350
+ intervalUnit: row.intervalUnit,
1351
+ intervalValue: row.intervalValue,
1352
+ endAt: row.endAt,
1353
+ maxOccurrences: row.maxOccurrences,
1354
+ lastScheduledAt: scheduledFor,
1355
+ existingOccurrenceCount: occurrenceCount + 1,
1356
+ now,
1357
+ });
1358
+ const currentOpenOccurrence = getCurrentReminderOccurrenceRowByReminderId(db, reminderId);
1359
+ if (currentOpenOccurrence) {
1360
+ const blockedNextRunAt = computeNextRunAt({
1361
+ scheduleKind: row.scheduleKind,
1362
+ startAt: row.startAt,
1363
+ intervalUnit: row.intervalUnit,
1364
+ intervalValue: row.intervalValue,
1365
+ endAt: row.endAt,
1366
+ maxOccurrences: row.maxOccurrences,
1367
+ lastScheduledAt: scheduledFor,
1368
+ existingOccurrenceCount: occurrenceCount,
1369
+ now,
1370
+ });
1371
+ db.prepare(`UPDATE agent_reminders
1372
+ SET last_scheduled_at = ?,
1373
+ next_run_at = ?,
1374
+ status = CASE WHEN schedule_kind = 'one_time' THEN 'in_progress' ELSE status END,
1375
+ updated_at = ?
1376
+ WHERE reminder_id = ?`).run(scheduledFor, blockedNextRunAt, now, reminderId);
1377
+ appendReminderEvent(db, {
1378
+ reminderId,
1379
+ occurrenceId: currentOpenOccurrence.occurrenceId,
1380
+ eventType: 'blocked_by_open_occurrence',
1381
+ actorType: 'system',
1382
+ actorName: 'system',
1383
+ payload: {
1384
+ scheduledFor,
1385
+ nextRunAt: blockedNextRunAt,
1386
+ blockingOccurrenceId: currentOpenOccurrence.occurrenceId,
1387
+ blockingStatus: currentOpenOccurrence.status,
1388
+ },
1389
+ createdAt: now,
1390
+ });
1391
+ return null;
1392
+ }
1393
+ const occurrenceId = randomUUID();
1394
+ db.prepare(`INSERT INTO agent_reminder_occurrences(
1395
+ occurrence_id, reminder_id, scheduled_for, trigger_kind, status, created_at, updated_at
1396
+ ) VALUES(?, ?, ?, 'scheduled', 'pending', ?, ?)`).run(occurrenceId, reminderId, scheduledFor, now, now);
1397
+ db.prepare(`UPDATE agent_reminders
1398
+ SET last_scheduled_at = ?,
1399
+ next_run_at = ?,
1400
+ status = CASE WHEN schedule_kind = 'one_time' THEN 'in_progress' ELSE status END,
1401
+ updated_at = ?
1402
+ WHERE reminder_id = ?`).run(scheduledFor, nextRunAt, now, reminderId);
1403
+ appendReminderEvent(db, {
1404
+ reminderId,
1405
+ occurrenceId,
1406
+ eventType: 'scheduled',
1407
+ actorType: 'system',
1408
+ actorName: 'system',
1409
+ payload: {
1410
+ scheduledFor,
1411
+ nextRunAt,
1412
+ },
1413
+ createdAt: now,
1414
+ });
1415
+ const reminderAfter = getReminderRowById(db, reminderId);
1416
+ const occurrenceAfter = getReminderOccurrenceRowById(db, occurrenceId);
1417
+ if (!reminderAfter || !occurrenceAfter)
1418
+ return null;
1419
+ return {
1420
+ reminder: buildReminderInfoForRow(db, reminderAfter),
1421
+ occurrence: mapOccurrenceRow(occurrenceAfter),
1422
+ };
1423
+ })();
1424
+ return scheduled;
1425
+ }
1426
+ export function markAgentReminderDispatchOutcome(db, params) {
1427
+ db.transaction(() => {
1428
+ const reminder = getReminderRowById(db, params.reminderId);
1429
+ const occurrence = getReminderOccurrenceRowById(db, params.occurrenceId);
1430
+ if (!occurrence)
1431
+ return;
1432
+ const dispatchOutcomeSuperseded = reminder?.status === 'cancelled'
1433
+ || reminder?.status === 'completed'
1434
+ || reminder?.status === 'awaiting_review'
1435
+ || occurrence.status === 'skipped'
1436
+ || occurrence.status === 'completed'
1437
+ || occurrence.status === 'awaiting_review';
1438
+ if (dispatchOutcomeSuperseded) {
1439
+ db.prepare(`UPDATE agent_reminder_occurrences
1440
+ SET started_at = COALESCE(started_at, ?),
1441
+ task_id = COALESCE(task_id, ?),
1442
+ conversation_id = COALESCE(conversation_id, ?),
1443
+ thread_root_id = COALESCE(thread_root_id, ?),
1444
+ updated_at = ?
1445
+ WHERE occurrence_id = ?`).run(params.now, params.result.taskId ?? null, params.result.conversationId ?? null, params.result.threadRootId ?? null, params.now, params.occurrenceId);
1446
+ return;
1447
+ }
1448
+ db.prepare(`UPDATE agent_reminder_occurrences
1449
+ SET status = ?,
1450
+ started_at = COALESCE(started_at, ?),
1451
+ task_id = COALESCE(?, task_id),
1452
+ conversation_id = COALESCE(?, conversation_id),
1453
+ thread_root_id = COALESCE(?, thread_root_id),
1454
+ error = ?,
1455
+ updated_at = ?
1456
+ WHERE occurrence_id = ?`).run(params.result.status, params.now, params.result.taskId ?? null, params.result.conversationId ?? null, params.result.threadRootId ?? null, params.result.error ?? null, params.now, params.occurrenceId);
1457
+ if (reminder?.scheduleKind === 'one_time') {
1458
+ const reminderStatus = params.result.status === 'failed'
1459
+ ? 'needs_attention'
1460
+ : 'in_progress';
1461
+ db.prepare(`UPDATE agent_reminders
1462
+ SET status = ?,
1463
+ updated_at = ?
1464
+ WHERE reminder_id = ?`).run(reminderStatus, params.now, params.reminderId);
1465
+ }
1466
+ appendReminderEvent(db, {
1467
+ reminderId: params.reminderId,
1468
+ occurrenceId: params.occurrenceId,
1469
+ eventType: params.result.status === 'dispatched'
1470
+ ? 'dispatched'
1471
+ : params.result.status === 'failed'
1472
+ ? 'failed'
1473
+ : 'skipped',
1474
+ actorType: 'system',
1475
+ actorName: 'system',
1476
+ payload: {
1477
+ taskId: params.result.taskId ?? null,
1478
+ conversationId: params.result.conversationId ?? null,
1479
+ threadRootId: params.result.threadRootId ?? null,
1480
+ error: params.result.error ?? null,
1481
+ },
1482
+ createdAt: params.now,
1483
+ });
1484
+ })();
1485
+ }
1486
+ export function markReminderOccurrenceFailedForConversation(db, params) {
1487
+ const row = db.prepare(`SELECT aro.occurrence_id as occurrenceId
1488
+ FROM agent_reminder_occurrences aro
1489
+ INNER JOIN agent_reminders ar
1490
+ ON ar.reminder_id = aro.reminder_id
1491
+ WHERE aro.conversation_id = ?
1492
+ AND aro.status = 'dispatched'
1493
+ ORDER BY CASE ar.schedule_kind
1494
+ WHEN 'one_time' THEN 0
1495
+ ELSE 1
1496
+ END,
1497
+ aro.scheduled_for DESC,
1498
+ aro.created_at DESC
1499
+ LIMIT 1`).get(params.conversationId);
1500
+ if (!row)
1501
+ return null;
1502
+ const occurrence = getReminderOccurrenceRowById(db, row.occurrenceId);
1503
+ if (!occurrence || occurrence.status !== 'dispatched')
1504
+ return null;
1505
+ const reminder = getReminderRowById(db, occurrence.reminderId);
1506
+ if (!reminder)
1507
+ return null;
1508
+ const now = params.now ?? Date.now();
1509
+ db.transaction(() => {
1510
+ db.prepare(`UPDATE agent_reminder_occurrences
1511
+ SET status = 'failed',
1512
+ error = ?,
1513
+ updated_at = ?
1514
+ WHERE occurrence_id = ?
1515
+ AND status = 'dispatched'`).run(params.error, now, occurrence.occurrenceId);
1516
+ if (reminder.scheduleKind === 'one_time') {
1517
+ db.prepare(`UPDATE agent_reminders
1518
+ SET status = 'needs_attention',
1519
+ updated_at = ?
1520
+ WHERE reminder_id = ?
1521
+ AND status NOT IN ('cancelled', 'completed', 'awaiting_review')`).run(now, reminder.reminderId);
1522
+ }
1523
+ appendReminderEvent(db, {
1524
+ reminderId: reminder.reminderId,
1525
+ occurrenceId: occurrence.occurrenceId,
1526
+ eventType: 'failed',
1527
+ actorType: 'system',
1528
+ actorName: 'system',
1529
+ payload: {
1530
+ conversationId: params.conversationId,
1531
+ error: params.error,
1532
+ source: 'conversation_runtime',
1533
+ },
1534
+ createdAt: now,
1535
+ });
1536
+ })();
1537
+ const updatedReminder = getReminderRowById(db, reminder.reminderId);
1538
+ const updatedOccurrence = getReminderOccurrenceRowById(db, occurrence.occurrenceId);
1539
+ if (!updatedReminder || !updatedOccurrence)
1540
+ return null;
1541
+ return {
1542
+ ok: true,
1543
+ reminder: buildReminderInfoForRow(db, updatedReminder),
1544
+ occurrence: mapOccurrenceRow(updatedOccurrence),
1545
+ task: updatedOccurrence.taskId && updatedOccurrence.taskNumber != null && updatedOccurrence.taskStatus != null
1546
+ ? {
1547
+ taskId: updatedOccurrence.taskId,
1548
+ status: updatedOccurrence.taskStatus,
1549
+ taskNumber: updatedOccurrence.taskNumber,
1550
+ }
1551
+ : null,
1552
+ };
1553
+ }
1554
+ function listDueSnoozedOccurrences(db, now, limit) {
1555
+ const rows = db.prepare(`SELECT aro.occurrence_id as occurrenceId,
1556
+ aro.reminder_id as reminderId
1557
+ FROM agent_reminder_occurrences aro
1558
+ INNER JOIN agent_reminders ar
1559
+ ON ar.reminder_id = aro.reminder_id
1560
+ WHERE aro.status = 'snoozed'
1561
+ AND aro.scheduled_for <= ?
1562
+ AND ar.status = 'active'
1563
+ ORDER BY aro.scheduled_for ASC, aro.created_at ASC
1564
+ LIMIT ?`).all(now, limit);
1565
+ const due = [];
1566
+ for (const row of rows) {
1567
+ const reminder = getReminderRowById(db, row.reminderId);
1568
+ const occurrence = getReminderOccurrenceRowById(db, row.occurrenceId);
1569
+ if (!reminder || !occurrence)
1570
+ continue;
1571
+ if (occurrence.status !== 'snoozed' || occurrence.scheduledFor > now)
1572
+ continue;
1573
+ due.push({
1574
+ reminder: buildReminderInfoForRow(db, reminder),
1575
+ occurrence: mapOccurrenceRow(occurrence),
1576
+ });
1577
+ }
1578
+ return due;
1579
+ }
1580
+ export async function processDueAgentReminders(params) {
1581
+ void params.conversationManager;
1582
+ const now = params.now ?? Date.now();
1583
+ const limit = Math.max(1, params.limit ?? 25);
1584
+ const dueSnoozedOccurrences = listDueSnoozedOccurrences(params.db, now, limit);
1585
+ for (const due of dueSnoozedOccurrences) {
1586
+ let result;
1587
+ try {
1588
+ result = await params.onDispatchOccurrence(due);
1589
+ }
1590
+ catch (error) {
1591
+ result = {
1592
+ ok: false,
1593
+ status: 'failed',
1594
+ error: String(error?.message ?? error),
1595
+ };
1596
+ }
1597
+ markAgentReminderDispatchOutcome(params.db, {
1598
+ occurrenceId: due.occurrence.occurrenceId,
1599
+ reminderId: due.reminder.reminderId,
1600
+ result,
1601
+ now,
1602
+ });
1603
+ }
1604
+ const dueRows = params.db.prepare(`SELECT reminder_id as reminderId
1605
+ FROM agent_reminders
1606
+ WHERE status = 'active'
1607
+ AND next_run_at IS NOT NULL
1608
+ AND next_run_at <= ?
1609
+ ORDER BY next_run_at ASC
1610
+ LIMIT ?`).all(now, limit);
1611
+ for (const row of dueRows) {
1612
+ const scheduled = scheduleDueReminderOccurrence(params.db, row.reminderId, now);
1613
+ if (!scheduled)
1614
+ continue;
1615
+ let result;
1616
+ try {
1617
+ result = await params.onDispatchOccurrence(scheduled);
1618
+ }
1619
+ catch (error) {
1620
+ result = {
1621
+ ok: false,
1622
+ status: 'failed',
1623
+ error: String(error?.message ?? error),
1624
+ };
1625
+ }
1626
+ markAgentReminderDispatchOutcome(params.db, {
1627
+ occurrenceId: scheduled.occurrence.occurrenceId,
1628
+ reminderId: scheduled.reminder.reminderId,
1629
+ result,
1630
+ now,
1631
+ });
1632
+ }
1633
+ }
1634
+ export function startAgentReminderService(params) {
1635
+ const intervalMs = Math.max(1_000, params.intervalMs ?? AGENT_REMINDER_POLL_INTERVAL_MS);
1636
+ let closed = false;
1637
+ let running = false;
1638
+ let timer = null;
1639
+ const tick = async () => {
1640
+ if (closed || running)
1641
+ return;
1642
+ running = true;
1643
+ try {
1644
+ await processDueAgentReminders({
1645
+ db: params.db,
1646
+ conversationManager: params.conversationManager,
1647
+ onDispatchOccurrence: params.onDispatchOccurrence,
1648
+ });
1649
+ }
1650
+ finally {
1651
+ running = false;
1652
+ }
1653
+ };
1654
+ timer = setInterval(() => {
1655
+ void tick();
1656
+ }, intervalMs);
1657
+ timer.unref?.();
1658
+ return {
1659
+ tick,
1660
+ stop: () => {
1661
+ closed = true;
1662
+ if (!timer)
1663
+ return;
1664
+ clearInterval(timer);
1665
+ timer = null;
1666
+ },
1667
+ };
1668
+ }