@pasko70/pibo 3.0.1 → 3.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/README.md +20 -6
- package/compute-image/Dockerfile +47 -0
- package/compute-image/Dockerfile.dockerignore +5 -0
- package/dist/agent-runtime/context-build.js +26 -2
- package/dist/agent-runtime/contract.js +78 -28
- package/dist/agent-runtime/portable-history.js +19 -9
- package/dist/agent-runtime/registry.js +8 -2
- package/dist/agent-runtime/routed-session.js +120 -15
- package/dist/agent-runtimes/codex-native/adapter.js +12 -0
- package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
- package/dist/agent-runtimes/omp/adapter.js +3 -7
- package/dist/agent-runtimes/pi/adapter.js +5 -3
- package/dist/agent-runtimes/pi/routed-session.js +59 -12
- package/dist/agent-runtimes/pi/runtime.js +6 -0
- package/dist/apps/chat/agent-profiles.js +31 -2
- package/dist/apps/chat/agent-store.js +98 -6
- package/dist/apps/chat/chat-request-normalizers.js +31 -0
- package/dist/apps/chat/data/chat-data-mappers.js +61 -11
- package/dist/apps/chat/data/project-service.js +119 -15
- package/dist/apps/chat/data/room-service.js +21 -0
- package/dist/apps/chat/data/session-query-service.js +33 -10
- package/dist/apps/chat/data/timeline-query-service.js +60 -35
- package/dist/apps/chat/loop-api.js +10 -2
- package/dist/apps/chat/output-compactor.js +144 -21
- package/dist/apps/chat/output-event-policy.js +179 -2
- package/dist/apps/chat/stream.js +16 -7
- package/dist/apps/chat/trace-v2.js +1 -0
- package/dist/apps/chat/web-app.js +317 -86
- package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
- package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/InkSessionApp.js +111 -12
- package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
- package/dist/cli-session/localSessionSource.js +153 -161
- package/dist/cli.js +7 -11
- package/dist/compute/cli.js +35 -16
- package/dist/compute/docker.js +341 -94
- package/dist/config/config.js +3 -0
- package/dist/core/context-build.js +7 -16
- package/dist/core/output-persistence-retry.js +484 -0
- package/dist/core/output-render-sequence.js +331 -0
- package/dist/core/profiles.js +24 -0
- package/dist/core/provider-recovery.js +7 -1
- package/dist/core/session-model.js +37 -1
- package/dist/core/session-router.js +78 -110
- package/dist/data/ingest-service.js +91 -9
- package/dist/data/navigation-store.js +5 -5
- package/dist/data/pibo-store.js +8 -1
- package/dist/data/schema.js +243 -3
- package/dist/data/session-store.js +3 -3
- package/dist/debug/agents.js +91 -89
- package/dist/debug/index.js +46 -2
- package/dist/debug/pty.js +127 -48
- package/dist/gateway/cli.js +4 -0
- package/dist/gateway/web.js +8 -8
- package/dist/local/client.js +3 -2
- package/dist/loops/channel.js +3 -1
- package/dist/loops/cli.js +2 -1
- package/dist/loops/service.js +90 -11
- package/dist/loops/store.js +100 -74
- package/dist/mcp/agent-context.js +13 -26
- package/dist/mcp/commands/info.js +3 -1
- package/dist/mcp/config-command.js +46 -2
- package/dist/mcp/config.js +18 -4
- package/dist/plugins/context-files-store.js +36 -6
- package/dist/plugins/context-files.js +52 -1
- package/dist/plugins/registry.js +2 -0
- package/dist/plugins/user-profile-resources.js +22 -0
- package/dist/previews/base-url.js +34 -0
- package/dist/previews/config.js +2 -32
- package/dist/ralph/store.js +5 -0
- package/dist/reliability/store.js +378 -106
- package/dist/session-ui/terminalRows.js +8 -4
- package/dist/sessions/pibo-data-store.js +145 -0
- package/dist/sessions/store.js +42 -0
- package/dist/setup/cli.js +422 -6
- package/dist/setup/installation-profiles.js +464 -0
- package/dist/shared/deterministic-digest.js +94 -0
- package/dist/shared/trace-engine.js +54 -0
- package/dist/shared/trace-event-projection.js +124 -67
- package/dist/shared/trace-history.js +47 -12
- package/dist/shared/trace-live-reducer.js +23 -4
- package/dist/shared/trace-nodes.js +17 -28
- package/dist/shared/trace-order.js +41 -5
- package/dist/shared/trace-page-merge.js +44 -14
- package/dist/shared/trace-patch-nodes.js +2 -0
- package/dist/shared/trace-subagent-links.js +4 -1
- package/dist/shared/trace-tool-identity.js +15 -8
- package/dist/signals/projector.js +28 -6
- package/dist/subagents/observation-query.js +121 -0
- package/dist/subagents/tool.js +7 -3
- package/dist/tools/agent-browser-leases.js +58 -23
- package/dist/tools/agent-browser-wrapper.js +1 -1
- package/dist/tools/browser-use-leases.js +129 -16
- package/dist/tools/browser-use-wrapper.js +1 -1
- package/dist/tools/index.js +27 -11
- package/dist/tools/python-runtime.js +10 -3
- package/dist/tools/registry.js +1 -1
- package/dist/tools/runtime/node-backend.js +16 -5
- package/dist/tools/runtime/node-worker-source.js +112 -33
- package/dist/tools/runtime/python-backend.js +16 -5
- package/dist/tools/runtime/python-worker-source.js +167 -16
- package/dist/tools/runtime/registry.js +23 -10
- package/dist/user-skills/store.js +0 -1
- package/docs/ops/vscode-extension-release.md +9 -1
- package/npm-shrinkwrap.json +9 -2
- package/package.json +9 -2
- package/scripts/docker-entrypoint.sh +46 -0
- package/scripts/prepare-agent-browser-wrapper.sh +70 -0
- package/scripts/prepare-browser-use-wrapper.sh +255 -0
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
|
@@ -5,6 +5,8 @@ import { ensurePrivatePiboHomeForPath, piboHomePath } from "../core/pibo-home.js
|
|
|
5
5
|
import { protectPrivateFileSync } from "../core/private-path.js";
|
|
6
6
|
import { sqliteTableColumns } from "../data/sqlite-schema.js";
|
|
7
7
|
import { DatabaseSync } from "node:sqlite";
|
|
8
|
+
const MAX_ATTEMPTS_ERROR = "Job exhausted retry attempts.";
|
|
9
|
+
const MAX_ATTEMPTS_REASON = "max_attempts";
|
|
8
10
|
function now() {
|
|
9
11
|
return new Date().toISOString();
|
|
10
12
|
}
|
|
@@ -36,6 +38,11 @@ function ensurePiboRunColumn(db, name, definition) {
|
|
|
36
38
|
if (!columns.has(name))
|
|
37
39
|
db.exec(`ALTER TABLE pibo_runs ADD COLUMN ${name} ${definition}`);
|
|
38
40
|
}
|
|
41
|
+
function ensurePiboJobColumn(db, name, definition) {
|
|
42
|
+
const columns = sqliteTableColumns(db, "pibo_jobs");
|
|
43
|
+
if (!columns.has(name))
|
|
44
|
+
db.exec(`ALTER TABLE pibo_jobs ADD COLUMN ${name} ${definition}`);
|
|
45
|
+
}
|
|
39
46
|
export class PiboReliabilityStore {
|
|
40
47
|
db;
|
|
41
48
|
appendEventStatement;
|
|
@@ -74,6 +81,13 @@ export class PiboReliabilityStore {
|
|
|
74
81
|
CREATE INDEX IF NOT EXISTS idx_pibo_event_stream_topic_stream
|
|
75
82
|
ON pibo_event_stream(topic, stream_id);
|
|
76
83
|
|
|
84
|
+
CREATE TABLE IF NOT EXISTS pibo_delivery_receipts (
|
|
85
|
+
delivery_id TEXT NOT NULL,
|
|
86
|
+
projection TEXT NOT NULL,
|
|
87
|
+
delivered_at TEXT NOT NULL,
|
|
88
|
+
PRIMARY KEY(delivery_id, projection)
|
|
89
|
+
);
|
|
90
|
+
|
|
77
91
|
CREATE TABLE IF NOT EXISTS pibo_event_consumers (
|
|
78
92
|
consumer TEXT NOT NULL,
|
|
79
93
|
topic TEXT NOT NULL,
|
|
@@ -91,6 +105,7 @@ export class PiboReliabilityStore {
|
|
|
91
105
|
priority INTEGER NOT NULL DEFAULT 0,
|
|
92
106
|
worker_id TEXT,
|
|
93
107
|
claim_expires_at TEXT,
|
|
108
|
+
claim_token INTEGER NOT NULL DEFAULT 0,
|
|
94
109
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
95
110
|
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
96
111
|
idempotency_key TEXT,
|
|
@@ -157,6 +172,7 @@ export class PiboReliabilityStore {
|
|
|
157
172
|
CREATE INDEX IF NOT EXISTS idx_pibo_runs_status
|
|
158
173
|
ON pibo_runs(status);
|
|
159
174
|
`);
|
|
175
|
+
ensurePiboJobColumn(this.db, "claim_token", "INTEGER NOT NULL DEFAULT 0");
|
|
160
176
|
ensurePiboRunColumn(this.db, "timeout_ms", "INTEGER");
|
|
161
177
|
ensurePiboRunColumn(this.db, "timeout_at", "TEXT");
|
|
162
178
|
ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
|
|
@@ -189,6 +205,21 @@ export class PiboReliabilityStore {
|
|
|
189
205
|
throw error;
|
|
190
206
|
}
|
|
191
207
|
}
|
|
208
|
+
hasDeliveryReceipt(deliveryId, projection) {
|
|
209
|
+
return this.db
|
|
210
|
+
.prepare("SELECT 1 AS found FROM pibo_delivery_receipts WHERE delivery_id = ? AND projection = ?")
|
|
211
|
+
.get(deliveryId, projection) !== undefined;
|
|
212
|
+
}
|
|
213
|
+
recordDeliveryReceipt(deliveryId, projection, deliveredAt = now()) {
|
|
214
|
+
this.db.prepare(`
|
|
215
|
+
INSERT OR IGNORE INTO pibo_delivery_receipts (delivery_id, projection, delivered_at)
|
|
216
|
+
VALUES (?, ?, ?)
|
|
217
|
+
`).run(deliveryId, projection, deliveredAt);
|
|
218
|
+
const row = this.db
|
|
219
|
+
.prepare("SELECT delivery_id, projection, delivered_at FROM pibo_delivery_receipts WHERE delivery_id = ? AND projection = ?")
|
|
220
|
+
.get(deliveryId, projection);
|
|
221
|
+
return { deliveryId: row.delivery_id, projection: row.projection, deliveredAt: row.delivered_at };
|
|
222
|
+
}
|
|
192
223
|
list(input = {}) {
|
|
193
224
|
const clauses = [];
|
|
194
225
|
const values = [];
|
|
@@ -323,56 +354,123 @@ export class PiboReliabilityStore {
|
|
|
323
354
|
claimBatch(workerId, limit, options = {}) {
|
|
324
355
|
const timestamp = now();
|
|
325
356
|
const claimExpiresAt = new Date(Date.now() + (options.visibilityTimeoutMs ?? 30000)).toISOString();
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
return this.inImmediateTransaction(() => {
|
|
358
|
+
const clauses = [
|
|
359
|
+
"((state = 'pending' AND run_at <= ?) OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
|
|
360
|
+
"(expires_at IS NULL OR expires_at > ?)",
|
|
361
|
+
"attempts < max_attempts",
|
|
362
|
+
];
|
|
363
|
+
const values = [timestamp, timestamp, timestamp];
|
|
364
|
+
if (options.queue) {
|
|
365
|
+
clauses.push("queue = ?");
|
|
366
|
+
values.push(options.queue);
|
|
367
|
+
}
|
|
368
|
+
this.moveExpiredJobs(timestamp);
|
|
369
|
+
this.moveExhaustedRecoverableJobs(timestamp, { queue: options.queue });
|
|
370
|
+
const rows = this.db
|
|
371
|
+
.prepare(`
|
|
372
|
+
UPDATE pibo_jobs
|
|
373
|
+
SET state = 'running',
|
|
374
|
+
worker_id = ?,
|
|
375
|
+
claim_expires_at = ?,
|
|
376
|
+
claim_token = claim_token + 1,
|
|
377
|
+
attempts = attempts + 1,
|
|
378
|
+
updated_at = ?
|
|
379
|
+
WHERE job_id IN (
|
|
380
|
+
SELECT job_id FROM pibo_jobs
|
|
381
|
+
WHERE ${clauses.join(" AND ")}
|
|
382
|
+
ORDER BY priority DESC, run_at ASC, created_at ASC
|
|
383
|
+
LIMIT ?
|
|
384
|
+
)
|
|
385
|
+
RETURNING *
|
|
386
|
+
`)
|
|
387
|
+
.all(workerId, claimExpiresAt, timestamp, ...values, clampLimit(limit));
|
|
388
|
+
return rows.map(jobFromRow);
|
|
389
|
+
});
|
|
354
390
|
}
|
|
355
391
|
claimJob(jobId, workerId, visibilityTimeoutMs = 30000) {
|
|
356
392
|
const timestamp = now();
|
|
357
393
|
const claimExpiresAt = new Date(Date.now() + visibilityTimeoutMs).toISOString();
|
|
358
|
-
|
|
359
|
-
.
|
|
394
|
+
return this.inImmediateTransaction(() => {
|
|
395
|
+
this.moveExpiredJobs(timestamp);
|
|
396
|
+
this.moveExhaustedRecoverableJobs(timestamp, { jobId });
|
|
397
|
+
const row = this.db
|
|
398
|
+
.prepare(`
|
|
399
|
+
UPDATE pibo_jobs
|
|
400
|
+
SET state = 'running',
|
|
401
|
+
worker_id = ?,
|
|
402
|
+
claim_expires_at = ?,
|
|
403
|
+
claim_token = claim_token + 1,
|
|
404
|
+
attempts = attempts + 1,
|
|
405
|
+
updated_at = ?
|
|
406
|
+
WHERE job_id = ?
|
|
407
|
+
AND state = 'pending'
|
|
408
|
+
AND run_at <= ?
|
|
409
|
+
AND attempts < max_attempts
|
|
410
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
411
|
+
RETURNING *
|
|
412
|
+
`)
|
|
413
|
+
.get(workerId, claimExpiresAt, timestamp, jobId, timestamp, timestamp);
|
|
414
|
+
return row ? jobFromRow(row) : undefined;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
claimRecoverableJob(jobId, workerId, visibilityTimeoutMs = 30000, forcePending = false) {
|
|
418
|
+
const timestamp = now();
|
|
419
|
+
const claimExpiresAt = new Date(Date.now() + visibilityTimeoutMs).toISOString();
|
|
420
|
+
return this.inImmediateTransaction(() => {
|
|
421
|
+
this.moveExpiredJobs(timestamp);
|
|
422
|
+
this.moveExhaustedRecoverableJobs(timestamp, { jobId });
|
|
423
|
+
const row = this.db.prepare(`
|
|
360
424
|
UPDATE pibo_jobs
|
|
361
425
|
SET state = 'running',
|
|
362
426
|
worker_id = ?,
|
|
363
427
|
claim_expires_at = ?,
|
|
428
|
+
claim_token = claim_token + 1,
|
|
364
429
|
attempts = attempts + 1,
|
|
365
430
|
updated_at = ?
|
|
366
431
|
WHERE job_id = ?
|
|
367
|
-
AND
|
|
368
|
-
|
|
432
|
+
AND (
|
|
433
|
+
(state = 'pending' AND (? = 1 OR run_at <= ?))
|
|
434
|
+
OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
435
|
+
)
|
|
436
|
+
AND attempts < max_attempts
|
|
369
437
|
AND (expires_at IS NULL OR expires_at > ?)
|
|
370
438
|
RETURNING *
|
|
371
|
-
`)
|
|
372
|
-
|
|
373
|
-
|
|
439
|
+
`).get(workerId, claimExpiresAt, timestamp, jobId, forcePending ? 1 : 0, timestamp, timestamp, timestamp);
|
|
440
|
+
return row ? jobFromRow(row) : undefined;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
updateJobPayload(jobId, workerId, payload, claimToken) {
|
|
444
|
+
const result = this.db.prepare(`
|
|
445
|
+
UPDATE pibo_jobs
|
|
446
|
+
SET payload_json = ?, updated_at = ?
|
|
447
|
+
WHERE job_id = ? AND state = 'running' AND worker_id = ?
|
|
448
|
+
AND (? IS NULL OR claim_token = ?)
|
|
449
|
+
AND claim_expires_at IS NOT NULL AND claim_expires_at > ?
|
|
450
|
+
`).run(json(payload), now(), jobId, workerId, claimToken ?? null, claimToken ?? null, now());
|
|
451
|
+
return Number(result.changes ?? 0) > 0;
|
|
374
452
|
}
|
|
375
|
-
|
|
453
|
+
releaseJob(jobId, workerId, delayMs = 0, claimToken) {
|
|
454
|
+
const timestamp = now();
|
|
455
|
+
return this.inImmediateTransaction(() => {
|
|
456
|
+
const row = this.getLiveWorkerJob(jobId, workerId, timestamp, claimToken);
|
|
457
|
+
if (!row)
|
|
458
|
+
return false;
|
|
459
|
+
if (row.attempts >= row.max_attempts) {
|
|
460
|
+
this.moveJobToDead(row, MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
const runAt = new Date(Date.parse(timestamp) + Math.max(0, delayMs)).toISOString();
|
|
464
|
+
const result = this.db.prepare(`
|
|
465
|
+
UPDATE pibo_jobs
|
|
466
|
+
SET state = 'pending', worker_id = NULL, claim_expires_at = NULL, run_at = ?, updated_at = ?
|
|
467
|
+
WHERE job_id = ? AND state = 'running' AND worker_id = ?
|
|
468
|
+
AND (? IS NULL OR claim_token = ?)
|
|
469
|
+
`).run(runAt, timestamp, jobId, workerId, claimToken ?? null, claimToken ?? null);
|
|
470
|
+
return Number(result.changes ?? 0) > 0;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
ack(jobId, workerId, claimToken) {
|
|
376
474
|
const timestamp = now();
|
|
377
475
|
const result = this.db
|
|
378
476
|
.prepare(`
|
|
@@ -380,47 +478,53 @@ export class PiboReliabilityStore {
|
|
|
380
478
|
WHERE job_id = ?
|
|
381
479
|
AND state = 'running'
|
|
382
480
|
AND worker_id = ?
|
|
481
|
+
AND (? IS NULL OR claim_token = ?)
|
|
383
482
|
AND claim_expires_at IS NOT NULL
|
|
384
483
|
AND claim_expires_at > ?
|
|
385
484
|
`)
|
|
386
|
-
.run(jobId, workerId, timestamp);
|
|
485
|
+
.run(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
|
|
387
486
|
return Number(result.changes ?? 0) > 0;
|
|
388
487
|
}
|
|
389
488
|
retry(jobId, workerId, input = {}) {
|
|
390
489
|
const timestamp = now();
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
490
|
+
return this.inImmediateTransaction(() => {
|
|
491
|
+
const row = this.getLiveWorkerJob(jobId, workerId, timestamp, input.claimToken);
|
|
492
|
+
if (!row)
|
|
493
|
+
return false;
|
|
494
|
+
const job = jobFromRow(row);
|
|
495
|
+
if (job.attempts >= job.maxAttempts) {
|
|
496
|
+
this.moveJobToDead(row, input.error ?? MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
const delayMs = input.delayMs ?? retryDelayMs(job.attempts, input);
|
|
500
|
+
const runAt = new Date(asDate(input.now).getTime() + delayMs).toISOString();
|
|
501
|
+
const result = this.db
|
|
502
|
+
.prepare(`
|
|
503
|
+
UPDATE pibo_jobs
|
|
504
|
+
SET state = 'pending',
|
|
505
|
+
worker_id = NULL,
|
|
506
|
+
claim_expires_at = NULL,
|
|
507
|
+
run_at = ?,
|
|
508
|
+
updated_at = ?,
|
|
509
|
+
last_error = ?
|
|
510
|
+
WHERE job_id = ? AND state = 'running' AND worker_id = ?
|
|
511
|
+
AND (? IS NULL OR claim_token = ?)
|
|
512
|
+
`)
|
|
513
|
+
.run(runAt, timestamp, input.error ?? null, jobId, workerId, input.claimToken ?? null, input.claimToken ?? null);
|
|
514
|
+
return Number(result.changes ?? 0) > 0;
|
|
515
|
+
});
|
|
414
516
|
}
|
|
415
|
-
fail(jobId, workerId, error) {
|
|
517
|
+
fail(jobId, workerId, error, claimToken) {
|
|
416
518
|
const timestamp = now();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
519
|
+
return this.inImmediateTransaction(() => {
|
|
520
|
+
const row = this.getLiveWorkerJob(jobId, workerId, timestamp, claimToken);
|
|
521
|
+
if (!row)
|
|
522
|
+
return false;
|
|
523
|
+
this.moveJobToDead(row, error, "failed", timestamp);
|
|
524
|
+
return true;
|
|
525
|
+
});
|
|
422
526
|
}
|
|
423
|
-
heartbeat(jobId, workerId, extendMs) {
|
|
527
|
+
heartbeat(jobId, workerId, extendMs, claimToken) {
|
|
424
528
|
const timestamp = now();
|
|
425
529
|
const claimExpiresAt = new Date(Date.now() + Math.max(1, extendMs)).toISOString();
|
|
426
530
|
const result = this.db
|
|
@@ -430,10 +534,11 @@ export class PiboReliabilityStore {
|
|
|
430
534
|
WHERE job_id = ?
|
|
431
535
|
AND state = 'running'
|
|
432
536
|
AND worker_id = ?
|
|
537
|
+
AND (? IS NULL OR claim_token = ?)
|
|
433
538
|
AND claim_expires_at IS NOT NULL
|
|
434
539
|
AND claim_expires_at > ?
|
|
435
540
|
`)
|
|
436
|
-
.run(claimExpiresAt, timestamp, jobId, workerId, timestamp);
|
|
541
|
+
.run(claimExpiresAt, timestamp, jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
|
|
437
542
|
return Number(result.changes ?? 0) > 0;
|
|
438
543
|
}
|
|
439
544
|
listJobs(input = {}) {
|
|
@@ -447,11 +552,56 @@ export class PiboReliabilityStore {
|
|
|
447
552
|
clauses.push("state = ?");
|
|
448
553
|
values.push(input.state);
|
|
449
554
|
}
|
|
555
|
+
if (input.excludeJobIds?.length) {
|
|
556
|
+
clauses.push(`job_id NOT IN (${input.excludeJobIds.map(() => "?").join(", ")})`);
|
|
557
|
+
values.push(...input.excludeJobIds);
|
|
558
|
+
}
|
|
450
559
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
451
560
|
const rows = this.db
|
|
452
561
|
.prepare(`SELECT * FROM pibo_jobs ${where} ORDER BY priority DESC, run_at ASC, created_at ASC LIMIT ?`)
|
|
453
562
|
.all(...values, clampLimit(input.limit, 100));
|
|
454
|
-
|
|
563
|
+
const jobs = [];
|
|
564
|
+
for (const row of rows) {
|
|
565
|
+
try {
|
|
566
|
+
jobs.push(jobFromRow(row));
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
this.quarantineJobRow(row, "payload_malformed");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return jobs;
|
|
573
|
+
}
|
|
574
|
+
listRecoverableJobs(input = {}) {
|
|
575
|
+
const timestamp = now();
|
|
576
|
+
const clauses = [
|
|
577
|
+
"((state = 'pending' AND (? = 1 OR run_at <= ?)) OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
|
|
578
|
+
"(expires_at IS NULL OR expires_at > ?)",
|
|
579
|
+
];
|
|
580
|
+
const values = [input.includeDeferredPending ? 1 : 0, timestamp, timestamp, timestamp];
|
|
581
|
+
if (input.queue) {
|
|
582
|
+
clauses.push("queue = ?");
|
|
583
|
+
values.push(input.queue);
|
|
584
|
+
}
|
|
585
|
+
if (input.excludeJobIds?.length) {
|
|
586
|
+
clauses.push(`job_id NOT IN (${input.excludeJobIds.map(() => "?").join(", ")})`);
|
|
587
|
+
values.push(...input.excludeJobIds);
|
|
588
|
+
}
|
|
589
|
+
const rows = this.db.prepare(`
|
|
590
|
+
SELECT * FROM pibo_jobs
|
|
591
|
+
WHERE ${clauses.join(" AND ")}
|
|
592
|
+
ORDER BY priority DESC, run_at ASC, created_at ASC
|
|
593
|
+
LIMIT ?
|
|
594
|
+
`).all(...values, clampLimit(input.limit, 100));
|
|
595
|
+
const jobs = [];
|
|
596
|
+
for (const row of rows) {
|
|
597
|
+
try {
|
|
598
|
+
jobs.push(jobFromRow(row));
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
this.quarantineJobRow(row, "payload_malformed");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return jobs;
|
|
455
605
|
}
|
|
456
606
|
listDead(input = {}) {
|
|
457
607
|
const clauses = [];
|
|
@@ -466,6 +616,51 @@ export class PiboReliabilityStore {
|
|
|
466
616
|
.all(...values, clampLimit(input.limit, 100));
|
|
467
617
|
return rows.map(deadJobFromRow);
|
|
468
618
|
}
|
|
619
|
+
hasLiveJob(jobId) {
|
|
620
|
+
return this.db.prepare("SELECT 1 AS found FROM pibo_jobs WHERE job_id = ?").get(jobId) !== undefined;
|
|
621
|
+
}
|
|
622
|
+
hasJobs(queue) {
|
|
623
|
+
return queue
|
|
624
|
+
? this.db.prepare("SELECT 1 AS found FROM pibo_jobs WHERE queue = ? LIMIT 1").get(queue) !== undefined
|
|
625
|
+
: this.db.prepare("SELECT 1 AS found FROM pibo_jobs LIMIT 1").get() !== undefined;
|
|
626
|
+
}
|
|
627
|
+
hasRecoverableJobs(queue, includeDeferredPending = false) {
|
|
628
|
+
const timestamp = now();
|
|
629
|
+
return this.inImmediateTransaction(() => {
|
|
630
|
+
this.moveExpiredJobs(timestamp);
|
|
631
|
+
this.moveExhaustedRecoverableJobs(timestamp, { queue });
|
|
632
|
+
return this.db.prepare(`
|
|
633
|
+
SELECT 1 AS found FROM pibo_jobs
|
|
634
|
+
WHERE queue = ?
|
|
635
|
+
AND (
|
|
636
|
+
(state = 'pending' AND (? = 1 OR run_at <= ?))
|
|
637
|
+
OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
638
|
+
)
|
|
639
|
+
AND attempts < max_attempts
|
|
640
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
641
|
+
LIMIT 1
|
|
642
|
+
`).get(queue, includeDeferredPending ? 1 : 0, timestamp, timestamp, timestamp) !== undefined;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
quarantineJob(jobId, reason, correlation = {}) {
|
|
646
|
+
const row = this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(jobId);
|
|
647
|
+
if (!row)
|
|
648
|
+
return false;
|
|
649
|
+
this.quarantineJobRow(row, reason, correlation);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
deadLetter(input) {
|
|
653
|
+
const timestamp = now();
|
|
654
|
+
const jobId = input.jobId ?? `job_${randomUUID()}`;
|
|
655
|
+
this.db.prepare(`
|
|
656
|
+
INSERT OR REPLACE INTO pibo_dead_jobs (
|
|
657
|
+
job_id, queue, payload_json, attempts, max_attempts, idempotency_key, created_at,
|
|
658
|
+
updated_at, expires_at, last_error, dead_at, dead_reason
|
|
659
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
660
|
+
`).run(jobId, input.queue, json(input.payload ?? null), Math.max(0, input.attempts ?? 0), Math.max(1, input.maxAttempts ?? 1), input.idempotencyKey ?? null, timestamp, timestamp, input.error ?? input.reason, timestamp, input.reason);
|
|
661
|
+
const row = this.db.prepare("SELECT * FROM pibo_dead_jobs WHERE job_id = ?").get(jobId);
|
|
662
|
+
return deadJobFromRow(row);
|
|
663
|
+
}
|
|
469
664
|
requeueDead(jobId, input = {}) {
|
|
470
665
|
const dead = this.db.prepare("SELECT * FROM pibo_dead_jobs WHERE job_id = ?").get(jobId);
|
|
471
666
|
if (!dead)
|
|
@@ -583,46 +778,62 @@ export class PiboReliabilityStore {
|
|
|
583
778
|
return Number(result.changes ?? 0);
|
|
584
779
|
}
|
|
585
780
|
recoverInterruptedRuns(workerId = `run-registry:${process.pid}`) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
this.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
781
|
+
return this.inImmediateTransaction(() => {
|
|
782
|
+
const rows = this.db.prepare("SELECT * FROM pibo_runs WHERE status = 'running'").all();
|
|
783
|
+
const recovered = [];
|
|
784
|
+
const timestamp = now();
|
|
785
|
+
for (const row of rows) {
|
|
786
|
+
if (row.job_id && this.hasUnexpiredJobClaim(row.job_id, timestamp, workerId))
|
|
787
|
+
continue;
|
|
788
|
+
const run = runFromRow(row);
|
|
789
|
+
if (run.timeoutAt && run.timeoutAt <= timestamp) {
|
|
790
|
+
const error = `Run deadline ${run.timeoutAt} elapsed before the interrupted runtime recovered.`;
|
|
791
|
+
if (run.jobId)
|
|
792
|
+
this.moveLiveJobToDead(run.jobId, error, "timeout", timestamp);
|
|
793
|
+
recovered.push(this.updateRun(run.runId, {
|
|
794
|
+
status: "timed_out",
|
|
795
|
+
error,
|
|
796
|
+
timeoutPhase: "lifetime",
|
|
797
|
+
summary: `${run.toolName} run started successfully, then reached its configured timeout.`,
|
|
798
|
+
completedAt: timestamp,
|
|
799
|
+
}) ?? run);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const jobRow = run.jobId
|
|
803
|
+
? this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(run.jobId)
|
|
804
|
+
: undefined;
|
|
805
|
+
if (run.retryable && jobRow && jobRow.attempts < jobRow.max_attempts) {
|
|
806
|
+
this.releaseJobForRetry(jobRow.job_id, timestamp);
|
|
807
|
+
recovered.push(this.updateRun(run.runId, {
|
|
808
|
+
status: "queued",
|
|
809
|
+
summary: `${run.toolName} run is queued for retry after interruption.`,
|
|
810
|
+
}) ?? run);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (run.retryable && jobRow && jobRow.attempts >= jobRow.max_attempts) {
|
|
814
|
+
this.moveJobToDead(jobRow, MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
|
|
815
|
+
recovered.push(this.updateRun(run.runId, {
|
|
816
|
+
status: "failed",
|
|
817
|
+
error: MAX_ATTEMPTS_ERROR,
|
|
818
|
+
summary: `${run.toolName} run failed after exhausting retry attempts.`,
|
|
819
|
+
completedAt: timestamp,
|
|
820
|
+
}) ?? run);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const error = run.retryable
|
|
824
|
+
? "Run was interrupted before completion and its retry job is no longer available."
|
|
825
|
+
: "Run was interrupted before completion and the tool is not retryable.";
|
|
826
|
+
if (jobRow)
|
|
827
|
+
this.moveJobToDead(jobRow, error, "interrupted", timestamp);
|
|
617
828
|
recovered.push(this.updateRun(run.runId, {
|
|
618
829
|
status: "failed",
|
|
619
|
-
error
|
|
830
|
+
error,
|
|
620
831
|
summary: `${run.toolName} run failed.`,
|
|
621
832
|
completedAt: timestamp,
|
|
622
833
|
}) ?? run);
|
|
623
834
|
}
|
|
624
|
-
|
|
625
|
-
|
|
835
|
+
return recovered;
|
|
836
|
+
});
|
|
626
837
|
}
|
|
627
838
|
close() {
|
|
628
839
|
this.db.close();
|
|
@@ -659,23 +870,42 @@ export class PiboReliabilityStore {
|
|
|
659
870
|
.get(queue, idempotencyKey);
|
|
660
871
|
return row ? jobFromRow(row) : undefined;
|
|
661
872
|
}
|
|
662
|
-
getLiveWorkerJob(jobId, workerId, timestamp) {
|
|
873
|
+
getLiveWorkerJob(jobId, workerId, timestamp, claimToken) {
|
|
663
874
|
return this.db
|
|
664
875
|
.prepare(`
|
|
665
876
|
SELECT * FROM pibo_jobs
|
|
666
877
|
WHERE job_id = ?
|
|
667
878
|
AND state = 'running'
|
|
668
879
|
AND worker_id = ?
|
|
880
|
+
AND (? IS NULL OR claim_token = ?)
|
|
669
881
|
AND claim_expires_at IS NOT NULL
|
|
670
882
|
AND claim_expires_at > ?
|
|
671
883
|
`)
|
|
672
|
-
.get(jobId, workerId, timestamp);
|
|
884
|
+
.get(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
|
|
673
885
|
}
|
|
674
886
|
moveExpiredJobs(timestamp) {
|
|
675
887
|
const expired = this.db.prepare("SELECT * FROM pibo_jobs WHERE expires_at IS NOT NULL AND expires_at <= ?").all(timestamp);
|
|
676
888
|
for (const row of expired)
|
|
677
889
|
this.moveJobToDead(row, row.last_error ?? "Job expired.", "expired", timestamp);
|
|
678
890
|
}
|
|
891
|
+
moveExhaustedRecoverableJobs(timestamp, options = {}) {
|
|
892
|
+
const clauses = [
|
|
893
|
+
"attempts >= max_attempts",
|
|
894
|
+
"(state = 'pending' OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
|
|
895
|
+
];
|
|
896
|
+
const values = [timestamp];
|
|
897
|
+
if (options.jobId) {
|
|
898
|
+
clauses.push("job_id = ?");
|
|
899
|
+
values.push(options.jobId);
|
|
900
|
+
}
|
|
901
|
+
if (options.queue) {
|
|
902
|
+
clauses.push("queue = ?");
|
|
903
|
+
values.push(options.queue);
|
|
904
|
+
}
|
|
905
|
+
const exhausted = this.db.prepare(`SELECT * FROM pibo_jobs WHERE ${clauses.join(" AND ")}`).all(...values);
|
|
906
|
+
for (const row of exhausted)
|
|
907
|
+
this.moveJobToDead(row, MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
|
|
908
|
+
}
|
|
679
909
|
hasUnexpiredJobClaim(jobId, timestamp, workerId) {
|
|
680
910
|
const row = this.db
|
|
681
911
|
.prepare(`
|
|
@@ -718,6 +948,39 @@ export class PiboReliabilityStore {
|
|
|
718
948
|
.run(row.job_id, row.queue, row.payload_json, row.attempts, row.max_attempts, row.idempotency_key, row.created_at, timestamp, row.expires_at, error, timestamp, reason);
|
|
719
949
|
this.db.prepare("DELETE FROM pibo_jobs WHERE job_id = ?").run(row.job_id);
|
|
720
950
|
}
|
|
951
|
+
quarantineJobRow(row, reason, correlation = {}) {
|
|
952
|
+
this.inImmediateTransaction(() => {
|
|
953
|
+
const current = this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(row.job_id);
|
|
954
|
+
if (!current)
|
|
955
|
+
return;
|
|
956
|
+
const payload = {
|
|
957
|
+
version: 1,
|
|
958
|
+
key: correlation.key ?? current.job_id,
|
|
959
|
+
...(correlation.piboSessionId ? { piboSessionId: correlation.piboSessionId } : {}),
|
|
960
|
+
...(correlation.eventId ? { eventId: correlation.eventId } : {}),
|
|
961
|
+
state: { phase: "quarantined", reasonCode: reason },
|
|
962
|
+
};
|
|
963
|
+
const sanitized = {
|
|
964
|
+
...current,
|
|
965
|
+
payload_json: json(payload),
|
|
966
|
+
idempotency_key: correlation.key ?? null,
|
|
967
|
+
last_error: reason,
|
|
968
|
+
};
|
|
969
|
+
this.moveJobToDead(sanitized, reason, reason, now());
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
inImmediateTransaction(operation) {
|
|
973
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
974
|
+
try {
|
|
975
|
+
const result = operation();
|
|
976
|
+
this.db.exec("COMMIT");
|
|
977
|
+
return result;
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
this.db.exec("ROLLBACK");
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
721
984
|
requireRun(runId) {
|
|
722
985
|
const run = this.getRun(runId);
|
|
723
986
|
if (!run)
|
|
@@ -750,6 +1013,7 @@ function jobFromRow(row) {
|
|
|
750
1013
|
priority: row.priority,
|
|
751
1014
|
workerId: row.worker_id ?? undefined,
|
|
752
1015
|
claimExpiresAt: row.claim_expires_at ?? undefined,
|
|
1016
|
+
claimToken: row.claim_token,
|
|
753
1017
|
attempts: row.attempts,
|
|
754
1018
|
maxAttempts: row.max_attempts,
|
|
755
1019
|
idempotencyKey: row.idempotency_key ?? undefined,
|
|
@@ -763,7 +1027,7 @@ function deadJobFromRow(row) {
|
|
|
763
1027
|
return {
|
|
764
1028
|
jobId: row.job_id,
|
|
765
1029
|
queue: row.queue,
|
|
766
|
-
payload:
|
|
1030
|
+
payload: safeDeadPayload(row.payload_json, row.dead_reason, row.job_id),
|
|
767
1031
|
attempts: row.attempts,
|
|
768
1032
|
maxAttempts: row.max_attempts,
|
|
769
1033
|
idempotencyKey: row.idempotency_key ?? undefined,
|
|
@@ -775,6 +1039,14 @@ function deadJobFromRow(row) {
|
|
|
775
1039
|
deadReason: row.dead_reason,
|
|
776
1040
|
};
|
|
777
1041
|
}
|
|
1042
|
+
function safeDeadPayload(value, reason, jobId) {
|
|
1043
|
+
try {
|
|
1044
|
+
return parseJson(value);
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
return { version: 1, key: jobId, state: { phase: "quarantined", reasonCode: reason } };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
778
1050
|
function runFromRow(row) {
|
|
779
1051
|
const output = {
|
|
780
1052
|
runId: row.run_id,
|