@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,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
|
+
}
|