@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.
Files changed (91) hide show
  1. package/dist/agent-runtime/routed-session.js +111 -25
  2. package/dist/agent-runtimes/codex-native/turn.js +27 -3
  3. package/dist/agent-runtimes/pi/adapter.js +1 -0
  4. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  5. package/dist/apps/chat/bounded-event-stream.js +98 -0
  6. package/dist/apps/chat/chat-settings-routes.js +3 -3
  7. package/dist/apps/chat/data/chat-data-mappers.js +40 -14
  8. package/dist/apps/chat/data/event-command-service.js +30 -21
  9. package/dist/apps/chat/data/history-query-service.js +34 -25
  10. package/dist/apps/chat/data/read-state-service.js +37 -3
  11. package/dist/apps/chat/data/session-query-service.js +19 -11
  12. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  13. package/dist/apps/chat/message-command-dispatcher.js +149 -0
  14. package/dist/apps/chat/output-compactor.js +9 -0
  15. package/dist/apps/chat/output-event-policy.js +9 -1
  16. package/dist/apps/chat/stream.js +21 -3
  17. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  18. package/dist/apps/chat/trace-response-cache.js +46 -0
  19. package/dist/apps/chat/trace-v2.js +12 -6
  20. package/dist/apps/chat/trace.js +1 -1
  21. package/dist/apps/chat/web-app.js +645 -297
  22. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
  27. package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
  28. package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
  29. package/dist/apps/chat-ui/index.html +2 -2
  30. package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
  31. package/dist/apps/chat-vscode-web/index.html +1 -1
  32. package/dist/cli-session/localSessionSource.js +3 -2
  33. package/dist/compute/pool/seeds.js +25 -3
  34. package/dist/core/events.js +4 -0
  35. package/dist/core/output-render-sequence.js +65 -6
  36. package/dist/core/provider-capacity.js +33 -0
  37. package/dist/core/provider-telemetry.js +21 -6
  38. package/dist/core/runtime-capacity.js +174 -0
  39. package/dist/core/runtime-telemetry.js +40 -12
  40. package/dist/core/session-router.js +178 -13
  41. package/dist/data/async-chat-reads.js +38 -0
  42. package/dist/data/async-chat-storage.js +96 -0
  43. package/dist/data/async-telemetry-maintenance.js +9 -0
  44. package/dist/data/bounded-worker-client.js +256 -0
  45. package/dist/data/chat-read-projections.js +159 -0
  46. package/dist/data/chat-read-worker.js +73 -0
  47. package/dist/data/chat-storage-worker.js +167 -0
  48. package/dist/data/ingest-service.js +152 -18
  49. package/dist/data/message-command-store.js +290 -0
  50. package/dist/data/payload-store.js +92 -11
  51. package/dist/data/pibo-store.js +10 -8
  52. package/dist/data/schema.js +39 -4
  53. package/dist/data/session-store.js +3 -1
  54. package/dist/data/storage-backup.js +278 -0
  55. package/dist/data/storage-maintenance.js +344 -0
  56. package/dist/data/storage-verification-worker.js +25 -0
  57. package/dist/data/telemetry-capture.js +188 -0
  58. package/dist/data/telemetry-command.js +3 -0
  59. package/dist/data/telemetry-maintenance-worker.js +40 -0
  60. package/dist/data/telemetry-maintenance.js +110 -0
  61. package/dist/data/telemetry-retention.js +16 -7
  62. package/dist/data/telemetry-worker.js +111 -0
  63. package/dist/data/telemetry-writer.js +150 -83
  64. package/dist/data/telemetry.js +5 -0
  65. package/dist/debug/index.js +207 -1
  66. package/dist/debug/message-queue.js +108 -0
  67. package/dist/debug/output-collision-repair.js +140 -0
  68. package/dist/debug/output-integrity.js +38 -2
  69. package/dist/debug/output-repair.js +1 -0
  70. package/dist/debug/storage-backup.js +41 -0
  71. package/dist/debug/storage-maintenance.js +78 -0
  72. package/dist/debug/telemetry-capture.js +66 -0
  73. package/dist/gateway/cli.js +71 -7
  74. package/dist/gateway/server.js +3 -0
  75. package/dist/providers/openai-gpt56.js +11 -6
  76. package/dist/reliability/store.js +119 -23
  77. package/dist/session-ui/terminalRows.js +54 -13
  78. package/dist/sessions/pibo-data-store.js +19 -14
  79. package/dist/shared/debug-features.js +4 -0
  80. package/dist/shared/model-inference-metrics.js +23 -0
  81. package/dist/shared/trace-event-projection.js +69 -2
  82. package/dist/shared/trace-history.js +9 -0
  83. package/dist/shared/trace-live-reducer.js +1 -0
  84. package/dist/shared/trace-patch-nodes.js +19 -0
  85. package/dist/web/channel.js +117 -12
  86. package/dist/web/http.js +135 -41
  87. package/npm-shrinkwrap.json +2 -2
  88. package/package.json +1 -1
  89. package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -1
  90. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  91. 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
+ }
@@ -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
- return { reachable, mode, generation: stringValue(obj?.generation), health: obj?.health, runtimeStatuses, activeRuns, ambiguous: incomplete || booleanValue(obj?.ambiguous) };
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(` runtime sessions: ${status.runtimeStatuses.length}`);
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
- printSafetyStatus(target, status);
389
- if (target === "web") {
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 && !status.error && status.mode === expectedMode(target) ? 0 : 1;
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
 
@@ -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 = countMissingGpt56Models(baseOpenAiModels, OPENAI_PROVIDER_ID);
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 hasBuiltInAstra = baseOpenAiCodexModels.some((model) => model.provider === OPENAI_CODEX_PROVIDER_ID && model.id === OPENAI_CODEX_GPT_6_ASTRA_MODEL.id);
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 countMissingGpt56Models(baseModels, providerId) {
136
+ function countAddedModels(baseModels, providerId, registeredModels) {
132
137
  const existingIds = new Set(baseModels.filter((model) => model.provider === providerId).map((model) => model.id));
133
- return OPENAI_GPT_56_MODELS.filter((model) => !existingIds.has(model.id)).length;
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
- const job = this.enqueue({
687
- queue: "runs",
688
- payload: {
689
- runId,
690
- controllerPiboSessionId: input.controllerPiboSessionId,
691
- toolName: input.toolName,
692
- params: input.params,
693
- timeoutMs: input.timeoutMs,
694
- },
695
- maxAttempts,
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)