agent-relay-server 0.33.0 → 0.34.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/package.json +1 -1
- package/runner/src/adapter.ts +21 -4
- package/src/channel-target.ts +24 -0
- package/src/db/activity.ts +194 -0
- package/src/db/agent-search.ts +174 -0
- package/src/db/agents.ts +551 -0
- package/src/db/artifacts.ts +342 -0
- package/src/db/channels.ts +576 -0
- package/src/db/connection.ts +71 -0
- package/src/db/delivery.ts +395 -0
- package/src/db/inbox.ts +249 -0
- package/src/db/index.ts +23 -0
- package/src/db/integrations.ts +339 -0
- package/src/db/mappers.ts +397 -0
- package/src/db/merge-lease.ts +160 -0
- package/src/db/message-reads.ts +304 -0
- package/src/db/messages.ts +434 -0
- package/src/db/migrations.ts +431 -0
- package/src/db/orchestrators.ts +358 -0
- package/src/db/pairs.ts +324 -0
- package/src/db/schema.ts +758 -0
- package/src/db/stats.ts +337 -0
- package/src/db/tasks.ts +407 -0
- package/src/db/workspaces.ts +440 -0
- package/src/db.ts +4 -5721
- package/src/routes/integrations.ts +6 -8
package/src/db/tasks.ts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isRecord, stringValue, isMechanicalMessageKind } from "agent-relay-sdk";
|
|
4
|
+
import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "../config.ts";
|
|
5
|
+
import { parseJson } from "../utils";
|
|
6
|
+
import { isLiveIsolatedWorkspace } from "../workspace-phase";
|
|
7
|
+
import {
|
|
8
|
+
CONTRACT_REQUIREMENTS,
|
|
9
|
+
contractCompatibility,
|
|
10
|
+
parseRuntimeCapabilities,
|
|
11
|
+
parseRuntimeContracts,
|
|
12
|
+
parseRuntimePackage,
|
|
13
|
+
type RuntimeContracts,
|
|
14
|
+
} from "../contracts";
|
|
15
|
+
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
|
|
16
|
+
import { matchAgents } from "../agent-ref";
|
|
17
|
+
import { getAgent, settleSingleTargetOnDemandTasks, validateAgentSession } from "./agents.ts";
|
|
18
|
+
import { ClaimError, getDb } from "./connection.ts";
|
|
19
|
+
import { rowToTask, rowToTaskEvent } from "./mappers.ts";
|
|
20
|
+
import { claimMessageRow } from "./messages.ts";
|
|
21
|
+
import type {
|
|
22
|
+
AgentCard,
|
|
23
|
+
ActivityEvent,
|
|
24
|
+
ActivityEventInput,
|
|
25
|
+
AgentKind,
|
|
26
|
+
AgentSessionGuard,
|
|
27
|
+
Artifact,
|
|
28
|
+
ArtifactBlob,
|
|
29
|
+
ArtifactKind,
|
|
30
|
+
ArtifactLink,
|
|
31
|
+
ArtifactSensitivity,
|
|
32
|
+
ArtifactVisibility,
|
|
33
|
+
AttachmentRef,
|
|
34
|
+
ChannelBinding,
|
|
35
|
+
ChannelBindingMode,
|
|
36
|
+
ChannelRouteTarget,
|
|
37
|
+
ChatHistoryImport,
|
|
38
|
+
ChatHistoryImportEntry,
|
|
39
|
+
ChannelSummary,
|
|
40
|
+
ChannelTargetHealth,
|
|
41
|
+
CreatePairInput,
|
|
42
|
+
HealthCheck,
|
|
43
|
+
HealthReport,
|
|
44
|
+
ManagedAgent,
|
|
45
|
+
ManagedSessionExitDiagnostics,
|
|
46
|
+
Message,
|
|
47
|
+
MessageDeliveryAttempt,
|
|
48
|
+
MessageDeliveryStatus,
|
|
49
|
+
Orchestrator,
|
|
50
|
+
OrchestratorHealth,
|
|
51
|
+
OrchestratorRuntimeInput,
|
|
52
|
+
OrchestratorStatus,
|
|
53
|
+
OrchestratorUpgradeState,
|
|
54
|
+
PairActionInput,
|
|
55
|
+
PairMessageInput,
|
|
56
|
+
PairSession,
|
|
57
|
+
PairStatus,
|
|
58
|
+
RegisterAgentInput,
|
|
59
|
+
ReplyObligation,
|
|
60
|
+
RegisterOrchestratorInput,
|
|
61
|
+
SendMessageInput,
|
|
62
|
+
PollQuery,
|
|
63
|
+
SpawnApprovalMode,
|
|
64
|
+
SpawnProvider,
|
|
65
|
+
Task,
|
|
66
|
+
TaskEvent,
|
|
67
|
+
TaskSeverity,
|
|
68
|
+
TaskStatus,
|
|
69
|
+
IntegrationEventInput,
|
|
70
|
+
IntegrationSummary,
|
|
71
|
+
IntegrationTaskStats,
|
|
72
|
+
InboxDraft,
|
|
73
|
+
InboxState,
|
|
74
|
+
InboxThreadState,
|
|
75
|
+
ContextSnapshot,
|
|
76
|
+
ContextState,
|
|
77
|
+
ProviderCapabilities,
|
|
78
|
+
TaskStatusInput,
|
|
79
|
+
WorkspaceMetadata,
|
|
80
|
+
WorkspaceRecord,
|
|
81
|
+
WorkspaceStatus,
|
|
82
|
+
} from "../types";
|
|
83
|
+
|
|
84
|
+
export const TASK_SELECT = "SELECT * FROM tasks";
|
|
85
|
+
|
|
86
|
+
export function findOpenTaskByDedupe(source: string, dedupeKey: string): Task | null {
|
|
87
|
+
const row = getDb()
|
|
88
|
+
.query(`${TASK_SELECT} WHERE source = ? AND dedupe_key = ? AND status NOT IN ('done', 'failed', 'canceled') ORDER BY id DESC LIMIT 1`)
|
|
89
|
+
.get(source, dedupeKey) as any;
|
|
90
|
+
return row ? rowToTask(row) : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" | "taskId" | "createdAt">>, now: number): TaskEvent {
|
|
94
|
+
const result = getDb().query(`
|
|
95
|
+
INSERT INTO task_events (task_id, source, type, severity, title, body, metadata, created_at)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
97
|
+
`).run(taskId, event.source, event.type, event.severity, event.title, event.body, JSON.stringify(event.metadata), now);
|
|
98
|
+
return getTaskEvent(Number(result.lastInsertRowid))!;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getTaskEvent(id: number): TaskEvent | null {
|
|
102
|
+
const row = getDb().query("SELECT * FROM task_events WHERE id = ?").get(id) as any;
|
|
103
|
+
return row ? rowToTaskEvent(row) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function taskMessageBody(task: Pick<Task, "id" | "source" | "severity" | "body" | "externalUrl" | "dedupeKey">): string {
|
|
107
|
+
const lines = [
|
|
108
|
+
`[task:${task.id}] ${task.severity.toUpperCase()} from ${task.source}`,
|
|
109
|
+
"",
|
|
110
|
+
task.body,
|
|
111
|
+
];
|
|
112
|
+
if (task.externalUrl) lines.push("", `External: ${task.externalUrl}`);
|
|
113
|
+
if (task.dedupeKey) lines.push(`Dedupe: ${task.dedupeKey}`);
|
|
114
|
+
lines.push("", "Claim this task before working it, then update task status when finished.");
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
export function getTask(id: number): Task | null {
|
|
120
|
+
const row = getDb().query(`${TASK_SELECT} WHERE id = ?`).get(id) as any;
|
|
121
|
+
return row ? rowToTask(row) : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function listTasks(filter?: { status?: string; source?: string; target?: string; limit?: number }): Task[] {
|
|
125
|
+
const conditions: string[] = [];
|
|
126
|
+
const params: any[] = [];
|
|
127
|
+
if (filter?.status) {
|
|
128
|
+
conditions.push("status = ?");
|
|
129
|
+
params.push(filter.status);
|
|
130
|
+
}
|
|
131
|
+
if (filter?.source) {
|
|
132
|
+
conditions.push("source = ?");
|
|
133
|
+
params.push(filter.source);
|
|
134
|
+
}
|
|
135
|
+
if (filter?.target) {
|
|
136
|
+
conditions.push("target = ?");
|
|
137
|
+
params.push(filter.target);
|
|
138
|
+
}
|
|
139
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
140
|
+
params.push(filter?.limit ?? 100);
|
|
141
|
+
return (getDb().query(`${TASK_SELECT} ${where} ORDER BY updated_at DESC LIMIT ?`).all(...params) as any[]).map(rowToTask);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
export function listTaskEvents(taskId: number): TaskEvent[] {
|
|
146
|
+
return (getDb().query("SELECT * FROM task_events WHERE task_id = ? ORDER BY id ASC").all(taskId) as any[]).map(rowToTaskEvent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function recordTaskEvent(taskId: number, input: {
|
|
150
|
+
source: string;
|
|
151
|
+
type: string;
|
|
152
|
+
severity?: TaskSeverity;
|
|
153
|
+
title: string;
|
|
154
|
+
body?: string;
|
|
155
|
+
metadata?: Record<string, unknown>;
|
|
156
|
+
}, now: number = Date.now()): TaskEvent | null {
|
|
157
|
+
const task = getTask(taskId);
|
|
158
|
+
if (!task) return null;
|
|
159
|
+
return insertTaskEvent(taskId, {
|
|
160
|
+
source: input.source,
|
|
161
|
+
type: input.type,
|
|
162
|
+
severity: input.severity ?? task.severity,
|
|
163
|
+
title: input.title,
|
|
164
|
+
body: input.body ?? "",
|
|
165
|
+
metadata: input.metadata ?? {},
|
|
166
|
+
}, now);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function releaseExpiredClaims(now: number = Date.now()): { messageIds: number[]; tasks: Task[] } {
|
|
170
|
+
return getDb().transaction(() => {
|
|
171
|
+
settleSingleTargetOnDemandTasks("claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?)", [now], now, "claim-lease-expired");
|
|
172
|
+
const releasableMessageClaim = "claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = messages.id AND t.status IN ('done', 'failed', 'canceled'))";
|
|
173
|
+
const messageRows = getDb()
|
|
174
|
+
.query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
|
|
175
|
+
.all(now) as any[];
|
|
176
|
+
const taskRows = getDb()
|
|
177
|
+
.query(`${TASK_SELECT} WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
|
|
178
|
+
.all(now) as any[];
|
|
179
|
+
|
|
180
|
+
if (messageRows.length > 0) {
|
|
181
|
+
getDb()
|
|
182
|
+
.query(`UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE ${releasableMessageClaim}`)
|
|
183
|
+
.run(now);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (taskRows.length > 0) {
|
|
187
|
+
getDb()
|
|
188
|
+
.query("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')")
|
|
189
|
+
.run(now, now);
|
|
190
|
+
|
|
191
|
+
for (const row of taskRows) {
|
|
192
|
+
insertTaskEvent(row.id, {
|
|
193
|
+
source: "agent-relay",
|
|
194
|
+
type: "claim.expired",
|
|
195
|
+
severity: row.severity,
|
|
196
|
+
title: "Task claim expired",
|
|
197
|
+
body: `Claim by ${row.claimed_by} expired and was released`,
|
|
198
|
+
metadata: { agentId: row.claimed_by },
|
|
199
|
+
}, now);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
messageIds: messageRows.map((row: any) => row.id),
|
|
205
|
+
tasks: taskRows.map((row: any) => getTask(row.id)!),
|
|
206
|
+
};
|
|
207
|
+
})();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function orphanTasksForAgent(agentId: string, now: number = Date.now()): Task[] {
|
|
211
|
+
return getDb().transaction(() => {
|
|
212
|
+
const rows = getDb()
|
|
213
|
+
.query(`${TASK_SELECT} WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')`)
|
|
214
|
+
.all(agentId) as any[];
|
|
215
|
+
if (rows.length === 0) return [];
|
|
216
|
+
|
|
217
|
+
getDb().query(`
|
|
218
|
+
UPDATE tasks
|
|
219
|
+
SET status = 'orphaned', updated_at = ?, last_seen_at = ?
|
|
220
|
+
WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')
|
|
221
|
+
`).run(now, now, agentId);
|
|
222
|
+
|
|
223
|
+
for (const row of rows) {
|
|
224
|
+
insertTaskEvent(row.id, {
|
|
225
|
+
source: "agent-relay",
|
|
226
|
+
type: "task.orphaned",
|
|
227
|
+
severity: row.severity,
|
|
228
|
+
title: "Task orphaned",
|
|
229
|
+
body: `Claimed agent ${agentId} went offline`,
|
|
230
|
+
metadata: { agentId },
|
|
231
|
+
}, now);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return rows.map((row: any) => getTask(row.id)!);
|
|
235
|
+
})();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function releaseOrphanedTasks(graceMs = 120_000, now: number = Date.now()): Task[] {
|
|
239
|
+
return getDb().transaction(() => {
|
|
240
|
+
const cutoff = now - graceMs;
|
|
241
|
+
settleSingleTargetOnDemandTasks("status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?", [cutoff], now, "orphan-grace-elapsed");
|
|
242
|
+
const rows = getDb()
|
|
243
|
+
.query(`${TASK_SELECT} WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
|
|
244
|
+
.all(cutoff) as any[];
|
|
245
|
+
if (rows.length === 0) return [];
|
|
246
|
+
|
|
247
|
+
getDb().query(`
|
|
248
|
+
UPDATE tasks
|
|
249
|
+
SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ?
|
|
250
|
+
WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?
|
|
251
|
+
`).run(now, cutoff);
|
|
252
|
+
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
insertTaskEvent(row.id, {
|
|
255
|
+
source: "agent-relay",
|
|
256
|
+
type: "orphan.released",
|
|
257
|
+
severity: row.severity,
|
|
258
|
+
title: "Orphaned task released",
|
|
259
|
+
body: "Task is available for claim again",
|
|
260
|
+
metadata: { previousAgentId: row.claimed_by },
|
|
261
|
+
}, now);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return rows.map((row: any) => getTask(row.id)!);
|
|
265
|
+
})();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function claimTask(taskId: number, agentId: string, guard?: AgentSessionGuard): { ok: boolean; error?: string; task?: Task } {
|
|
269
|
+
releaseExpiredClaims();
|
|
270
|
+
const session = validateAgentSession(agentId, guard);
|
|
271
|
+
if (!session.ok) return { ok: false, error: session.error };
|
|
272
|
+
const agent = getAgent(agentId);
|
|
273
|
+
if (!agent) return { ok: false, error: "claiming agent not found" };
|
|
274
|
+
if (agent.status === "offline") return { ok: false, error: "claiming agent is offline" };
|
|
275
|
+
const task = getTask(taskId);
|
|
276
|
+
if (!task) return { ok: false, error: "task not found" };
|
|
277
|
+
if (!["open", "blocked"].includes(task.status)) return { ok: false, error: `task is ${task.status}` };
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
return getDb().transaction(() => {
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
const expiresAt = now + CLAIM_LEASE_MS;
|
|
283
|
+
const result = getDb().query(`
|
|
284
|
+
UPDATE tasks SET status = 'claimed', claimed_by = ?, claimed_at = ?, claim_expires_at = ?, updated_at = ?
|
|
285
|
+
WHERE id = ? AND status IN ('open', 'blocked')
|
|
286
|
+
`).run(agentId, now, expiresAt, now, taskId);
|
|
287
|
+
if (result.changes === 0) return { ok: false, error: "claim race — already claimed" };
|
|
288
|
+
if (task.messageId) {
|
|
289
|
+
const messageClaim = claimMessageRow(task.messageId, agentId, now);
|
|
290
|
+
if (!messageClaim.ok) throw new ClaimError(messageClaim.error);
|
|
291
|
+
}
|
|
292
|
+
insertTaskEvent(taskId, {
|
|
293
|
+
source: "agent-relay",
|
|
294
|
+
type: "claimed",
|
|
295
|
+
severity: task.severity,
|
|
296
|
+
title: `Task claimed by ${agentId}`,
|
|
297
|
+
body: `Task claimed by ${agentId}`,
|
|
298
|
+
metadata: { agentId },
|
|
299
|
+
}, now);
|
|
300
|
+
return { ok: true, task: getTask(taskId)! };
|
|
301
|
+
})();
|
|
302
|
+
} catch (e) {
|
|
303
|
+
if (e instanceof ClaimError) return { ok: false, error: e.message };
|
|
304
|
+
throw e;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSessionGuard): { ok: boolean; error?: string; task?: Task } {
|
|
309
|
+
releaseExpiredClaims();
|
|
310
|
+
const session = validateAgentSession(agentId, guard);
|
|
311
|
+
if (!session.ok) return { ok: false, error: session.error };
|
|
312
|
+
const agent = getAgent(agentId);
|
|
313
|
+
if (!agent) return { ok: false, error: "claiming agent not found" };
|
|
314
|
+
if (agent.status === "offline") return { ok: false, error: "claiming agent is offline" };
|
|
315
|
+
const task = getTask(taskId);
|
|
316
|
+
if (!task) return { ok: false, error: "task not found" };
|
|
317
|
+
if (task.claimedBy !== agentId) return { ok: false, error: task.claimedBy ? `claimed by ${task.claimedBy}` : "task is not claimed" };
|
|
318
|
+
if (!["claimed", "in_progress", "blocked"].includes(task.status)) return { ok: false, error: `task is ${task.status}` };
|
|
319
|
+
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
const expiresAt = now + CLAIM_LEASE_MS;
|
|
322
|
+
getDb().query("UPDATE tasks SET claim_expires_at = ?, updated_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, now, taskId, agentId);
|
|
323
|
+
if (task.messageId) {
|
|
324
|
+
getDb().query("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, task.messageId, agentId);
|
|
325
|
+
}
|
|
326
|
+
return { ok: true, task: getTask(taskId)! };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok: boolean; error?: string; task?: Task; event?: TaskEvent } {
|
|
330
|
+
const task = getTask(taskId);
|
|
331
|
+
if (!task) return { ok: false, error: "task not found" };
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
const agentId = input.agentId ?? task.claimedBy ?? null;
|
|
334
|
+
if (agentId) {
|
|
335
|
+
const session = validateAgentSession(agentId, input);
|
|
336
|
+
if (!session.ok) return { ok: false, error: session.error };
|
|
337
|
+
}
|
|
338
|
+
const result = getDb().query(`
|
|
339
|
+
UPDATE tasks
|
|
340
|
+
SET status = ?, result = COALESCE(?, result), claimed_by = COALESCE(?, claimed_by),
|
|
341
|
+
claimed_at = CASE WHEN claimed_by IS NULL AND ? IS NOT NULL THEN ? ELSE claimed_at END,
|
|
342
|
+
claim_expires_at = CASE WHEN ? IN ('done', 'failed', 'canceled') THEN NULL ELSE claim_expires_at END,
|
|
343
|
+
updated_at = ?, last_seen_at = ?
|
|
344
|
+
WHERE id = ?
|
|
345
|
+
`).run(input.status, input.result ?? null, agentId, agentId, now, input.status, now, now, taskId);
|
|
346
|
+
if (result.changes === 0) return { ok: false, error: "task not found" };
|
|
347
|
+
if (["done", "failed", "canceled"].includes(input.status) && task.messageId) {
|
|
348
|
+
getDb().query("UPDATE messages SET claim_expires_at = NULL WHERE id = ?").run(task.messageId);
|
|
349
|
+
}
|
|
350
|
+
if (agentId && ["claimed", "in_progress", "blocked"].includes(input.status)) {
|
|
351
|
+
const expiresAt = now + CLAIM_LEASE_MS;
|
|
352
|
+
getDb().query("UPDATE tasks SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, taskId, agentId);
|
|
353
|
+
if (task.messageId) {
|
|
354
|
+
getDb().query("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, task.messageId, agentId);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const event = insertTaskEvent(taskId, {
|
|
358
|
+
source: input.agentId ?? "agent-relay",
|
|
359
|
+
type: `status:${input.status}`,
|
|
360
|
+
severity: task.severity,
|
|
361
|
+
title: `Task ${input.status}`,
|
|
362
|
+
body: input.body ?? input.result ?? `Task marked ${input.status}`,
|
|
363
|
+
metadata: input.metadata ?? {},
|
|
364
|
+
}, now);
|
|
365
|
+
return { ok: true, task: getTask(taskId)!, event };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function createCallbackDelivery(taskId: number, url: string, eventType: string, payload: unknown): number {
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
const result = getDb().query(`
|
|
371
|
+
INSERT INTO task_callback_deliveries (task_id, url, event_type, payload, status, attempts, created_at, updated_at)
|
|
372
|
+
VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)
|
|
373
|
+
`).run(taskId, url, eventType, JSON.stringify(payload), now, now);
|
|
374
|
+
return Number(result.lastInsertRowid);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function finishCallbackDelivery(id: number, ok: boolean, error?: string): void {
|
|
378
|
+
getDb().query(`
|
|
379
|
+
UPDATE task_callback_deliveries
|
|
380
|
+
SET status = ?, attempts = attempts + 1, last_error = ?, updated_at = ?
|
|
381
|
+
WHERE id = ?
|
|
382
|
+
`).run(ok ? "delivered" : "failed", error ?? null, Date.now(), id);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function listCallbackDeliveries(taskId: number): Array<{
|
|
386
|
+
id: number;
|
|
387
|
+
taskId: number;
|
|
388
|
+
url: string;
|
|
389
|
+
eventType: string;
|
|
390
|
+
status: string;
|
|
391
|
+
attempts: number;
|
|
392
|
+
lastError?: string;
|
|
393
|
+
}> {
|
|
394
|
+
return (getDb().query("SELECT * FROM task_callback_deliveries WHERE task_id = ? ORDER BY id ASC").all(taskId) as any[])
|
|
395
|
+
.map((row) => ({
|
|
396
|
+
id: row.id,
|
|
397
|
+
taskId: row.task_id,
|
|
398
|
+
url: row.url,
|
|
399
|
+
eventType: row.event_type,
|
|
400
|
+
status: row.status,
|
|
401
|
+
attempts: row.attempts,
|
|
402
|
+
lastError: row.last_error ?? undefined,
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// --- Pair sessions ---
|
|
407
|
+
|