@pasko70/pibo 3.4.4 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runtime/routed-session.js +111 -25
- package/dist/agent-runtimes/codex-native/turn.js +27 -3
- package/dist/agent-runtimes/pi/adapter.js +1 -0
- package/dist/agent-runtimes/pi/routed-session.js +2 -2
- package/dist/apps/chat/bounded-event-stream.js +98 -0
- package/dist/apps/chat/chat-settings-routes.js +3 -3
- package/dist/apps/chat/data/chat-data-mappers.js +40 -14
- package/dist/apps/chat/data/event-command-service.js +30 -21
- package/dist/apps/chat/data/history-query-service.js +34 -25
- package/dist/apps/chat/data/read-state-service.js +37 -3
- package/dist/apps/chat/data/session-query-service.js +19 -11
- package/dist/apps/chat/data/timeline-query-service.js +19 -7
- package/dist/apps/chat/message-command-dispatcher.js +149 -0
- package/dist/apps/chat/output-compactor.js +9 -0
- package/dist/apps/chat/output-event-policy.js +9 -1
- package/dist/apps/chat/stream.js +21 -3
- package/dist/apps/chat/telemetry-retention-service.js +113 -8
- package/dist/apps/chat/trace-response-cache.js +46 -0
- package/dist/apps/chat/trace-v2.js +12 -6
- package/dist/apps/chat/trace.js +1 -1
- package/dist/apps/chat/web-app.js +645 -297
- package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
- package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
- package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli-session/localSessionSource.js +3 -2
- package/dist/compute/pool/seeds.js +25 -3
- package/dist/core/events.js +4 -0
- package/dist/core/output-render-sequence.js +65 -6
- package/dist/core/provider-capacity.js +33 -0
- package/dist/core/provider-telemetry.js +21 -6
- package/dist/core/runtime-capacity.js +174 -0
- package/dist/core/runtime-telemetry.js +40 -12
- package/dist/core/session-router.js +178 -13
- package/dist/data/async-chat-reads.js +38 -0
- package/dist/data/async-chat-storage.js +96 -0
- package/dist/data/async-telemetry-maintenance.js +9 -0
- package/dist/data/bounded-worker-client.js +256 -0
- package/dist/data/chat-read-projections.js +159 -0
- package/dist/data/chat-read-worker.js +73 -0
- package/dist/data/chat-storage-worker.js +167 -0
- package/dist/data/ingest-service.js +152 -18
- package/dist/data/message-command-store.js +290 -0
- package/dist/data/payload-store.js +92 -11
- package/dist/data/pibo-store.js +10 -8
- package/dist/data/schema.js +39 -4
- package/dist/data/session-store.js +3 -1
- package/dist/data/storage-backup.js +278 -0
- package/dist/data/storage-maintenance.js +344 -0
- package/dist/data/storage-verification-worker.js +25 -0
- package/dist/data/telemetry-capture.js +188 -0
- package/dist/data/telemetry-command.js +3 -0
- package/dist/data/telemetry-maintenance-worker.js +40 -0
- package/dist/data/telemetry-maintenance.js +110 -0
- package/dist/data/telemetry-retention.js +16 -7
- package/dist/data/telemetry-worker.js +111 -0
- package/dist/data/telemetry-writer.js +150 -83
- package/dist/data/telemetry.js +5 -0
- package/dist/debug/index.js +207 -1
- package/dist/debug/message-queue.js +108 -0
- package/dist/debug/output-collision-repair.js +140 -0
- package/dist/debug/output-integrity.js +38 -2
- package/dist/debug/output-repair.js +1 -0
- package/dist/debug/storage-backup.js +41 -0
- package/dist/debug/storage-maintenance.js +78 -0
- package/dist/debug/telemetry-capture.js +66 -0
- package/dist/gateway/cli.js +71 -7
- package/dist/gateway/server.js +3 -0
- package/dist/providers/openai-gpt56.js +11 -6
- package/dist/reliability/store.js +119 -23
- package/dist/session-ui/terminalRows.js +54 -13
- package/dist/sessions/pibo-data-store.js +19 -14
- package/dist/shared/debug-features.js +4 -0
- package/dist/shared/model-inference-metrics.js +23 -0
- package/dist/shared/trace-event-projection.js +69 -2
- package/dist/shared/trace-history.js +9 -0
- package/dist/shared/trace-live-reducer.js +1 -0
- package/dist/shared/trace-patch-nodes.js +19 -0
- package/dist/web/channel.js +117 -12
- package/dist/web/http.js +135 -41
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -1
- package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { createStorageBackup, readStorageBackupManifest, verifyStorageBackup, restoreStorageBackup } from "../data/storage-backup.js";
|
|
3
|
+
import { recordStorageMaintenance } from "../data/storage-maintenance.js";
|
|
4
|
+
export async function runStorageBackupCli(args) {
|
|
5
|
+
const action = args[0];
|
|
6
|
+
if (!action || args.includes("--help") || args.includes("-h")) {
|
|
7
|
+
console.log(`pibo debug backup - explicit SQLite snapshot and payload backup
|
|
8
|
+
Commands:
|
|
9
|
+
create --source <sqlite> --payload-root <dir> --destination <new-dir>
|
|
10
|
+
[--resume] [--max-bytes <n>] [--max-payloads <n>] [--max-ms <n>] [--max-wal-growth <n>]
|
|
11
|
+
inspect --archive <dir> Read the manifest only
|
|
12
|
+
verify --archive <dir> Verify database and all payload hashes
|
|
13
|
+
restore --archive <dir> --destination <new-dir>
|
|
14
|
+
Defaults: 1 GiB data, 100000 payloads, 60000 ms and 64 MiB extra source WAL per create attempt. Resume preserves the original snapshot.
|
|
15
|
+
Each archive contains one database snapshot. Product and reliability snapshots are separate cuts.
|
|
16
|
+
No source deletion, vacuum, or overwrite of an existing restore destination.`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const { values } = parseArgs({ args: args.slice(1), options: { source: { type: "string" }, "payload-root": { type: "string" }, destination: { type: "string" }, archive: { type: "string" }, resume: { type: "boolean" }, "max-bytes": { type: "string" }, "max-payloads": { type: "string" }, "max-ms": { type: "string" }, "max-wal-growth": { type: "string" }, json: { type: "boolean" } } });
|
|
20
|
+
const required = (name) => { const value = values[name]; if (typeof value !== "string" || !value)
|
|
21
|
+
throw Error(`Missing --${name}`); return value; };
|
|
22
|
+
let result;
|
|
23
|
+
if (action === "create") {
|
|
24
|
+
const source = required("source");
|
|
25
|
+
result = await createStorageBackup({ source, payloadRoot: required("payload-root"), destination: required("destination"), resume: values.resume, maxBytes: values["max-bytes"] ? Number(values["max-bytes"]) : undefined, maxPayloads: values["max-payloads"] ? Number(values["max-payloads"]) : undefined, maxMilliseconds: values["max-ms"] ? Number(values["max-ms"]) : undefined, maxWalGrowthBytes: values["max-wal-growth"] ? Number(values["max-wal-growth"]) : undefined });
|
|
26
|
+
recordStorageMaintenance(source, { operation: "backup", at: new Date().toISOString(), status: result.status, destination: required("destination") });
|
|
27
|
+
}
|
|
28
|
+
else if (action === "inspect")
|
|
29
|
+
result = await readStorageBackupManifest(required("archive"));
|
|
30
|
+
else if (action === "verify") {
|
|
31
|
+
const archive = required("archive");
|
|
32
|
+
result = await verifyStorageBackup(archive, AbortSignal.timeout(60000));
|
|
33
|
+
const manifest = await readStorageBackupManifest(archive);
|
|
34
|
+
recordStorageMaintenance(manifest.source, { operation: "backup", at: new Date().toISOString(), status: "verified", archive });
|
|
35
|
+
}
|
|
36
|
+
else if (action === "restore")
|
|
37
|
+
result = await restoreStorageBackup(required("archive"), required("destination"), AbortSignal.timeout(60000));
|
|
38
|
+
else
|
|
39
|
+
throw Error("Unknown backup action; use backup --help");
|
|
40
|
+
console.log(JSON.stringify(result, null, 2));
|
|
41
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { piboHomePath } from "../core/pibo-home.js";
|
|
3
|
+
import { checkpointStorage, inspectStorageStatus, maintainStorageRetention, verifyStorage } from "../data/storage-maintenance.js";
|
|
4
|
+
import { resolveDebugStore } from "./stores.js";
|
|
5
|
+
export async function runStorageMaintenanceCli(args) {
|
|
6
|
+
const action = args[0];
|
|
7
|
+
if (!action || args.includes("--help") || args.includes("-h")) {
|
|
8
|
+
printHelp();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const { values } = parseArgs({ args: args.slice(1), strict: true, options: {
|
|
12
|
+
store: { type: "string" }, path: { type: "string" }, json: { type: "boolean" },
|
|
13
|
+
"timeout-ms": { type: "string" }, full: { type: "boolean" }, quick: { type: "boolean" },
|
|
14
|
+
apply: { type: "boolean" }, "dry-run": { type: "boolean" }, mode: { type: "string" },
|
|
15
|
+
before: { type: "string" }, limit: { type: "string" }, "payload-root": { type: "string" },
|
|
16
|
+
"database-warn-bytes": { type: "string" }, "wal-warn-bytes": { type: "string" }, "payload-warn-bytes": { type: "string" },
|
|
17
|
+
} });
|
|
18
|
+
if (values.apply && values["dry-run"])
|
|
19
|
+
throw new Error("Choose either --dry-run or --apply");
|
|
20
|
+
const path = typeof values.path === "string" ? values.path : resolveStorePath(values.store);
|
|
21
|
+
let result;
|
|
22
|
+
if (action === "status" || action === "doctor") {
|
|
23
|
+
result = inspectStorageStatus({ path, databaseWarnBytes: numberValue(values["database-warn-bytes"]), walWarnBytes: numberValue(values["wal-warn-bytes"]), payloadWarnBytes: numberValue(values["payload-warn-bytes"]) });
|
|
24
|
+
}
|
|
25
|
+
else if (action === "verify") {
|
|
26
|
+
if (values.full && values.quick)
|
|
27
|
+
throw new Error("Choose either --quick or --full");
|
|
28
|
+
result = await verifyStorage({ path, mode: values.full ? "full" : "quick", timeoutMs: numberValue(values["timeout-ms"]) });
|
|
29
|
+
}
|
|
30
|
+
else if (action === "checkpoint") {
|
|
31
|
+
const mode = values.mode ?? "passive";
|
|
32
|
+
if (!new Set(["passive", "restart", "truncate"]).has(mode))
|
|
33
|
+
throw new Error("Checkpoint --mode must be passive, restart, or truncate");
|
|
34
|
+
result = checkpointStorage({ path, mode: mode, apply: values.apply });
|
|
35
|
+
}
|
|
36
|
+
else if (action === "retention") {
|
|
37
|
+
if (typeof values.before !== "string")
|
|
38
|
+
throw new Error("Storage retention requires --before <iso-date>");
|
|
39
|
+
result = await maintainStorageRetention({ path, before: values.before, limit: numberValue(values.limit), apply: values.apply, payloadRoot: typeof values["payload-root"] === "string" ? values["payload-root"] : piboHomePath("payloads") });
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
throw new Error(`Unknown storage action "${action}"; run pibo debug storage --help`);
|
|
43
|
+
if (values.json)
|
|
44
|
+
console.log(JSON.stringify(result, null, 2));
|
|
45
|
+
else
|
|
46
|
+
console.log(formatText(result));
|
|
47
|
+
}
|
|
48
|
+
function resolveStorePath(value) {
|
|
49
|
+
if (!value || value === "pibo-data")
|
|
50
|
+
return resolveDebugStore("pibo-data").path;
|
|
51
|
+
if (value === "reliability")
|
|
52
|
+
return resolveDebugStore("reliability").path;
|
|
53
|
+
throw new Error("--store must be pibo-data or reliability; use --path for another SQLite store");
|
|
54
|
+
}
|
|
55
|
+
function numberValue(value) { if (value === undefined)
|
|
56
|
+
return undefined; const parsed = Number(value); if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
57
|
+
throw new Error(`Invalid numeric value "${value}"`); return parsed; }
|
|
58
|
+
function formatText(value) { const object = value; const lines = [`pibo debug storage ${String(object.resultType ?? "result").split(".").at(-1)}`]; for (const [key, item] of Object.entries(object)) {
|
|
59
|
+
if (key === "resultType" || typeof item === "object")
|
|
60
|
+
continue;
|
|
61
|
+
lines.push(`${key}\t${String(item)}`);
|
|
62
|
+
} if (Array.isArray(object.warnings))
|
|
63
|
+
lines.push(...object.warnings.map((warning) => `warning\t${String(warning)}`)); lines.push("", "Use --json for bounded row, size, projection, and progress detail."); return lines.join("\n"); }
|
|
64
|
+
function printHelp() {
|
|
65
|
+
console.log(`pibo debug storage - bounded SQLite health and maintenance
|
|
66
|
+
|
|
67
|
+
Commands:
|
|
68
|
+
status [--store pibo-data|reliability | --path <sqlite>] [--json]
|
|
69
|
+
doctor [--store pibo-data|reliability | --path <sqlite>] [--json]
|
|
70
|
+
verify [--quick|--full] [--timeout-ms <n>] [--store <name>|--path <sqlite>] [--json]
|
|
71
|
+
checkpoint [--mode passive|restart|truncate] [--dry-run|--apply] [--store <name>|--path <sqlite>] [--json]
|
|
72
|
+
retention --before <iso-date> [--limit <1..10000>] [--dry-run|--apply] [--payload-root <dir>] [--store <name>|--path <sqlite>] [--json]
|
|
73
|
+
|
|
74
|
+
Status is read-only and reports DB/WAL/SHM/payload size, pages/freelist, bounded row counts, payload-reference integrity, maintenance metadata, and degraded thresholds.
|
|
75
|
+
Verification runs in a cancellable worker. Timeout is partial and never healthy. Quick/full checks can still be I/O intensive; use backup verification or offline checks when the online budget is insufficient.
|
|
76
|
+
Checkpoint and retention default to dry-run. Apply is bounded and audited in the database maintenance sidecar. Retention deletes only eligible live_delta rows; chat messages, audit events, idempotency evidence, and referenced payloads are preserved.
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { opendir } from "node:fs/promises";
|
|
4
|
+
import { resolveDebugStore } from "./stores.js";
|
|
5
|
+
import { startTelemetryCapture, inspectTelemetryCapture, finalizeTelemetryCapture, readTelemetryCapturePage } from "../data/telemetry-capture.js";
|
|
6
|
+
export async function runTelemetryCaptureCli(args) {
|
|
7
|
+
const action = args[0];
|
|
8
|
+
if (!action || args.includes("--help") || args.includes("-h")) {
|
|
9
|
+
console.log(`pibo debug telemetry capture - scoped provider metadata in isolated stores
|
|
10
|
+
Commands:
|
|
11
|
+
start --session <ps_...> --owner <name> --duration-ms <n> --max-bytes <n> --max-rows <n>
|
|
12
|
+
stop <capture-id> Fence appends and finalize an independent closed archive
|
|
13
|
+
list List up to 100 manifests without opening databases
|
|
14
|
+
inspect <capture-id> Read one manifest without opening its database
|
|
15
|
+
page <capture-id> [--after <sequence>] [--limit <1..100>]
|
|
16
|
+
Limits: one active capture, at most one hour, 64 MiB logical data and 100000 rows per run.
|
|
17
|
+
Detail is allow-listed provider metadata only. Arbitrary provider bodies, credentials and prompts are excluded.
|
|
18
|
+
Stopping/finalizing is explicit; archived data is never opened by normal gateway reads.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { session: { type: "string" }, owner: { type: "string" }, "duration-ms": { type: "string" }, "max-bytes": { type: "string" }, "max-rows": { type: "string" }, after: { type: "string" }, limit: { type: "string" }, json: { type: "boolean" } } });
|
|
22
|
+
const root = join(dirname(resolveDebugStore("pibo-data").path), "telemetry-captures");
|
|
23
|
+
let result;
|
|
24
|
+
if (action === "start")
|
|
25
|
+
result = startTelemetryCapture(root, { sessionId: values.session ?? "", owner: values.owner ?? "", durationMs: Number(values["duration-ms"]), maxBytes: Number(values["max-bytes"]), maxRows: Number(values["max-rows"]) });
|
|
26
|
+
else if (action === "list") {
|
|
27
|
+
const entries = [];
|
|
28
|
+
let scanned = 0, truncated = false;
|
|
29
|
+
try {
|
|
30
|
+
const directory = await opendir(root);
|
|
31
|
+
for await (const entry of directory) {
|
|
32
|
+
if (++scanned > 1000 || entries.length >= 100) {
|
|
33
|
+
truncated = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (!entry.isDirectory() || !entry.name.startsWith("capture_"))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
entries.push(inspectTelemetryCapture(root, entry.name));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
entries.push({ id: entry.name, status: "manifest_unavailable" });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT")
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
result = { entries, truncated };
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const id = positionals[0];
|
|
54
|
+
if (!id)
|
|
55
|
+
throw Error("Capture id is required");
|
|
56
|
+
if (action === "stop")
|
|
57
|
+
result = await finalizeTelemetryCapture(root, id);
|
|
58
|
+
else if (action === "inspect")
|
|
59
|
+
result = inspectTelemetryCapture(root, id);
|
|
60
|
+
else if (action === "page")
|
|
61
|
+
result = await readTelemetryCapturePage(root, id, Number(values.after ?? 0), Number(values.limit ?? 50));
|
|
62
|
+
else
|
|
63
|
+
throw Error("Unknown capture action; use capture --help");
|
|
64
|
+
}
|
|
65
|
+
console.log(JSON.stringify(result, null, 2));
|
|
66
|
+
}
|
package/dist/gateway/cli.js
CHANGED
|
@@ -231,14 +231,40 @@ function activeRun(value) {
|
|
|
231
231
|
piboSessionId: stringValue(obj.controllerPiboSessionId) ?? stringValue(obj.piboSessionId),
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
|
+
function runJobReliability(value) {
|
|
235
|
+
const obj = objectValue(value);
|
|
236
|
+
if (!obj || (obj.status !== "ok" && obj.status !== "degraded")
|
|
237
|
+
|| !Number.isInteger(obj.expiredOrphanRunJobs) || Number(obj.expiredOrphanRunJobs) < 0
|
|
238
|
+
|| !Number.isInteger(obj.orphanRunDeadLetters) || Number(obj.orphanRunDeadLetters) < 0)
|
|
239
|
+
return undefined;
|
|
240
|
+
return {
|
|
241
|
+
status: obj.status,
|
|
242
|
+
expiredOrphanRunJobs: Number(obj.expiredOrphanRunJobs),
|
|
243
|
+
orphanRunDeadLetters: Number(obj.orphanRunDeadLetters),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function durableMessageQueueStatus(value) {
|
|
247
|
+
const obj = objectValue(value);
|
|
248
|
+
if (!obj)
|
|
249
|
+
return undefined;
|
|
250
|
+
const status = obj.status === "healthy" || obj.status === "degraded" || obj.status === "ambiguous" ? obj.status : undefined;
|
|
251
|
+
const storage = objectValue(obj.storage);
|
|
252
|
+
const counts = Array.isArray(obj.counts) ? obj.counts.flatMap(value => { const row = objectValue(value); return row ? [{ state: stringValue(row.state), delivery: stringValue(row.delivery), count: numberValue(row.count), bytes: numberValue(row.bytes) }] : []; }) : undefined;
|
|
253
|
+
const affectedScopes = Array.isArray(obj.affectedScopes) ? obj.affectedScopes.flatMap(value => { const row = objectValue(value); return row ? [{ sessionId: stringValue(row.sessionId), roomId: stringValue(row.roomId), blockingCommandId: stringValue(row.blockingCommandId), blockedSince: numberValue(row.blockedSince), blockedSuccessors: numberValue(row.blockedSuccessors) }] : []; }) : undefined;
|
|
254
|
+
return { status, storage: storage ? { available: booleanValue(storage.available), error: stringValue(storage.error) } : undefined, counts, interruptedPredecessors: numberValue(obj.interruptedPredecessors), blockedSuccessors: numberValue(obj.blockedSuccessors), expiredOwnedLeases: numberValue(obj.expiredOwnedLeases), oldestDispatchableWaitMs: numberValue(obj.oldestDispatchableWaitMs), oldestBlockedWaitMs: numberValue(obj.oldestBlockedWaitMs), affectedScopes, degradedReasons: Array.isArray(obj.degradedReasons) && obj.degradedReasons.every(v => typeof v === "string") ? obj.degradedReasons : undefined, admissionCapacity: obj.admissionCapacity, bounded: obj.bounded };
|
|
255
|
+
}
|
|
234
256
|
function parseGatewaySafetyPayload(payload, reachable) {
|
|
235
257
|
const obj = objectValue(payload);
|
|
236
258
|
const mode = obj && (obj.mode === "dev" || obj.mode === "prod" || obj.mode === "fallback") ? obj.mode : "unknown";
|
|
237
259
|
const runtimeStatuses = Array.isArray(obj?.runtimeStatuses) ? obj.runtimeStatuses.map(runtimeStatus).filter((item) => Boolean(item)) : [];
|
|
238
260
|
const activeRuns = Array.isArray(obj?.activeRuns) ? obj.activeRuns.map(activeRun).filter((item) => Boolean(item)) : [];
|
|
261
|
+
const reliability = obj?.reliability === undefined ? undefined : runJobReliability(obj.reliability);
|
|
262
|
+
const durableMessageQueue = durableMessageQueueStatus(obj?.durableMessageQueue);
|
|
239
263
|
const incomplete = !Array.isArray(obj?.runtimeStatuses) || !Array.isArray(obj?.activeRuns)
|
|
240
|
-
|| runtimeStatuses.length !== obj.runtimeStatuses.length || activeRuns.length !== obj.activeRuns.length
|
|
241
|
-
|
|
264
|
+
|| runtimeStatuses.length !== obj.runtimeStatuses.length || activeRuns.length !== obj.activeRuns.length
|
|
265
|
+
|| (obj?.reliability !== undefined && !reliability)
|
|
266
|
+
|| (obj?.durableMessageQueue !== undefined && !durableMessageQueue?.status);
|
|
267
|
+
return { reachable, mode, generation: stringValue(obj?.generation), health: obj?.health, runtimeStatuses, activeRuns, reliability, durableMessageQueue, ambiguous: incomplete || booleanValue(obj?.ambiguous) };
|
|
242
268
|
}
|
|
243
269
|
export function checkActiveWork(status, target = "web") {
|
|
244
270
|
const reasons = [];
|
|
@@ -337,7 +363,8 @@ function printSafetyStatus(target, status) {
|
|
|
337
363
|
console.log(` mode: ${status.mode}`);
|
|
338
364
|
if (status.error)
|
|
339
365
|
console.log(` status error: ${status.error}`);
|
|
340
|
-
console.log(
|
|
366
|
+
console.log(" runtime queue layer:");
|
|
367
|
+
console.log(` sessions: ${status.runtimeStatuses.length}`);
|
|
341
368
|
for (const session of status.runtimeStatuses) {
|
|
342
369
|
console.log(` ${session.piboSessionId ?? "unknown"}: processing=${session.processing === true} streaming=${session.streaming === true} queued=${session.queuedMessages ?? 0}`);
|
|
343
370
|
if (session.activeEventId)
|
|
@@ -359,6 +386,31 @@ function printSafetyStatus(target, status) {
|
|
|
359
386
|
console.log(` active yielded runs: ${status.activeRuns.length}`);
|
|
360
387
|
for (const run of status.activeRuns)
|
|
361
388
|
console.log(` ${run.runId ?? "unknown"}: ${run.status ?? "active"}${run.toolName ? ` (${run.toolName})` : ""} session=${run.piboSessionId ?? "unknown"}`);
|
|
389
|
+
if (status.reliability) {
|
|
390
|
+
console.log(` run-job reliability: ${status.reliability.status}`);
|
|
391
|
+
console.log(` expired orphan jobs: ${status.reliability.expiredOrphanRunJobs}`);
|
|
392
|
+
console.log(` orphan DLQ records: ${status.reliability.orphanRunDeadLetters}`);
|
|
393
|
+
}
|
|
394
|
+
console.log(" durable message queue layer:");
|
|
395
|
+
const durable = status.durableMessageQueue;
|
|
396
|
+
if (!durable)
|
|
397
|
+
console.log(" status: unavailable (gateway app did not report this layer)");
|
|
398
|
+
else {
|
|
399
|
+
console.log(` status: ${durable.status ?? "ambiguous"}`);
|
|
400
|
+
console.log(` storage: ${durable.storage?.available === true ? "available" : "unavailable"}${durable.storage?.error ? ` (${durable.storage.error})` : ""}`);
|
|
401
|
+
for (const row of durable.counts ?? [])
|
|
402
|
+
console.log(` ${row.delivery ?? "unknown"}.${row.state ?? "unknown"}: count=${row.count ?? 0} bytes=${row.bytes ?? 0}`);
|
|
403
|
+
console.log(` interrupted predecessors: ${durable.interruptedPredecessors ?? 0}`);
|
|
404
|
+
console.log(` FIFO-blocked successors: ${durable.blockedSuccessors ?? 0}`);
|
|
405
|
+
console.log(` expired owned leases: ${durable.expiredOwnedLeases ?? 0}`);
|
|
406
|
+
console.log(` oldest dispatchable wait: ${durable.oldestDispatchableWaitMs ?? 0}ms`);
|
|
407
|
+
console.log(` oldest blocked wait: ${durable.oldestBlockedWaitMs ?? 0}ms`);
|
|
408
|
+
for (const scope of durable.affectedScopes ?? [])
|
|
409
|
+
console.log(` affected session=${scope.sessionId ?? "unknown"} room=${scope.roomId ?? "unknown"} blocker=${scope.blockingCommandId ?? "unknown"} successors=${scope.blockedSuccessors ?? 0}`);
|
|
410
|
+
for (const reason of durable.degradedReasons ?? [])
|
|
411
|
+
console.log(` degraded: ${reason}`);
|
|
412
|
+
}
|
|
413
|
+
console.log(" next: pibo debug message-queue");
|
|
362
414
|
}
|
|
363
415
|
function managerRequiresShell(command) {
|
|
364
416
|
return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
|
|
@@ -385,8 +437,11 @@ async function waitForManagedGatewayHealth(target) {
|
|
|
385
437
|
async function runManagedGatewayCommand(target, command, args, argv = process.argv) {
|
|
386
438
|
if (command === "status" || command === "doctor") {
|
|
387
439
|
const status = await readGatewaySafetyStatus(target);
|
|
388
|
-
|
|
389
|
-
|
|
440
|
+
if (args.includes("--json"))
|
|
441
|
+
console.log(JSON.stringify({ ...status, nextCommands: [`pibo gateway ${target} doctor`, `pibo debug message-queue`] }, null, 2));
|
|
442
|
+
else
|
|
443
|
+
printSafetyStatus(target, status);
|
|
444
|
+
if (target === "web" && !args.includes("--json")) {
|
|
390
445
|
const active = checkActiveWork(status, target);
|
|
391
446
|
if (active.unsafe) {
|
|
392
447
|
console.log(" restart safety: blocked");
|
|
@@ -397,8 +452,16 @@ async function runManagedGatewayCommand(target, command, args, argv = process.ar
|
|
|
397
452
|
console.log(" restart safety: idle");
|
|
398
453
|
printRestartApproval(status);
|
|
399
454
|
}
|
|
400
|
-
if (command === "doctor")
|
|
401
|
-
process.exitCode = status.reachable
|
|
455
|
+
if (command === "doctor") {
|
|
456
|
+
process.exitCode = status.reachable
|
|
457
|
+
&& !status.error
|
|
458
|
+
&& status.mode === expectedMode(target)
|
|
459
|
+
&& status.reliability?.status !== "degraded"
|
|
460
|
+
&& status.durableMessageQueue?.status === "healthy"
|
|
461
|
+
&& status.durableMessageQueue.storage?.available === true
|
|
462
|
+
? 0
|
|
463
|
+
: 1;
|
|
464
|
+
}
|
|
402
465
|
return true;
|
|
403
466
|
}
|
|
404
467
|
if (command === "start") {
|
|
@@ -730,6 +793,7 @@ Commands:
|
|
|
730
793
|
dev doctor Check dev gateway health
|
|
731
794
|
|
|
732
795
|
Options:
|
|
796
|
+
--json Print status or doctor output as JSON with next discovery commands
|
|
733
797
|
--force --confirm <snapshot-token>
|
|
734
798
|
Restart only the work explicitly approved from web status
|
|
735
799
|
|
package/dist/gateway/server.js
CHANGED
|
@@ -360,6 +360,7 @@ export class PiboGatewayServer {
|
|
|
360
360
|
},
|
|
361
361
|
findSessions: (input) => this.requireSessionStore().find(input),
|
|
362
362
|
listSessions: () => this.requireSessionStore().list?.() ?? [],
|
|
363
|
+
getSessionStructureRevision: () => this.requireSessionStore().getStructureRevision?.(),
|
|
363
364
|
getSessionRuntimeBinding: (piboSessionId) => this.requireRouter().getSessionRuntimeBinding(piboSessionId),
|
|
364
365
|
getSessionRuntimeProfile: (piboSessionId) => this.requireRouter().getSessionRuntimeProfile(piboSessionId),
|
|
365
366
|
inspectSessionRuntimeHistory: async (piboSessionId) => {
|
|
@@ -424,7 +425,9 @@ export class PiboGatewayServer {
|
|
|
424
425
|
getSessionStatusSnapshot: (piboSessionId, options) => this.requireRouter().getSessionStatusSnapshot(piboSessionId, options),
|
|
425
426
|
getSessionForkCandidates: (piboSessionId) => this.requireRouter().getSessionForkCandidates(piboSessionId),
|
|
426
427
|
listSessionRuntimeStatuses: () => this.requireRouter().listSessionRuntimeStatuses(),
|
|
428
|
+
getRuntimeCapacityStatus: () => this.requireRouter().getRuntimeCapacityStatus(),
|
|
427
429
|
listRuns: (options) => this.requireRouter().listRuns(options),
|
|
430
|
+
getRunJobReliabilityStatus: () => this.requireRouter().getRunJobReliabilityStatus(),
|
|
428
431
|
snapshotSignalSession: (piboSessionId) => this.requireRouter().snapshotSignalSession(piboSessionId),
|
|
429
432
|
snapshotSignalTree: (rootPiboSessionId) => this.requireRouter().snapshotSignalTree(rootPiboSessionId),
|
|
430
433
|
snapshotSignalStatuses: () => this.requireRouter().snapshotSignalStatuses(),
|
|
@@ -38,6 +38,9 @@ export const OPENAI_CODEX_GPT_6_ASTRA_MODEL = {
|
|
|
38
38
|
name: "GPT-6-Astra",
|
|
39
39
|
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
40
40
|
};
|
|
41
|
+
// Codex serves the Reserve quota alias from the Luna-class model, so Reserve borrows Luna's metadata.
|
|
42
|
+
export const OPENAI_CODEX_RESERVE_MODEL = { id: "gpt-reserve", name: "Luna Reserve" };
|
|
43
|
+
const OPENAI_CODEX_RESERVE_BASE_MODEL_ID = "gpt-5.6-luna";
|
|
41
44
|
const OPENAI_GPT_56_MODEL_IDS = new Set(OPENAI_GPT_56_MODELS.map((model) => model.id));
|
|
42
45
|
export function getBuiltInOpenAiModels() {
|
|
43
46
|
return getBuiltInProviderModels(OPENAI_PROVIDER_ID);
|
|
@@ -69,6 +72,10 @@ export function buildOpenAiCodexGpt56Models(baseModels = getBuiltInOpenAiCodexMo
|
|
|
69
72
|
}
|
|
70
73
|
export function buildOpenAiCodexSupplementalModels(baseModels = getBuiltInOpenAiCodexModels()) {
|
|
71
74
|
const models = buildOpenAiCodexGpt56Models(baseModels);
|
|
75
|
+
const reserveBase = models.find((model) => model.id === OPENAI_CODEX_RESERVE_BASE_MODEL_ID);
|
|
76
|
+
if (reserveBase && !models.some((model) => model.id === OPENAI_CODEX_RESERVE_MODEL.id)) {
|
|
77
|
+
models.push({ ...cloneModel(reserveBase), ...OPENAI_CODEX_RESERVE_MODEL });
|
|
78
|
+
}
|
|
72
79
|
if (models.some((model) => model.id === OPENAI_CODEX_GPT_6_ASTRA_MODEL.id))
|
|
73
80
|
return models;
|
|
74
81
|
return [...models, openAiCodexAstraModelToRegistryModel()];
|
|
@@ -76,7 +83,7 @@ export function buildOpenAiCodexSupplementalModels(baseModels = getBuiltInOpenAi
|
|
|
76
83
|
export function registerOpenAiSupplementalModels(modelRegistry, options = {}) {
|
|
77
84
|
const baseOpenAiModels = options.baseOpenAiModels ?? getBuiltInOpenAiModels();
|
|
78
85
|
const openAiModels = buildOpenAiGpt56Models(baseOpenAiModels);
|
|
79
|
-
const openAiAdded =
|
|
86
|
+
const openAiAdded = countAddedModels(baseOpenAiModels, OPENAI_PROVIDER_ID, openAiModels);
|
|
80
87
|
modelRegistry.registerProvider(OPENAI_PROVIDER_ID, {
|
|
81
88
|
baseUrl: OPENAI_BASE_URL,
|
|
82
89
|
api: OPENAI_RESPONSES_API,
|
|
@@ -85,9 +92,7 @@ export function registerOpenAiSupplementalModels(modelRegistry, options = {}) {
|
|
|
85
92
|
});
|
|
86
93
|
const baseOpenAiCodexModels = options.baseOpenAiCodexModels ?? getBuiltInOpenAiCodexModels();
|
|
87
94
|
const openAiCodexModels = buildOpenAiCodexSupplementalModels(baseOpenAiCodexModels);
|
|
88
|
-
const
|
|
89
|
-
const openAiCodexAdded = countMissingGpt56Models(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID)
|
|
90
|
-
+ (hasBuiltInAstra ? 0 : 1);
|
|
95
|
+
const openAiCodexAdded = countAddedModels(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID, openAiCodexModels);
|
|
91
96
|
modelRegistry.registerProvider(OPENAI_CODEX_PROVIDER_ID, {
|
|
92
97
|
baseUrl: OPENAI_CODEX_BASE_URL,
|
|
93
98
|
api: OPENAI_CODEX_RESPONSES_API,
|
|
@@ -128,9 +133,9 @@ function buildProviderGpt56Models(options) {
|
|
|
128
133
|
.map((model) => openAiGpt56ModelToRegistryModel(model, options));
|
|
129
134
|
return [...providerBaseModels, ...additions];
|
|
130
135
|
}
|
|
131
|
-
function
|
|
136
|
+
function countAddedModels(baseModels, providerId, registeredModels) {
|
|
132
137
|
const existingIds = new Set(baseModels.filter((model) => model.provider === providerId).map((model) => model.id));
|
|
133
|
-
return
|
|
138
|
+
return registeredModels.filter((model) => !existingIds.has(model.id)).length;
|
|
134
139
|
}
|
|
135
140
|
function cloneModel(model) {
|
|
136
141
|
return {
|
|
@@ -44,12 +44,14 @@ function ensurePiboJobColumn(db, name, definition) {
|
|
|
44
44
|
db.exec(`ALTER TABLE pibo_jobs ADD COLUMN ${name} ${definition}`);
|
|
45
45
|
}
|
|
46
46
|
export class PiboReliabilityStore {
|
|
47
|
+
options;
|
|
47
48
|
db;
|
|
48
49
|
appendEventStatement;
|
|
49
50
|
getEventByStreamIdStatement;
|
|
50
51
|
getEventByTopicEventIdStatement;
|
|
51
52
|
getEventByTopicIdempotencyKeyStatement;
|
|
52
|
-
constructor(path = piboHomePath("pibo-events.sqlite")) {
|
|
53
|
+
constructor(path = piboHomePath("pibo-events.sqlite"), options = {}) {
|
|
54
|
+
this.options = options;
|
|
53
55
|
const resolvedPath = path === ":memory:" ? path : resolve(path);
|
|
54
56
|
const insidePiboHome = ensurePrivatePiboHomeForPath(resolvedPath);
|
|
55
57
|
if (resolvedPath !== ":memory:")
|
|
@@ -571,6 +573,38 @@ export class PiboReliabilityStore {
|
|
|
571
573
|
}
|
|
572
574
|
return jobs;
|
|
573
575
|
}
|
|
576
|
+
inspectJobs(input = {}) {
|
|
577
|
+
const timestamp = now();
|
|
578
|
+
return this.listJobs(input).map((job) => this.inspectJob(job, timestamp));
|
|
579
|
+
}
|
|
580
|
+
reconcileOrphanRunJobs(input) {
|
|
581
|
+
const checkedAt = asDate(input.now).toISOString();
|
|
582
|
+
return this.inImmediateTransaction(() => this.reconcileOrphanRunJobsInTransaction(checkedAt, input.apply));
|
|
583
|
+
}
|
|
584
|
+
getRunJobReliabilityStatus() {
|
|
585
|
+
const timestamp = now();
|
|
586
|
+
const expired = this.db.prepare(`
|
|
587
|
+
SELECT COUNT(*) AS count
|
|
588
|
+
FROM pibo_jobs AS job
|
|
589
|
+
LEFT JOIN pibo_runs AS run ON run.job_id = job.job_id
|
|
590
|
+
WHERE job.queue = 'runs'
|
|
591
|
+
AND job.state = 'running'
|
|
592
|
+
AND job.claim_expires_at IS NOT NULL
|
|
593
|
+
AND job.claim_expires_at <= ?
|
|
594
|
+
AND run.run_id IS NULL
|
|
595
|
+
`).get(timestamp);
|
|
596
|
+
const dead = this.db.prepare(`
|
|
597
|
+
SELECT COUNT(*) AS count FROM pibo_dead_jobs
|
|
598
|
+
WHERE queue = 'runs' AND dead_reason = 'orphan_run_job'
|
|
599
|
+
`).get();
|
|
600
|
+
const expiredOrphanRunJobs = Number(expired.count);
|
|
601
|
+
const orphanRunDeadLetters = Number(dead.count);
|
|
602
|
+
return {
|
|
603
|
+
status: expiredOrphanRunJobs > 0 || orphanRunDeadLetters > 0 ? "degraded" : "ok",
|
|
604
|
+
expiredOrphanRunJobs,
|
|
605
|
+
orphanRunDeadLetters,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
574
608
|
listRecoverableJobs(input = {}) {
|
|
575
609
|
const timestamp = now();
|
|
576
610
|
const clauses = [
|
|
@@ -683,28 +717,37 @@ export class PiboReliabilityStore {
|
|
|
683
717
|
const runId = input.runId ?? `run_${randomUUID()}`;
|
|
684
718
|
const maxAttempts = Math.max(1, input.maxAttempts ?? 1);
|
|
685
719
|
const timeoutAt = input.timeoutMs === undefined ? undefined : new Date(Date.parse(timestamp) + input.timeoutMs).toISOString();
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
720
|
+
return this.inImmediateTransaction(() => {
|
|
721
|
+
const job = this.enqueue({
|
|
722
|
+
queue: "runs",
|
|
723
|
+
runAt: timestamp,
|
|
724
|
+
payload: {
|
|
725
|
+
runId,
|
|
726
|
+
controllerPiboSessionId: input.controllerPiboSessionId,
|
|
727
|
+
toolName: input.toolName,
|
|
728
|
+
params: input.params,
|
|
729
|
+
timeoutMs: input.timeoutMs,
|
|
730
|
+
},
|
|
731
|
+
maxAttempts,
|
|
732
|
+
});
|
|
733
|
+
this.options.onRunCreationBoundary?.("after_enqueue");
|
|
734
|
+
const claimed = this.claimPendingJobInTransaction(job.jobId, input.workerId ?? `run-registry:${process.pid}`, timestamp, 24 * 60 * 60 * 1000);
|
|
735
|
+
if (!claimed)
|
|
736
|
+
throw new Error(`Failed to claim newly-created run job "${job.jobId}"`);
|
|
737
|
+
this.options.onRunCreationBoundary?.("after_claim");
|
|
738
|
+
this.options.onRunCreationBoundary?.("before_run_insert");
|
|
739
|
+
this.db
|
|
740
|
+
.prepare(`
|
|
741
|
+
INSERT INTO pibo_runs (
|
|
742
|
+
run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
|
|
743
|
+
summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
|
|
744
|
+
completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json, origin_json
|
|
745
|
+
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
746
|
+
`)
|
|
747
|
+
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null, input.origin ? JSON.stringify(input.origin) : null);
|
|
748
|
+
this.options.onRunCreationBoundary?.("after_run_insert");
|
|
749
|
+
return this.requireRun(runId);
|
|
696
750
|
});
|
|
697
|
-
this.db
|
|
698
|
-
.prepare(`
|
|
699
|
-
INSERT INTO pibo_runs (
|
|
700
|
-
run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
|
|
701
|
-
summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
|
|
702
|
-
completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json, origin_json
|
|
703
|
-
) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
|
|
704
|
-
`)
|
|
705
|
-
.run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null, input.origin ? JSON.stringify(input.origin) : null);
|
|
706
|
-
this.claimJob(job.jobId, input.workerId ?? `run-registry:${process.pid}`, 24 * 60 * 60 * 1000);
|
|
707
|
-
return this.requireRun(runId);
|
|
708
751
|
}
|
|
709
752
|
updateRun(runId, patch) {
|
|
710
753
|
const existing = this.getRun(runId);
|
|
@@ -779,9 +822,10 @@ export class PiboReliabilityStore {
|
|
|
779
822
|
}
|
|
780
823
|
recoverInterruptedRuns(workerId = `run-registry:${process.pid}`) {
|
|
781
824
|
return this.inImmediateTransaction(() => {
|
|
825
|
+
const timestamp = now();
|
|
826
|
+
this.reconcileOrphanRunJobsInTransaction(timestamp, true);
|
|
782
827
|
const rows = this.db.prepare("SELECT * FROM pibo_runs WHERE status = 'running'").all();
|
|
783
828
|
const recovered = [];
|
|
784
|
-
const timestamp = now();
|
|
785
829
|
for (const row of rows) {
|
|
786
830
|
if (row.job_id && this.hasUnexpiredJobClaim(row.job_id, timestamp, workerId))
|
|
787
831
|
continue;
|
|
@@ -883,6 +927,58 @@ export class PiboReliabilityStore {
|
|
|
883
927
|
`)
|
|
884
928
|
.get(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
|
|
885
929
|
}
|
|
930
|
+
claimPendingJobInTransaction(jobId, workerId, timestamp, visibilityTimeoutMs) {
|
|
931
|
+
const claimExpiresAt = new Date(Date.parse(timestamp) + visibilityTimeoutMs).toISOString();
|
|
932
|
+
return this.db.prepare(`
|
|
933
|
+
UPDATE pibo_jobs
|
|
934
|
+
SET state = 'running',
|
|
935
|
+
worker_id = ?,
|
|
936
|
+
claim_expires_at = ?,
|
|
937
|
+
claim_token = claim_token + 1,
|
|
938
|
+
attempts = attempts + 1,
|
|
939
|
+
updated_at = ?
|
|
940
|
+
WHERE job_id = ?
|
|
941
|
+
AND state = 'pending'
|
|
942
|
+
AND run_at <= ?
|
|
943
|
+
AND attempts < max_attempts
|
|
944
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
945
|
+
RETURNING *
|
|
946
|
+
`).get(workerId, claimExpiresAt, timestamp, jobId, timestamp, timestamp);
|
|
947
|
+
}
|
|
948
|
+
inspectJob(job, timestamp) {
|
|
949
|
+
const claimExpired = job.state === "running"
|
|
950
|
+
&& (!job.claimExpiresAt || job.claimExpiresAt <= timestamp);
|
|
951
|
+
const missingRunRecord = job.queue === "runs"
|
|
952
|
+
&& this.db.prepare("SELECT 1 AS found FROM pibo_runs WHERE job_id = ? LIMIT 1").get(job.jobId) === undefined;
|
|
953
|
+
let effectiveLiveness;
|
|
954
|
+
if (job.state === "pending")
|
|
955
|
+
effectiveLiveness = missingRunRecord ? "orphan_pending" : "pending";
|
|
956
|
+
else if (claimExpired)
|
|
957
|
+
effectiveLiveness = missingRunRecord ? "expired_orphan" : "expired_claim";
|
|
958
|
+
else
|
|
959
|
+
effectiveLiveness = missingRunRecord ? "live_orphan_claim" : "live_claim";
|
|
960
|
+
return { ...job, claimExpired, missingRunRecord, effectiveLiveness };
|
|
961
|
+
}
|
|
962
|
+
reconcileOrphanRunJobsInTransaction(timestamp, apply) {
|
|
963
|
+
const rows = this.db.prepare(`
|
|
964
|
+
SELECT job.*
|
|
965
|
+
FROM pibo_jobs AS job
|
|
966
|
+
LEFT JOIN pibo_runs AS run ON run.job_id = job.job_id
|
|
967
|
+
WHERE job.queue = 'runs'
|
|
968
|
+
AND job.state = 'running'
|
|
969
|
+
AND job.claim_expires_at IS NOT NULL
|
|
970
|
+
AND job.claim_expires_at <= ?
|
|
971
|
+
AND run.run_id IS NULL
|
|
972
|
+
ORDER BY job.created_at ASC, job.job_id ASC
|
|
973
|
+
`).all(timestamp);
|
|
974
|
+
const candidates = rows.map((row) => this.inspectJob(jobFromRow(row), timestamp));
|
|
975
|
+
if (apply) {
|
|
976
|
+
for (const row of rows) {
|
|
977
|
+
this.moveJobToDead(row, "Expired runs job has no matching pibo_runs record.", "orphan_run_job", timestamp);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return { checkedAt: timestamp, apply, candidates, moved: apply ? rows.length : 0 };
|
|
981
|
+
}
|
|
886
982
|
moveExpiredJobs(timestamp) {
|
|
887
983
|
const expired = this.db.prepare("SELECT * FROM pibo_jobs WHERE expires_at IS NOT NULL AND expires_at <= ?").all(timestamp);
|
|
888
984
|
for (const row of expired)
|