@agent-native/core 0.119.4 → 0.119.6
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +1 -0
- package/corpus/core/src/agent/engine/index.ts +2 -0
- package/corpus/core/src/agent/engine/registry.ts +95 -2
- package/corpus/core/src/deploy/build.ts +144 -2
- package/corpus/core/src/deploy/workspace-deploy.ts +123 -1
- package/corpus/core/src/integrations/integration-durable-dispatch-config.ts +22 -0
- package/corpus/core/src/integrations/integration-durable-dispatch.ts +190 -0
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +175 -87
- package/corpus/core/src/integrations/pending-tasks-store.ts +114 -11
- package/corpus/core/src/integrations/plugin.ts +113 -60
- package/corpus/core/src/integrations/task-queue-stats.ts +68 -15
- package/corpus/core/src/integrations/webhook-handler.ts +21 -46
- package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +8 -0
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +9 -1
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/core/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/core/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/templates/dispatch/changelog/2026-07-21-recover-stalled-slack-agent-work.md +6 -0
- package/corpus/templates/slides/actions/patch-deck.ts +2 -1
- package/corpus/templates/slides/changelog/2026-07-23-opening-or-refining-a-deck-no-longer-crashes-with-a-blank-er.md +6 -0
- package/corpus/templates/slides/shared/aspect-ratios.ts +4 -1
- package/corpus/templates/tasks/.oxfmtrc.json +8 -0
- package/corpus/templates/tasks/AGENTS.md +34 -41
- package/corpus/templates/tasks/README.md +13 -6
- package/corpus/templates/tasks/actions/bulk-delete-inbox-items.ts +10 -8
- package/corpus/templates/tasks/actions/bulk-delete-tasks.ts +10 -8
- package/corpus/templates/tasks/actions/bulk-mark-inbox-items-ready.ts +1 -1
- package/corpus/templates/tasks/actions/bulk-update-tasks.ts +12 -10
- package/corpus/templates/tasks/actions/create-inbox-item.ts +7 -4
- package/corpus/templates/tasks/actions/create-task.ts +2 -1
- package/corpus/templates/tasks/actions/delete-inbox-item.ts +6 -4
- package/corpus/templates/tasks/actions/delete-task.ts +1 -1
- package/corpus/templates/tasks/actions/list-inbox-items.ts +4 -2
- package/corpus/templates/tasks/actions/list-visible-task-fields.ts +3 -1
- package/corpus/templates/tasks/actions/mark-inbox-item-ready.ts +6 -4
- package/corpus/templates/tasks/actions/navigate.ts +16 -14
- package/corpus/templates/tasks/actions/reorder-inbox-items.ts +9 -7
- package/corpus/templates/tasks/actions/update-inbox-item.ts +6 -4
- package/corpus/templates/tasks/actions/update-task.ts +12 -9
- package/corpus/templates/tasks/actions/update-visible-task-fields.ts +10 -8
- package/corpus/templates/tasks/actions/view-screen.ts +1 -1
- package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +15 -0
- package/corpus/templates/tasks/changelog/2026-07-23-task-detail-extensions.md +6 -0
- package/corpus/templates/tasks/drizzle.config.ts +3 -0
- package/corpus/templates/tasks/package.json +4 -1
- package/corpus/templates/tasks/server/db/schema.ts +0 -10
- package/corpus/templates/tasks/server/db/test-tasks-table.ts +0 -5
- package/corpus/templates/tasks/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/tasks/server/plugins/db.ts +43 -10
- package/corpus/templates/tasks/server/user-config/store.ts +42 -81
- package/corpus/templates/tasks/tsconfig.json +1 -2
- package/corpus/templates/tasks/vitest.config.ts +10 -1
- package/dist/agent/durable-background.d.ts +1 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +1 -0
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/index.d.ts +1 -1
- package/dist/agent/engine/index.d.ts.map +1 -1
- package/dist/agent/engine/index.js +1 -1
- package/dist/agent/engine/index.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +32 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +75 -3
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/deploy/build.d.ts +3 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +106 -3
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +103 -2
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/integration-durable-dispatch-config.d.ts +7 -0
- package/dist/integrations/integration-durable-dispatch-config.d.ts.map +1 -0
- package/dist/integrations/integration-durable-dispatch-config.js +16 -0
- package/dist/integrations/integration-durable-dispatch-config.js.map +1 -0
- package/dist/integrations/integration-durable-dispatch.d.ts +23 -0
- package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -0
- package/dist/integrations/integration-durable-dispatch.js +115 -0
- package/dist/integrations/integration-durable-dispatch.js.map +1 -0
- package/dist/integrations/pending-tasks-retry-job.d.ts +13 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +133 -74
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/pending-tasks-store.d.ts +14 -2
- package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-store.js +75 -11
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +88 -55
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/task-queue-stats.d.ts +15 -3
- package/dist/integrations/task-queue-stats.d.ts.map +1 -1
- package/dist/integrations/task-queue-stats.js +45 -15
- package/dist/integrations/task-queue-stats.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +18 -42
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +8 -2
- package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +5 -2
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +9 -2
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/dist/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/dist/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/package.json +1 -1
- package/src/agent/durable-background.ts +1 -0
- package/src/agent/engine/index.ts +2 -0
- package/src/agent/engine/registry.ts +95 -2
- package/src/deploy/build.ts +144 -2
- package/src/deploy/workspace-deploy.ts +123 -1
- package/src/integrations/integration-durable-dispatch-config.ts +22 -0
- package/src/integrations/integration-durable-dispatch.ts +190 -0
- package/src/integrations/pending-tasks-retry-job.ts +175 -87
- package/src/integrations/pending-tasks-store.ts +114 -11
- package/src/integrations/plugin.ts +113 -60
- package/src/integrations/task-queue-stats.ts +68 -15
- package/src/integrations/webhook-handler.ts +21 -46
- package/src/scripts/agent-engines/list-agent-engines.ts +8 -0
- package/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
- package/src/scripts/agent-engines/set-agent-engine.ts +9 -1
- package/src/server/auth.ts +6 -0
- package/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/templates/tasks/.prettierrc +0 -5
|
@@ -33,6 +33,10 @@ async function ensureTable(): Promise<void> {
|
|
|
33
33
|
org_id TEXT,
|
|
34
34
|
status TEXT NOT NULL,
|
|
35
35
|
attempts ${intType()} NOT NULL DEFAULT 0,
|
|
36
|
+
dispatch_attempts ${intType()} NOT NULL DEFAULT 0,
|
|
37
|
+
last_dispatch_at ${intType()},
|
|
38
|
+
last_dispatch_outcome TEXT,
|
|
39
|
+
dispatch_scope TEXT,
|
|
36
40
|
error_message TEXT,
|
|
37
41
|
created_at ${intType()} NOT NULL,
|
|
38
42
|
updated_at ${intType()} NOT NULL,
|
|
@@ -47,6 +51,26 @@ async function ensureTable(): Promise<void> {
|
|
|
47
51
|
"external_event_key",
|
|
48
52
|
`ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS external_event_key TEXT`,
|
|
49
53
|
);
|
|
54
|
+
await ensureColumnExists(
|
|
55
|
+
"integration_pending_tasks",
|
|
56
|
+
"dispatch_attempts",
|
|
57
|
+
`ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS dispatch_attempts ${intType()} NOT NULL DEFAULT 0`,
|
|
58
|
+
);
|
|
59
|
+
await ensureColumnExists(
|
|
60
|
+
"integration_pending_tasks",
|
|
61
|
+
"last_dispatch_at",
|
|
62
|
+
`ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS last_dispatch_at ${intType()}`,
|
|
63
|
+
);
|
|
64
|
+
await ensureColumnExists(
|
|
65
|
+
"integration_pending_tasks",
|
|
66
|
+
"last_dispatch_outcome",
|
|
67
|
+
`ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS last_dispatch_outcome TEXT`,
|
|
68
|
+
);
|
|
69
|
+
await ensureColumnExists(
|
|
70
|
+
"integration_pending_tasks",
|
|
71
|
+
"dispatch_scope",
|
|
72
|
+
`ALTER TABLE integration_pending_tasks ADD COLUMN IF NOT EXISTS dispatch_scope TEXT`,
|
|
73
|
+
);
|
|
50
74
|
await ensureIndexExists(
|
|
51
75
|
"idx_pending_tasks_status_created",
|
|
52
76
|
`CREATE INDEX IF NOT EXISTS idx_pending_tasks_status_created ON integration_pending_tasks(status, created_at)`,
|
|
@@ -55,6 +79,10 @@ async function ensureTable(): Promise<void> {
|
|
|
55
79
|
"idx_pending_tasks_event_key",
|
|
56
80
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_tasks_event_key ON integration_pending_tasks(platform, external_event_key)`,
|
|
57
81
|
);
|
|
82
|
+
await ensureIndexExists(
|
|
83
|
+
"idx_pending_tasks_dispatch_scope",
|
|
84
|
+
`CREATE INDEX IF NOT EXISTS idx_pending_tasks_dispatch_scope ON integration_pending_tasks(platform, dispatch_scope)`,
|
|
85
|
+
);
|
|
58
86
|
return;
|
|
59
87
|
}
|
|
60
88
|
|
|
@@ -69,6 +97,10 @@ async function ensureTable(): Promise<void> {
|
|
|
69
97
|
// unique index ensures a duplicate INSERT raises an error we can
|
|
70
98
|
// catch as "already-enqueued".
|
|
71
99
|
await ensureExternalEventKey(client);
|
|
100
|
+
await ensureDispatchColumns(client);
|
|
101
|
+
await client.execute(
|
|
102
|
+
`CREATE INDEX IF NOT EXISTS idx_pending_tasks_dispatch_scope ON integration_pending_tasks(platform, dispatch_scope)`,
|
|
103
|
+
);
|
|
72
104
|
await client.execute(
|
|
73
105
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_tasks_event_key ON integration_pending_tasks(platform, external_event_key)`,
|
|
74
106
|
);
|
|
@@ -81,6 +113,10 @@ async function ensureTable(): Promise<void> {
|
|
|
81
113
|
return _initPromise;
|
|
82
114
|
}
|
|
83
115
|
|
|
116
|
+
export async function ensurePendingTasksTable(): Promise<void> {
|
|
117
|
+
await ensureTable();
|
|
118
|
+
}
|
|
119
|
+
|
|
84
120
|
async function ensureExternalEventKey(
|
|
85
121
|
client: ReturnType<typeof getDbExec>,
|
|
86
122
|
): Promise<void> {
|
|
@@ -107,6 +143,32 @@ async function ensureExternalEventKey(
|
|
|
107
143
|
}
|
|
108
144
|
}
|
|
109
145
|
|
|
146
|
+
async function ensureDispatchColumns(
|
|
147
|
+
client: ReturnType<typeof getDbExec>,
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
const columns = [
|
|
150
|
+
`dispatch_attempts ${intType()} NOT NULL DEFAULT 0`,
|
|
151
|
+
`last_dispatch_at ${intType()}`,
|
|
152
|
+
"last_dispatch_outcome TEXT",
|
|
153
|
+
"dispatch_scope TEXT",
|
|
154
|
+
];
|
|
155
|
+
for (const column of columns) {
|
|
156
|
+
try {
|
|
157
|
+
await client.execute(
|
|
158
|
+
`ALTER TABLE integration_pending_tasks ADD COLUMN ${column}`,
|
|
159
|
+
);
|
|
160
|
+
} catch (err: any) {
|
|
161
|
+
if (
|
|
162
|
+
!String(err?.message ?? err)
|
|
163
|
+
.toLowerCase()
|
|
164
|
+
.includes("duplicate")
|
|
165
|
+
) {
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
110
172
|
/** Status values for an integration pending task. */
|
|
111
173
|
export type PendingTaskStatus =
|
|
112
174
|
| "pending"
|
|
@@ -123,6 +185,10 @@ export interface PendingTask {
|
|
|
123
185
|
orgId: string | null;
|
|
124
186
|
status: PendingTaskStatus;
|
|
125
187
|
attempts: number;
|
|
188
|
+
dispatchAttempts: number;
|
|
189
|
+
lastDispatchAt: number | null;
|
|
190
|
+
lastDispatchOutcome: string | null;
|
|
191
|
+
dispatchScope: string | null;
|
|
126
192
|
errorMessage: string | null;
|
|
127
193
|
createdAt: number;
|
|
128
194
|
updatedAt: number;
|
|
@@ -164,6 +230,13 @@ function rowToTask(row: Record<string, unknown>): PendingTask {
|
|
|
164
230
|
orgId: (row.org_id as string | null) ?? null,
|
|
165
231
|
status: row.status as PendingTaskStatus,
|
|
166
232
|
attempts: Number(row.attempts ?? 0),
|
|
233
|
+
dispatchAttempts: Number(row.dispatch_attempts ?? 0),
|
|
234
|
+
lastDispatchAt:
|
|
235
|
+
row.last_dispatch_at == null
|
|
236
|
+
? null
|
|
237
|
+
: Number(row.last_dispatch_at as number),
|
|
238
|
+
lastDispatchOutcome: (row.last_dispatch_outcome as string | null) ?? null,
|
|
239
|
+
dispatchScope: (row.dispatch_scope as string | null) ?? null,
|
|
167
240
|
errorMessage: (row.error_message as string | null) ?? null,
|
|
168
241
|
createdAt: Number(row.created_at ?? 0),
|
|
169
242
|
updatedAt: Number(row.updated_at ?? 0),
|
|
@@ -189,14 +262,15 @@ export async function insertPendingTask(input: {
|
|
|
189
262
|
ownerEmail: string;
|
|
190
263
|
orgId?: string | null;
|
|
191
264
|
externalEventKey?: string | null;
|
|
265
|
+
dispatchScope?: string | null;
|
|
192
266
|
}): Promise<void> {
|
|
193
267
|
await ensureTable();
|
|
194
268
|
const client = getDbExec();
|
|
195
269
|
const now = Date.now();
|
|
196
270
|
await client.execute({
|
|
197
271
|
sql: `INSERT INTO integration_pending_tasks
|
|
198
|
-
(id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, created_at, updated_at, external_event_key)
|
|
199
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
272
|
+
(id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, created_at, updated_at, external_event_key, dispatch_scope)
|
|
273
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
200
274
|
args: [
|
|
201
275
|
input.id,
|
|
202
276
|
input.platform,
|
|
@@ -209,6 +283,7 @@ export async function insertPendingTask(input: {
|
|
|
209
283
|
now,
|
|
210
284
|
now,
|
|
211
285
|
input.externalEventKey ?? null,
|
|
286
|
+
input.dispatchScope ?? null,
|
|
212
287
|
],
|
|
213
288
|
});
|
|
214
289
|
}
|
|
@@ -238,7 +313,7 @@ export async function getPendingTask(id: string): Promise<PendingTask | null> {
|
|
|
238
313
|
await ensureTable();
|
|
239
314
|
const client = getDbExec();
|
|
240
315
|
const { rows } = await client.execute({
|
|
241
|
-
sql: `SELECT id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at
|
|
316
|
+
sql: `SELECT id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, dispatch_attempts, last_dispatch_at, last_dispatch_outcome, dispatch_scope, error_message, created_at, updated_at, completed_at
|
|
242
317
|
FROM integration_pending_tasks WHERE id = ? LIMIT 1`,
|
|
243
318
|
args: [id],
|
|
244
319
|
});
|
|
@@ -342,6 +417,7 @@ export async function resolveIntegrationSourceContext(
|
|
|
342
417
|
*/
|
|
343
418
|
export async function claimPendingTask(
|
|
344
419
|
id: string,
|
|
420
|
+
options?: { dispatchOutcome?: string },
|
|
345
421
|
): Promise<PendingTask | null> {
|
|
346
422
|
await ensureTable();
|
|
347
423
|
const client = getDbExec();
|
|
@@ -352,7 +428,8 @@ export async function claimPendingTask(
|
|
|
352
428
|
const result = await client.execute({
|
|
353
429
|
sql: isPostgres()
|
|
354
430
|
? `UPDATE integration_pending_tasks
|
|
355
|
-
SET status = ?, attempts = attempts + 1, updated_at =
|
|
431
|
+
SET status = ?, attempts = attempts + 1, updated_at = ?,
|
|
432
|
+
last_dispatch_outcome = COALESCE(?, last_dispatch_outcome)
|
|
356
433
|
WHERE id = ? AND status = 'pending'
|
|
357
434
|
AND NOT EXISTS (
|
|
358
435
|
SELECT 1 FROM integration_pending_tasks active
|
|
@@ -374,9 +451,10 @@ export async function claimPendingTask(
|
|
|
374
451
|
)
|
|
375
452
|
)
|
|
376
453
|
)
|
|
377
|
-
RETURNING id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at`
|
|
454
|
+
RETURNING id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, dispatch_attempts, last_dispatch_at, last_dispatch_outcome, dispatch_scope, error_message, created_at, updated_at, completed_at`
|
|
378
455
|
: `UPDATE integration_pending_tasks
|
|
379
|
-
SET status = ?, attempts = attempts + 1, updated_at =
|
|
456
|
+
SET status = ?, attempts = attempts + 1, updated_at = ?,
|
|
457
|
+
last_dispatch_outcome = COALESCE(?, last_dispatch_outcome)
|
|
380
458
|
WHERE id = ? AND status = 'pending'
|
|
381
459
|
AND NOT EXISTS (
|
|
382
460
|
SELECT 1 FROM integration_pending_tasks active
|
|
@@ -398,7 +476,7 @@ export async function claimPendingTask(
|
|
|
398
476
|
)
|
|
399
477
|
)
|
|
400
478
|
)`,
|
|
401
|
-
args: ["processing", now, id],
|
|
479
|
+
args: ["processing", now, options?.dispatchOutcome ?? null, id],
|
|
402
480
|
});
|
|
403
481
|
const rows = result.rows ?? [];
|
|
404
482
|
|
|
@@ -418,19 +496,44 @@ export async function claimPendingTask(
|
|
|
418
496
|
return fetched;
|
|
419
497
|
}
|
|
420
498
|
|
|
499
|
+
export async function recordPendingTaskDispatchAttempt(
|
|
500
|
+
id: string,
|
|
501
|
+
outcome: string,
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
await ensureTable();
|
|
504
|
+
await getDbExec().execute({
|
|
505
|
+
sql: `UPDATE integration_pending_tasks
|
|
506
|
+
SET dispatch_attempts = dispatch_attempts + 1,
|
|
507
|
+
last_dispatch_at = ?,
|
|
508
|
+
last_dispatch_outcome = CASE
|
|
509
|
+
WHEN status = 'processing'
|
|
510
|
+
AND last_dispatch_outcome = 'background-acknowledged'
|
|
511
|
+
THEN last_dispatch_outcome
|
|
512
|
+
ELSE ?
|
|
513
|
+
END
|
|
514
|
+
WHERE id = ?`,
|
|
515
|
+
args: [Date.now(), outcome.slice(0, 80), id],
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
421
519
|
/** Next queued turn for a provider thread after its current task completes. */
|
|
422
|
-
export async function
|
|
520
|
+
export async function getNextPendingTaskForThread(
|
|
423
521
|
platform: string,
|
|
424
522
|
externalThreadId: string,
|
|
425
|
-
): Promise<string | null> {
|
|
523
|
+
): Promise<{ id: string; dispatchScope: string | null } | null> {
|
|
426
524
|
await ensureTable();
|
|
427
525
|
const { rows } = await getDbExec().execute({
|
|
428
|
-
sql: `SELECT id FROM integration_pending_tasks
|
|
526
|
+
sql: `SELECT id, dispatch_scope FROM integration_pending_tasks
|
|
429
527
|
WHERE platform = ? AND external_thread_id = ? AND status = 'pending'
|
|
430
528
|
ORDER BY created_at ASC, id ASC LIMIT 1`,
|
|
431
529
|
args: [platform, externalThreadId],
|
|
432
530
|
});
|
|
433
|
-
return rows[0]?.id
|
|
531
|
+
return rows[0]?.id
|
|
532
|
+
? {
|
|
533
|
+
id: String(rows[0].id),
|
|
534
|
+
dispatchScope: (rows[0].dispatch_scope as string | null) ?? null,
|
|
535
|
+
}
|
|
536
|
+
: null;
|
|
434
537
|
}
|
|
435
538
|
|
|
436
539
|
/** Mark a task as completed. */
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
import { getRequestHeader } from "h3";
|
|
10
10
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
AGENT_BACKGROUND_PROCESSOR_FIELD,
|
|
14
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
|
|
15
|
+
} from "../agent/durable-background.js";
|
|
12
16
|
import { abortRun } from "../agent/run-manager.js";
|
|
13
17
|
import { getOrgContext, resolveOrgIdForEmail } from "../org/context.js";
|
|
14
18
|
import { loadResourcesForPrompt } from "../server/agent-chat-plugin.js";
|
|
@@ -65,22 +69,27 @@ import {
|
|
|
65
69
|
updateIntegrationInstallation,
|
|
66
70
|
upsertIntegrationInstallation,
|
|
67
71
|
} from "./installations-store.js";
|
|
72
|
+
import {
|
|
73
|
+
dispatchPendingIntegrationTask,
|
|
74
|
+
INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
|
|
75
|
+
integrationDispatchScopeValue,
|
|
76
|
+
isIntegrationDurableDispatchConfigured,
|
|
77
|
+
} from "./integration-durable-dispatch.js";
|
|
68
78
|
import {
|
|
69
79
|
forgetIntegrationMemory,
|
|
70
80
|
integrationMemoryActions,
|
|
71
81
|
listIntegrationMemory,
|
|
72
82
|
rememberForIntegrationScope,
|
|
73
83
|
} from "./integration-memory.js";
|
|
84
|
+
import { extractBearerToken, verifyInternalToken } from "./internal-token.js";
|
|
74
85
|
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} from "./internal-token.js";
|
|
79
|
-
import { startPendingTasksRetryJob } from "./pending-tasks-retry-job.js";
|
|
86
|
+
retryStuckPendingTasks,
|
|
87
|
+
startPendingTasksRetryJob,
|
|
88
|
+
} from "./pending-tasks-retry-job.js";
|
|
80
89
|
import {
|
|
81
90
|
claimPendingTask,
|
|
82
91
|
failTaskDeliveryTransition,
|
|
83
|
-
|
|
92
|
+
getNextPendingTaskForThread,
|
|
84
93
|
insertPendingTask,
|
|
85
94
|
isDuplicateEventError,
|
|
86
95
|
MAX_PENDING_TASK_ATTEMPTS,
|
|
@@ -725,40 +734,29 @@ export function createIntegrationsPlugin(
|
|
|
725
734
|
externalEventKey: opts?.dedupeKey
|
|
726
735
|
? systemNoticeEventKey(opts.dedupeKey, dedupeTtlMs)
|
|
727
736
|
: undefined,
|
|
737
|
+
dispatchScope: integrationDispatchScopeValue({
|
|
738
|
+
platform: incoming.platform,
|
|
739
|
+
externalThreadId: noticeThreadId,
|
|
740
|
+
platformContext: incoming.platformContext,
|
|
741
|
+
}),
|
|
728
742
|
});
|
|
729
743
|
} catch (err) {
|
|
730
744
|
if (isDuplicateEventError(err)) return;
|
|
731
745
|
throw err;
|
|
732
746
|
}
|
|
733
747
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
} catch (err) {
|
|
741
|
-
if (process.env.NODE_ENV === "production") throw err;
|
|
742
|
-
}
|
|
743
|
-
void fetch(`${getBaseUrl(event)}${P}/process-task`, {
|
|
744
|
-
method: "POST",
|
|
745
|
-
headers: {
|
|
746
|
-
"Content-Type": "application/json",
|
|
747
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
748
|
+
await dispatchPendingIntegrationTask({
|
|
749
|
+
taskId,
|
|
750
|
+
task: {
|
|
751
|
+
platform: incoming.platform,
|
|
752
|
+
externalThreadId: noticeThreadId,
|
|
753
|
+
platformContext: incoming.platformContext,
|
|
748
754
|
},
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
console.warn("[integrations] System notice dispatch failed:", err);
|
|
755
|
+
event,
|
|
756
|
+
baseUrl: getBaseUrl(event),
|
|
752
757
|
});
|
|
753
758
|
}
|
|
754
759
|
|
|
755
|
-
async function requireSession(event: any): Promise<boolean> {
|
|
756
|
-
const session = await getSession(event).catch(() => null);
|
|
757
|
-
if (session?.email) return true;
|
|
758
|
-
setResponseStatus(event, 401);
|
|
759
|
-
return false;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
760
|
async function requireSessionContext(
|
|
763
761
|
event: any,
|
|
764
762
|
): Promise<{ ownerEmail: string; orgId: string | null } | null> {
|
|
@@ -903,9 +901,10 @@ export function createIntegrationsPlugin(
|
|
|
903
901
|
setResponseStatus(event, 405);
|
|
904
902
|
return { error: "Method not allowed" };
|
|
905
903
|
}
|
|
906
|
-
|
|
904
|
+
const scope = await requireSessionContext(event);
|
|
905
|
+
if (!scope) return { error: "unauthorized" };
|
|
907
906
|
try {
|
|
908
|
-
return await getTaskQueueStats();
|
|
907
|
+
return await getTaskQueueStats(scope);
|
|
909
908
|
} catch (err: any) {
|
|
910
909
|
setResponseStatus(event, 500);
|
|
911
910
|
return { error: err?.message ?? String(err) };
|
|
@@ -1595,10 +1594,53 @@ export function createIntegrationsPlugin(
|
|
|
1595
1594
|
}),
|
|
1596
1595
|
);
|
|
1597
1596
|
|
|
1597
|
+
// ─── Durable pending-task recovery sweep ─────────────────────
|
|
1598
|
+
h3.use(
|
|
1599
|
+
`${P}/retry-stuck-tasks`,
|
|
1600
|
+
defineEventHandler(async (event) => {
|
|
1601
|
+
if (getMethod(event) !== "POST") {
|
|
1602
|
+
setResponseStatus(event, 405);
|
|
1603
|
+
return { error: "Method not allowed" };
|
|
1604
|
+
}
|
|
1605
|
+
const body = (await readBody(event)) as {
|
|
1606
|
+
taskId?: string;
|
|
1607
|
+
[AGENT_BACKGROUND_PROCESSOR_FIELD]?: string;
|
|
1608
|
+
};
|
|
1609
|
+
if (body?.taskId !== INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT) {
|
|
1610
|
+
setResponseStatus(event, 400);
|
|
1611
|
+
return { error: "invalid sweep subject" };
|
|
1612
|
+
}
|
|
1613
|
+
if (!process.env.A2A_SECRET) {
|
|
1614
|
+
setResponseStatus(event, 503);
|
|
1615
|
+
return { error: "durable integration recovery is not configured" };
|
|
1616
|
+
}
|
|
1617
|
+
const token = extractBearerToken(
|
|
1618
|
+
getRequestHeader(event, "authorization"),
|
|
1619
|
+
);
|
|
1620
|
+
if (
|
|
1621
|
+
!token ||
|
|
1622
|
+
!verifyInternalToken(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, token)
|
|
1623
|
+
) {
|
|
1624
|
+
setResponseStatus(event, 401);
|
|
1625
|
+
return { error: "Invalid or expired internal token" };
|
|
1626
|
+
}
|
|
1627
|
+
if (!isIntegrationDurableDispatchConfigured()) {
|
|
1628
|
+
return { ok: true, disabled: true };
|
|
1629
|
+
}
|
|
1630
|
+
const result = await retryStuckPendingTasks({
|
|
1631
|
+
webhookBaseUrl: getBaseUrl(event),
|
|
1632
|
+
limit: 20,
|
|
1633
|
+
durableOnly: true,
|
|
1634
|
+
});
|
|
1635
|
+
return { ok: true, ...result };
|
|
1636
|
+
}),
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1598
1639
|
// ─── Process pending task (cross-platform task queue) ────────
|
|
1599
1640
|
// POST /_agent-native/integrations/process-task
|
|
1600
|
-
// Internal endpoint invoked
|
|
1601
|
-
//
|
|
1641
|
+
// Internal endpoint invoked from the public webhook handler through either
|
|
1642
|
+
// the portable self-dispatch path or an acknowledged background handoff.
|
|
1643
|
+
// Auth: HMAC bearer signed with A2A_SECRET.
|
|
1602
1644
|
// Each invocation runs the agent loop in a fresh function execution.
|
|
1603
1645
|
h3.use(
|
|
1604
1646
|
`${P}/process-task`,
|
|
@@ -1608,7 +1650,10 @@ export function createIntegrationsPlugin(
|
|
|
1608
1650
|
return { error: "Method not allowed" };
|
|
1609
1651
|
}
|
|
1610
1652
|
|
|
1611
|
-
const body = (await readBody(event)) as {
|
|
1653
|
+
const body = (await readBody(event)) as {
|
|
1654
|
+
taskId?: string;
|
|
1655
|
+
[AGENT_BACKGROUND_PROCESSOR_FIELD]?: string;
|
|
1656
|
+
};
|
|
1612
1657
|
const taskId = body?.taskId;
|
|
1613
1658
|
if (!taskId) {
|
|
1614
1659
|
setResponseStatus(event, 400);
|
|
@@ -1645,7 +1690,12 @@ export function createIntegrationsPlugin(
|
|
|
1645
1690
|
}
|
|
1646
1691
|
|
|
1647
1692
|
// Atomic claim: only one invocation gets to process this task
|
|
1648
|
-
const
|
|
1693
|
+
const dispatchOutcome =
|
|
1694
|
+
body[AGENT_BACKGROUND_PROCESSOR_FIELD] ===
|
|
1695
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION
|
|
1696
|
+
? "background-acknowledged"
|
|
1697
|
+
: "portable-unconfirmed";
|
|
1698
|
+
const task = await claimPendingTask(taskId, { dispatchOutcome });
|
|
1649
1699
|
if (!task) {
|
|
1650
1700
|
setResponseStatus(event, 200);
|
|
1651
1701
|
return { ok: true, skipped: "already-claimed-or-missing" };
|
|
@@ -1779,24 +1829,22 @@ export function createIntegrationsPlugin(
|
|
|
1779
1829
|
return { ok: true, taskId, retrying: "response-delivery" };
|
|
1780
1830
|
}
|
|
1781
1831
|
await markTaskCompleted(taskId);
|
|
1782
|
-
const
|
|
1832
|
+
const nextTask = await getNextPendingTaskForThread(
|
|
1783
1833
|
task.platform,
|
|
1784
1834
|
task.externalThreadId,
|
|
1785
1835
|
);
|
|
1786
|
-
if (
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1836
|
+
if (nextTask) {
|
|
1837
|
+
await dispatchPendingIntegrationTask({
|
|
1838
|
+
taskId: nextTask.id,
|
|
1839
|
+
task: {
|
|
1840
|
+
platform: task.platform,
|
|
1841
|
+
externalThreadId: task.externalThreadId,
|
|
1842
|
+
platformContext: nextTask.dispatchScope
|
|
1843
|
+
? { channelId: nextTask.dispatchScope }
|
|
1844
|
+
: undefined,
|
|
1793
1845
|
},
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
console.error(
|
|
1797
|
-
"[integrations] Failed to dispatch queued thread turn:",
|
|
1798
|
-
err,
|
|
1799
|
-
);
|
|
1846
|
+
event,
|
|
1847
|
+
baseUrl: getBaseUrl(event),
|
|
1800
1848
|
});
|
|
1801
1849
|
}
|
|
1802
1850
|
await processDueA2AContinuations({
|
|
@@ -2018,18 +2066,21 @@ export function createIntegrationsPlugin(
|
|
|
2018
2066
|
ownerEmail: control.ownerEmail,
|
|
2019
2067
|
orgId: control.orgId,
|
|
2020
2068
|
externalEventKey: `control:${control.id}`,
|
|
2069
|
+
dispatchScope: integrationDispatchScopeValue({
|
|
2070
|
+
platform: incoming.platform,
|
|
2071
|
+
externalThreadId: incoming.externalThreadId,
|
|
2072
|
+
platformContext: incoming.platformContext,
|
|
2073
|
+
}),
|
|
2021
2074
|
});
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
2075
|
+
await dispatchPendingIntegrationTask({
|
|
2076
|
+
taskId,
|
|
2077
|
+
task: {
|
|
2078
|
+
platform: incoming.platform,
|
|
2079
|
+
externalThreadId: incoming.externalThreadId,
|
|
2080
|
+
platformContext: incoming.platformContext,
|
|
2029
2081
|
},
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
console.error("[slack] Approval dispatch failed:", err);
|
|
2082
|
+
event,
|
|
2083
|
+
baseUrl: getBaseUrl(event),
|
|
2033
2084
|
});
|
|
2034
2085
|
} catch (err) {
|
|
2035
2086
|
console.error("[slack] Interaction handling failed:", err);
|
|
@@ -2480,6 +2531,8 @@ export function createIntegrationsPlugin(
|
|
|
2480
2531
|
if (parts[0] === "remote") return;
|
|
2481
2532
|
// Already handled by the dedicated /process-task route above
|
|
2482
2533
|
if (parts[0] === "process-task") return;
|
|
2534
|
+
// Already handled by the signed durable recovery route above
|
|
2535
|
+
if (parts[0] === "retry-stuck-tasks") return;
|
|
2483
2536
|
// Already handled by the dedicated /process-a2a-continuation route above
|
|
2484
2537
|
if (parts[0] === "process-a2a-continuation") return;
|
|
2485
2538
|
// These are framework-owned control-plane routes, not integration
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lives in its own file so it stays out of `pending-tasks-store.ts`, which is
|
|
5
5
|
* actively being edited by the agent that owns the queue itself. These
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Queue reads never expose payloads or user text. The helper first runs the
|
|
7
|
+
* additive schema guard so older deployments gain the dispatch diagnostic
|
|
8
|
+
* columns before the SELECTs execute.
|
|
9
9
|
*/
|
|
10
10
|
import { getDbExec } from "../db/client.js";
|
|
11
|
+
import { ensurePendingTasksTable } from "./pending-tasks-store.js";
|
|
11
12
|
|
|
12
13
|
export interface RecentFailure {
|
|
13
14
|
id: string;
|
|
@@ -23,6 +24,20 @@ export interface TaskQueueStats {
|
|
|
23
24
|
failed_last_hour: number;
|
|
24
25
|
oldest_pending_age_seconds: number;
|
|
25
26
|
recent_failures: RecentFailure[];
|
|
27
|
+
recent_tasks: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
platform: string;
|
|
30
|
+
status: string;
|
|
31
|
+
attempts: number;
|
|
32
|
+
dispatch_attempts: number;
|
|
33
|
+
last_dispatch_outcome: string | null;
|
|
34
|
+
age_seconds: number;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TaskQueueStatsScope {
|
|
39
|
+
ownerEmail: string;
|
|
40
|
+
orgId: string | null;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
const ZERO_STATS: TaskQueueStats = {
|
|
@@ -32,6 +47,7 @@ const ZERO_STATS: TaskQueueStats = {
|
|
|
32
47
|
failed_last_hour: 0,
|
|
33
48
|
oldest_pending_age_seconds: 0,
|
|
34
49
|
recent_failures: [],
|
|
50
|
+
recent_tasks: [],
|
|
35
51
|
};
|
|
36
52
|
|
|
37
53
|
function isMissingTableError(err: unknown): boolean {
|
|
@@ -44,21 +60,27 @@ function isMissingTableError(err: unknown): boolean {
|
|
|
44
60
|
/**
|
|
45
61
|
* Get a snapshot of the integration task queue health.
|
|
46
62
|
*
|
|
47
|
-
*
|
|
48
|
-
* pending-tasks store has initialised the schema.
|
|
63
|
+
* Safe to call before the pending-tasks store has initialized the schema.
|
|
49
64
|
*/
|
|
50
|
-
export async function getTaskQueueStats(
|
|
65
|
+
export async function getTaskQueueStats(
|
|
66
|
+
scope: TaskQueueStatsScope,
|
|
67
|
+
): Promise<TaskQueueStats> {
|
|
68
|
+
await ensurePendingTasksTable();
|
|
51
69
|
const client = getDbExec();
|
|
52
70
|
const now = Date.now();
|
|
53
71
|
const oneHourAgo = now - 60 * 60 * 1000;
|
|
72
|
+
const scopeSql = `owner_email = ?
|
|
73
|
+
AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)`;
|
|
74
|
+
const scopeArgs = [scope.ownerEmail, scope.orgId, scope.orgId];
|
|
54
75
|
|
|
55
76
|
try {
|
|
56
77
|
// Status counts (pending, processing) — only need the live ones.
|
|
57
78
|
const liveCounts = await client.execute({
|
|
58
79
|
sql: `SELECT status, COUNT(*) AS c FROM integration_pending_tasks
|
|
59
|
-
WHERE
|
|
80
|
+
WHERE ${scopeSql}
|
|
81
|
+
AND status IN ('pending', 'processing')
|
|
60
82
|
GROUP BY status`,
|
|
61
|
-
args:
|
|
83
|
+
args: scopeArgs,
|
|
62
84
|
});
|
|
63
85
|
|
|
64
86
|
let pending = 0;
|
|
@@ -75,9 +97,10 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
75
97
|
// miss tasks queued >1h ago that just finished now.
|
|
76
98
|
const lastHourCounts = await client.execute({
|
|
77
99
|
sql: `SELECT status, COUNT(*) AS c FROM integration_pending_tasks
|
|
78
|
-
WHERE
|
|
100
|
+
WHERE ${scopeSql}
|
|
101
|
+
AND status IN ('completed', 'failed') AND updated_at >= ?
|
|
79
102
|
GROUP BY status`,
|
|
80
|
-
args: [oneHourAgo],
|
|
103
|
+
args: [...scopeArgs, oneHourAgo],
|
|
81
104
|
});
|
|
82
105
|
|
|
83
106
|
let completedLastHour = 0;
|
|
@@ -94,10 +117,11 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
94
117
|
if (pending > 0) {
|
|
95
118
|
const oldest = await client.execute({
|
|
96
119
|
sql: `SELECT created_at FROM integration_pending_tasks
|
|
97
|
-
WHERE
|
|
120
|
+
WHERE ${scopeSql}
|
|
121
|
+
AND status = 'pending'
|
|
98
122
|
ORDER BY created_at ASC
|
|
99
123
|
LIMIT 1`,
|
|
100
|
-
args:
|
|
124
|
+
args: scopeArgs,
|
|
101
125
|
});
|
|
102
126
|
const oldestRow = oldest.rows[0] as Record<string, unknown> | undefined;
|
|
103
127
|
if (oldestRow) {
|
|
@@ -113,10 +137,11 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
113
137
|
// blowing up the response payload.
|
|
114
138
|
const failures = await client.execute({
|
|
115
139
|
sql: `SELECT id, platform, error_message, attempts FROM integration_pending_tasks
|
|
116
|
-
WHERE
|
|
140
|
+
WHERE ${scopeSql}
|
|
141
|
+
AND status = 'failed' AND updated_at >= ?
|
|
117
142
|
ORDER BY updated_at DESC
|
|
118
143
|
LIMIT 5`,
|
|
119
|
-
args: [oneHourAgo],
|
|
144
|
+
args: [...scopeArgs, oneHourAgo],
|
|
120
145
|
});
|
|
121
146
|
const recentFailures: RecentFailure[] = (
|
|
122
147
|
failures.rows as Array<Record<string, unknown>>
|
|
@@ -127,6 +152,33 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
127
152
|
attempts: Number(row.attempts ?? 0),
|
|
128
153
|
}));
|
|
129
154
|
|
|
155
|
+
const recent = await client.execute({
|
|
156
|
+
sql: `SELECT id, platform, status, attempts, dispatch_attempts,
|
|
157
|
+
last_dispatch_outcome, created_at
|
|
158
|
+
FROM integration_pending_tasks
|
|
159
|
+
WHERE ${scopeSql}
|
|
160
|
+
ORDER BY created_at DESC
|
|
161
|
+
LIMIT 10`,
|
|
162
|
+
args: scopeArgs,
|
|
163
|
+
});
|
|
164
|
+
const recentTasks = (recent.rows as Array<Record<string, unknown>>).map(
|
|
165
|
+
(row) => ({
|
|
166
|
+
id: String(row.id ?? ""),
|
|
167
|
+
platform: String(row.platform ?? ""),
|
|
168
|
+
status: String(row.status ?? ""),
|
|
169
|
+
attempts: Number(row.attempts ?? 0),
|
|
170
|
+
dispatch_attempts: Number(row.dispatch_attempts ?? 0),
|
|
171
|
+
last_dispatch_outcome:
|
|
172
|
+
row.last_dispatch_outcome == null
|
|
173
|
+
? null
|
|
174
|
+
: String(row.last_dispatch_outcome),
|
|
175
|
+
age_seconds: Math.max(
|
|
176
|
+
0,
|
|
177
|
+
Math.floor((now - Number(row.created_at ?? now)) / 1000),
|
|
178
|
+
),
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
|
|
130
182
|
return {
|
|
131
183
|
pending,
|
|
132
184
|
processing,
|
|
@@ -134,10 +186,11 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
134
186
|
failed_last_hour: failedLastHour,
|
|
135
187
|
oldest_pending_age_seconds: oldestPendingAgeSeconds,
|
|
136
188
|
recent_failures: recentFailures,
|
|
189
|
+
recent_tasks: recentTasks,
|
|
137
190
|
};
|
|
138
191
|
} catch (err) {
|
|
139
192
|
if (isMissingTableError(err)) {
|
|
140
|
-
return { ...ZERO_STATS, recent_failures: [] };
|
|
193
|
+
return { ...ZERO_STATS, recent_failures: [], recent_tasks: [] };
|
|
141
194
|
}
|
|
142
195
|
throw err;
|
|
143
196
|
}
|