@pasko70/pibo 3.0.0 → 3.0.2

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 (77) hide show
  1. package/README.md +20 -6
  2. package/dist/agent-runtime/context-build.js +26 -2
  3. package/dist/agent-runtime/routed-session.js +108 -9
  4. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  5. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  6. package/dist/agent-runtimes/omp/adapter.js +3 -7
  7. package/dist/agent-runtimes/pi/adapter.js +5 -3
  8. package/dist/agent-runtimes/pi/routed-session.js +49 -8
  9. package/dist/agent-runtimes/pi/runtime.js +6 -0
  10. package/dist/apps/chat/agent-profiles.js +31 -2
  11. package/dist/apps/chat/agent-store.js +43 -3
  12. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  13. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  14. package/dist/apps/chat/data/session-query-service.js +33 -10
  15. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  16. package/dist/apps/chat/output-compactor.js +144 -21
  17. package/dist/apps/chat/output-event-policy.js +179 -2
  18. package/dist/apps/chat/stream.js +16 -7
  19. package/dist/apps/chat/trace-v2.js +1 -0
  20. package/dist/apps/chat/web-app.js +236 -64
  21. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-B6GZgWSj.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-BPotHecb.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-BzqQKeVO.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CvaPTBTN.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-paagejNj.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +228 -0
  27. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +1 -0
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/{index-Dtw2Z7jz.js → index-U-MSErGa.js} +4 -4
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  32. package/dist/cli-session/localSessionSource.js +153 -161
  33. package/dist/cli.js +7 -11
  34. package/dist/core/context-build.js +7 -16
  35. package/dist/core/output-persistence-retry.js +484 -0
  36. package/dist/core/output-render-sequence.js +331 -0
  37. package/dist/core/profiles.js +24 -0
  38. package/dist/core/provider-recovery.js +7 -1
  39. package/dist/core/session-model.js +37 -1
  40. package/dist/core/session-router.js +67 -20
  41. package/dist/data/ingest-service.js +91 -9
  42. package/dist/data/navigation-store.js +5 -5
  43. package/dist/data/schema.js +122 -2
  44. package/dist/data/session-store.js +3 -3
  45. package/dist/debug/index.js +44 -2
  46. package/dist/gateway/web.js +8 -8
  47. package/dist/local/client.js +3 -2
  48. package/dist/plugins/context-files-store.js +30 -0
  49. package/dist/plugins/context-files.js +52 -1
  50. package/dist/plugins/registry.js +2 -0
  51. package/dist/plugins/user-profile-resources.js +22 -0
  52. package/dist/reliability/store.js +246 -37
  53. package/dist/session-ui/terminalRows.js +6 -3
  54. package/dist/sessions/pibo-data-store.js +145 -0
  55. package/dist/sessions/store.js +42 -0
  56. package/dist/setup/cli.js +422 -6
  57. package/dist/setup/installation-profiles.js +464 -0
  58. package/dist/shared/deterministic-digest.js +94 -0
  59. package/dist/shared/trace-engine.js +54 -0
  60. package/dist/shared/trace-event-projection.js +124 -67
  61. package/dist/shared/trace-history.js +47 -12
  62. package/dist/shared/trace-live-reducer.js +23 -4
  63. package/dist/shared/trace-nodes.js +17 -28
  64. package/dist/shared/trace-order.js +41 -5
  65. package/dist/shared/trace-page-merge.js +44 -14
  66. package/dist/shared/trace-patch-nodes.js +2 -0
  67. package/dist/shared/trace-subagent-links.js +4 -1
  68. package/dist/shared/trace-tool-identity.js +15 -8
  69. package/dist/signals/projector.js +28 -6
  70. package/dist/subagents/tool.js +7 -3
  71. package/docs/ops/vscode-extension-release.md +9 -1
  72. package/npm-shrinkwrap.json +2 -2
  73. package/package.json +2 -1
  74. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  75. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  76. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  77. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
@@ -36,6 +36,11 @@ function ensurePiboRunColumn(db, name, definition) {
36
36
  if (!columns.has(name))
37
37
  db.exec(`ALTER TABLE pibo_runs ADD COLUMN ${name} ${definition}`);
38
38
  }
39
+ function ensurePiboJobColumn(db, name, definition) {
40
+ const columns = sqliteTableColumns(db, "pibo_jobs");
41
+ if (!columns.has(name))
42
+ db.exec(`ALTER TABLE pibo_jobs ADD COLUMN ${name} ${definition}`);
43
+ }
39
44
  export class PiboReliabilityStore {
40
45
  db;
41
46
  appendEventStatement;
@@ -74,6 +79,13 @@ export class PiboReliabilityStore {
74
79
  CREATE INDEX IF NOT EXISTS idx_pibo_event_stream_topic_stream
75
80
  ON pibo_event_stream(topic, stream_id);
76
81
 
82
+ CREATE TABLE IF NOT EXISTS pibo_delivery_receipts (
83
+ delivery_id TEXT NOT NULL,
84
+ projection TEXT NOT NULL,
85
+ delivered_at TEXT NOT NULL,
86
+ PRIMARY KEY(delivery_id, projection)
87
+ );
88
+
77
89
  CREATE TABLE IF NOT EXISTS pibo_event_consumers (
78
90
  consumer TEXT NOT NULL,
79
91
  topic TEXT NOT NULL,
@@ -91,6 +103,7 @@ export class PiboReliabilityStore {
91
103
  priority INTEGER NOT NULL DEFAULT 0,
92
104
  worker_id TEXT,
93
105
  claim_expires_at TEXT,
106
+ claim_token INTEGER NOT NULL DEFAULT 0,
94
107
  attempts INTEGER NOT NULL DEFAULT 0,
95
108
  max_attempts INTEGER NOT NULL DEFAULT 1,
96
109
  idempotency_key TEXT,
@@ -157,6 +170,7 @@ export class PiboReliabilityStore {
157
170
  CREATE INDEX IF NOT EXISTS idx_pibo_runs_status
158
171
  ON pibo_runs(status);
159
172
  `);
173
+ ensurePiboJobColumn(this.db, "claim_token", "INTEGER NOT NULL DEFAULT 0");
160
174
  ensurePiboRunColumn(this.db, "timeout_ms", "INTEGER");
161
175
  ensurePiboRunColumn(this.db, "timeout_at", "TEXT");
162
176
  ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
@@ -189,6 +203,21 @@ export class PiboReliabilityStore {
189
203
  throw error;
190
204
  }
191
205
  }
206
+ hasDeliveryReceipt(deliveryId, projection) {
207
+ return this.db
208
+ .prepare("SELECT 1 AS found FROM pibo_delivery_receipts WHERE delivery_id = ? AND projection = ?")
209
+ .get(deliveryId, projection) !== undefined;
210
+ }
211
+ recordDeliveryReceipt(deliveryId, projection, deliveredAt = now()) {
212
+ this.db.prepare(`
213
+ INSERT OR IGNORE INTO pibo_delivery_receipts (delivery_id, projection, delivered_at)
214
+ VALUES (?, ?, ?)
215
+ `).run(deliveryId, projection, deliveredAt);
216
+ const row = this.db
217
+ .prepare("SELECT delivery_id, projection, delivered_at FROM pibo_delivery_receipts WHERE delivery_id = ? AND projection = ?")
218
+ .get(deliveryId, projection);
219
+ return { deliveryId: row.delivery_id, projection: row.projection, deliveredAt: row.delivered_at };
220
+ }
192
221
  list(input = {}) {
193
222
  const clauses = [];
194
223
  const values = [];
@@ -339,6 +368,7 @@ export class PiboReliabilityStore {
339
368
  SET state = 'running',
340
369
  worker_id = ?,
341
370
  claim_expires_at = ?,
371
+ claim_token = claim_token + 1,
342
372
  attempts = attempts + 1,
343
373
  updated_at = ?
344
374
  WHERE job_id IN (
@@ -361,6 +391,7 @@ export class PiboReliabilityStore {
361
391
  SET state = 'running',
362
392
  worker_id = ?,
363
393
  claim_expires_at = ?,
394
+ claim_token = claim_token + 1,
364
395
  attempts = attempts + 1,
365
396
  updated_at = ?
366
397
  WHERE job_id = ?
@@ -372,7 +403,50 @@ export class PiboReliabilityStore {
372
403
  .get(workerId, claimExpiresAt, timestamp, jobId, timestamp, timestamp);
373
404
  return row ? jobFromRow(row) : undefined;
374
405
  }
375
- ack(jobId, workerId) {
406
+ claimRecoverableJob(jobId, workerId, visibilityTimeoutMs = 30000, forcePending = false) {
407
+ const timestamp = now();
408
+ const claimExpiresAt = new Date(Date.now() + visibilityTimeoutMs).toISOString();
409
+ const row = this.db.prepare(`
410
+ UPDATE pibo_jobs
411
+ SET state = 'running',
412
+ worker_id = ?,
413
+ claim_expires_at = ?,
414
+ claim_token = claim_token + 1,
415
+ attempts = attempts + 1,
416
+ updated_at = ?
417
+ WHERE job_id = ?
418
+ AND (
419
+ (state = 'pending' AND (? = 1 OR run_at <= ?))
420
+ OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
421
+ )
422
+ AND (expires_at IS NULL OR expires_at > ?)
423
+ RETURNING *
424
+ `).get(workerId, claimExpiresAt, timestamp, jobId, forcePending ? 1 : 0, timestamp, timestamp, timestamp);
425
+ return row ? jobFromRow(row) : undefined;
426
+ }
427
+ updateJobPayload(jobId, workerId, payload, claimToken) {
428
+ const result = this.db.prepare(`
429
+ UPDATE pibo_jobs
430
+ SET payload_json = ?, updated_at = ?
431
+ WHERE job_id = ? AND state = 'running' AND worker_id = ?
432
+ AND (? IS NULL OR claim_token = ?)
433
+ AND claim_expires_at IS NOT NULL AND claim_expires_at > ?
434
+ `).run(json(payload), now(), jobId, workerId, claimToken ?? null, claimToken ?? null, now());
435
+ return Number(result.changes ?? 0) > 0;
436
+ }
437
+ releaseJob(jobId, workerId, delayMs = 0, claimToken) {
438
+ const timestamp = now();
439
+ const runAt = new Date(Date.parse(timestamp) + Math.max(0, delayMs)).toISOString();
440
+ const result = this.db.prepare(`
441
+ UPDATE pibo_jobs
442
+ SET state = 'pending', worker_id = NULL, claim_expires_at = NULL, run_at = ?, updated_at = ?
443
+ WHERE job_id = ? AND state = 'running' AND worker_id = ?
444
+ AND (? IS NULL OR claim_token = ?)
445
+ AND claim_expires_at IS NOT NULL AND claim_expires_at > ?
446
+ `).run(runAt, timestamp, jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
447
+ return Number(result.changes ?? 0) > 0;
448
+ }
449
+ ack(jobId, workerId, claimToken) {
376
450
  const timestamp = now();
377
451
  const result = this.db
378
452
  .prepare(`
@@ -380,47 +454,53 @@ export class PiboReliabilityStore {
380
454
  WHERE job_id = ?
381
455
  AND state = 'running'
382
456
  AND worker_id = ?
457
+ AND (? IS NULL OR claim_token = ?)
383
458
  AND claim_expires_at IS NOT NULL
384
459
  AND claim_expires_at > ?
385
460
  `)
386
- .run(jobId, workerId, timestamp);
461
+ .run(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
387
462
  return Number(result.changes ?? 0) > 0;
388
463
  }
389
464
  retry(jobId, workerId, input = {}) {
390
465
  const timestamp = now();
391
- const row = this.getLiveWorkerJob(jobId, workerId, timestamp);
392
- if (!row)
393
- return false;
394
- const job = jobFromRow(row);
395
- if (job.attempts >= job.maxAttempts) {
396
- this.moveJobToDead(row, input.error ?? "Job exhausted retry attempts.", "max_attempts", timestamp);
397
- return true;
398
- }
399
- const delayMs = input.delayMs ?? retryDelayMs(job.attempts, input);
400
- const runAt = new Date(asDate(input.now).getTime() + delayMs).toISOString();
401
- this.db
402
- .prepare(`
403
- UPDATE pibo_jobs
404
- SET state = 'pending',
405
- worker_id = NULL,
406
- claim_expires_at = NULL,
407
- run_at = ?,
408
- updated_at = ?,
409
- last_error = ?
410
- WHERE job_id = ?
411
- `)
412
- .run(runAt, timestamp, input.error ?? null, jobId);
413
- return true;
466
+ return this.inImmediateTransaction(() => {
467
+ const row = this.getLiveWorkerJob(jobId, workerId, timestamp, input.claimToken);
468
+ if (!row)
469
+ return false;
470
+ const job = jobFromRow(row);
471
+ if (job.attempts >= job.maxAttempts) {
472
+ this.moveJobToDead(row, input.error ?? "Job exhausted retry attempts.", "max_attempts", timestamp);
473
+ return true;
474
+ }
475
+ const delayMs = input.delayMs ?? retryDelayMs(job.attempts, input);
476
+ const runAt = new Date(asDate(input.now).getTime() + delayMs).toISOString();
477
+ const result = this.db
478
+ .prepare(`
479
+ UPDATE pibo_jobs
480
+ SET state = 'pending',
481
+ worker_id = NULL,
482
+ claim_expires_at = NULL,
483
+ run_at = ?,
484
+ updated_at = ?,
485
+ last_error = ?
486
+ WHERE job_id = ? AND state = 'running' AND worker_id = ?
487
+ AND (? IS NULL OR claim_token = ?)
488
+ `)
489
+ .run(runAt, timestamp, input.error ?? null, jobId, workerId, input.claimToken ?? null, input.claimToken ?? null);
490
+ return Number(result.changes ?? 0) > 0;
491
+ });
414
492
  }
415
- fail(jobId, workerId, error) {
493
+ fail(jobId, workerId, error, claimToken) {
416
494
  const timestamp = now();
417
- const row = this.getLiveWorkerJob(jobId, workerId, timestamp);
418
- if (!row)
419
- return false;
420
- this.moveJobToDead(row, error, "failed", timestamp);
421
- return true;
495
+ return this.inImmediateTransaction(() => {
496
+ const row = this.getLiveWorkerJob(jobId, workerId, timestamp, claimToken);
497
+ if (!row)
498
+ return false;
499
+ this.moveJobToDead(row, error, "failed", timestamp);
500
+ return true;
501
+ });
422
502
  }
423
- heartbeat(jobId, workerId, extendMs) {
503
+ heartbeat(jobId, workerId, extendMs, claimToken) {
424
504
  const timestamp = now();
425
505
  const claimExpiresAt = new Date(Date.now() + Math.max(1, extendMs)).toISOString();
426
506
  const result = this.db
@@ -430,10 +510,11 @@ export class PiboReliabilityStore {
430
510
  WHERE job_id = ?
431
511
  AND state = 'running'
432
512
  AND worker_id = ?
513
+ AND (? IS NULL OR claim_token = ?)
433
514
  AND claim_expires_at IS NOT NULL
434
515
  AND claim_expires_at > ?
435
516
  `)
436
- .run(claimExpiresAt, timestamp, jobId, workerId, timestamp);
517
+ .run(claimExpiresAt, timestamp, jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
437
518
  return Number(result.changes ?? 0) > 0;
438
519
  }
439
520
  listJobs(input = {}) {
@@ -447,11 +528,56 @@ export class PiboReliabilityStore {
447
528
  clauses.push("state = ?");
448
529
  values.push(input.state);
449
530
  }
531
+ if (input.excludeJobIds?.length) {
532
+ clauses.push(`job_id NOT IN (${input.excludeJobIds.map(() => "?").join(", ")})`);
533
+ values.push(...input.excludeJobIds);
534
+ }
450
535
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
451
536
  const rows = this.db
452
537
  .prepare(`SELECT * FROM pibo_jobs ${where} ORDER BY priority DESC, run_at ASC, created_at ASC LIMIT ?`)
453
538
  .all(...values, clampLimit(input.limit, 100));
454
- return rows.map(jobFromRow);
539
+ const jobs = [];
540
+ for (const row of rows) {
541
+ try {
542
+ jobs.push(jobFromRow(row));
543
+ }
544
+ catch {
545
+ this.quarantineJobRow(row, "payload_malformed");
546
+ }
547
+ }
548
+ return jobs;
549
+ }
550
+ listRecoverableJobs(input = {}) {
551
+ const timestamp = now();
552
+ const clauses = [
553
+ "((state = 'pending' AND (? = 1 OR run_at <= ?)) OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
554
+ "(expires_at IS NULL OR expires_at > ?)",
555
+ ];
556
+ const values = [input.includeDeferredPending ? 1 : 0, timestamp, timestamp, timestamp];
557
+ if (input.queue) {
558
+ clauses.push("queue = ?");
559
+ values.push(input.queue);
560
+ }
561
+ if (input.excludeJobIds?.length) {
562
+ clauses.push(`job_id NOT IN (${input.excludeJobIds.map(() => "?").join(", ")})`);
563
+ values.push(...input.excludeJobIds);
564
+ }
565
+ const rows = this.db.prepare(`
566
+ SELECT * FROM pibo_jobs
567
+ WHERE ${clauses.join(" AND ")}
568
+ ORDER BY priority DESC, run_at ASC, created_at ASC
569
+ LIMIT ?
570
+ `).all(...values, clampLimit(input.limit, 100));
571
+ const jobs = [];
572
+ for (const row of rows) {
573
+ try {
574
+ jobs.push(jobFromRow(row));
575
+ }
576
+ catch {
577
+ this.quarantineJobRow(row, "payload_malformed");
578
+ }
579
+ }
580
+ return jobs;
455
581
  }
456
582
  listDead(input = {}) {
457
583
  const clauses = [];
@@ -466,6 +592,46 @@ export class PiboReliabilityStore {
466
592
  .all(...values, clampLimit(input.limit, 100));
467
593
  return rows.map(deadJobFromRow);
468
594
  }
595
+ hasLiveJob(jobId) {
596
+ return this.db.prepare("SELECT 1 AS found FROM pibo_jobs WHERE job_id = ?").get(jobId) !== undefined;
597
+ }
598
+ hasJobs(queue) {
599
+ return queue
600
+ ? this.db.prepare("SELECT 1 AS found FROM pibo_jobs WHERE queue = ? LIMIT 1").get(queue) !== undefined
601
+ : this.db.prepare("SELECT 1 AS found FROM pibo_jobs LIMIT 1").get() !== undefined;
602
+ }
603
+ hasRecoverableJobs(queue, includeDeferredPending = false) {
604
+ const timestamp = now();
605
+ return this.db.prepare(`
606
+ SELECT 1 AS found FROM pibo_jobs
607
+ WHERE queue = ?
608
+ AND (
609
+ (state = 'pending' AND (? = 1 OR run_at <= ?))
610
+ OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
611
+ )
612
+ AND (expires_at IS NULL OR expires_at > ?)
613
+ LIMIT 1
614
+ `).get(queue, includeDeferredPending ? 1 : 0, timestamp, timestamp, timestamp) !== undefined;
615
+ }
616
+ quarantineJob(jobId, reason, correlation = {}) {
617
+ const row = this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(jobId);
618
+ if (!row)
619
+ return false;
620
+ this.quarantineJobRow(row, reason, correlation);
621
+ return true;
622
+ }
623
+ deadLetter(input) {
624
+ const timestamp = now();
625
+ const jobId = input.jobId ?? `job_${randomUUID()}`;
626
+ this.db.prepare(`
627
+ INSERT OR REPLACE INTO pibo_dead_jobs (
628
+ job_id, queue, payload_json, attempts, max_attempts, idempotency_key, created_at,
629
+ updated_at, expires_at, last_error, dead_at, dead_reason
630
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
631
+ `).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);
632
+ const row = this.db.prepare("SELECT * FROM pibo_dead_jobs WHERE job_id = ?").get(jobId);
633
+ return deadJobFromRow(row);
634
+ }
469
635
  requeueDead(jobId, input = {}) {
470
636
  const dead = this.db.prepare("SELECT * FROM pibo_dead_jobs WHERE job_id = ?").get(jobId);
471
637
  if (!dead)
@@ -659,17 +825,18 @@ export class PiboReliabilityStore {
659
825
  .get(queue, idempotencyKey);
660
826
  return row ? jobFromRow(row) : undefined;
661
827
  }
662
- getLiveWorkerJob(jobId, workerId, timestamp) {
828
+ getLiveWorkerJob(jobId, workerId, timestamp, claimToken) {
663
829
  return this.db
664
830
  .prepare(`
665
831
  SELECT * FROM pibo_jobs
666
832
  WHERE job_id = ?
667
833
  AND state = 'running'
668
834
  AND worker_id = ?
835
+ AND (? IS NULL OR claim_token = ?)
669
836
  AND claim_expires_at IS NOT NULL
670
837
  AND claim_expires_at > ?
671
838
  `)
672
- .get(jobId, workerId, timestamp);
839
+ .get(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
673
840
  }
674
841
  moveExpiredJobs(timestamp) {
675
842
  const expired = this.db.prepare("SELECT * FROM pibo_jobs WHERE expires_at IS NOT NULL AND expires_at <= ?").all(timestamp);
@@ -718,6 +885,39 @@ export class PiboReliabilityStore {
718
885
  .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
886
  this.db.prepare("DELETE FROM pibo_jobs WHERE job_id = ?").run(row.job_id);
720
887
  }
888
+ quarantineJobRow(row, reason, correlation = {}) {
889
+ this.inImmediateTransaction(() => {
890
+ const current = this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(row.job_id);
891
+ if (!current)
892
+ return;
893
+ const payload = {
894
+ version: 1,
895
+ key: correlation.key ?? current.job_id,
896
+ ...(correlation.piboSessionId ? { piboSessionId: correlation.piboSessionId } : {}),
897
+ ...(correlation.eventId ? { eventId: correlation.eventId } : {}),
898
+ state: { phase: "quarantined", reasonCode: reason },
899
+ };
900
+ const sanitized = {
901
+ ...current,
902
+ payload_json: json(payload),
903
+ idempotency_key: correlation.key ?? null,
904
+ last_error: reason,
905
+ };
906
+ this.moveJobToDead(sanitized, reason, reason, now());
907
+ });
908
+ }
909
+ inImmediateTransaction(operation) {
910
+ this.db.exec("BEGIN IMMEDIATE");
911
+ try {
912
+ const result = operation();
913
+ this.db.exec("COMMIT");
914
+ return result;
915
+ }
916
+ catch (error) {
917
+ this.db.exec("ROLLBACK");
918
+ throw error;
919
+ }
920
+ }
721
921
  requireRun(runId) {
722
922
  const run = this.getRun(runId);
723
923
  if (!run)
@@ -750,6 +950,7 @@ function jobFromRow(row) {
750
950
  priority: row.priority,
751
951
  workerId: row.worker_id ?? undefined,
752
952
  claimExpiresAt: row.claim_expires_at ?? undefined,
953
+ claimToken: row.claim_token,
753
954
  attempts: row.attempts,
754
955
  maxAttempts: row.max_attempts,
755
956
  idempotencyKey: row.idempotency_key ?? undefined,
@@ -763,7 +964,7 @@ function deadJobFromRow(row) {
763
964
  return {
764
965
  jobId: row.job_id,
765
966
  queue: row.queue,
766
- payload: parseJson(row.payload_json),
967
+ payload: safeDeadPayload(row.payload_json, row.dead_reason, row.job_id),
767
968
  attempts: row.attempts,
768
969
  maxAttempts: row.max_attempts,
769
970
  idempotencyKey: row.idempotency_key ?? undefined,
@@ -775,6 +976,14 @@ function deadJobFromRow(row) {
775
976
  deadReason: row.dead_reason,
776
977
  };
777
978
  }
979
+ function safeDeadPayload(value, reason, jobId) {
980
+ try {
981
+ return parseJson(value);
982
+ }
983
+ catch {
984
+ return { version: 1, key: jobId, state: { phase: "quarantined", reasonCode: reason } };
985
+ }
986
+ }
778
987
  function runFromRow(row) {
779
988
  const output = {
780
989
  runId: row.run_id,
@@ -1,4 +1,5 @@
1
1
  import { compareTraceNodes } from "../shared/trace-nodes.js";
2
+ import { parseTraceToolNodeIdentity } from "../shared/trace-tool-identity.js";
2
3
  import { terminalTextValue } from "./terminalValue.js";
3
4
  export const MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS = 20;
4
5
  export const COMPACT_TERMINAL_OUTPUT_PREVIEW_LINES = 5;
@@ -168,9 +169,11 @@ function createRowCandidate(node, turnId) {
168
169
  }
169
170
  export function compactTerminalRowIdentity(node) {
170
171
  if (node.type === "tool.call" || node.type === "tool.result" || node.type === "agent.delegation") {
171
- const toolCallId = node.toolCallId ?? stableKeySuffix(node.stableKey, "tool:");
172
- if (toolCallId)
173
- return `terminal:tool:${toolCallId}`;
172
+ if (node.toolCallId) {
173
+ return parseTraceToolNodeIdentity(node.id)?.qualifier
174
+ ? `terminal:${node.id}`
175
+ : `terminal:tool:${node.toolCallId}`;
176
+ }
174
177
  if (node.type === "agent.delegation" && node.linkedPiboSessionId) {
175
178
  return `terminal:delegation:${node.linkedPiboSessionId}`;
176
179
  }
@@ -100,6 +100,151 @@ export class PiboDataSessionStore {
100
100
  `).run(updated.piSessionId || null, rootSessionId(updated), updated.parentId ?? null, updated.originId ?? null, updated.profile, updated.activeModel ? JSON.stringify(updated.activeModel) : null, updated.workspace ?? null, updated.title ?? "Untitled Session", JSON.stringify(updated.metadata ?? {}), updated.updatedAt, updated.updatedAt, id);
101
101
  return this.get(id);
102
102
  }
103
+ claimOutputRenderSequence(piboSessionId, minimum) {
104
+ return this.dataStore.transaction(() => {
105
+ const session = this.db.prepare("SELECT 1 AS present FROM sessions WHERE id = ? AND deleted_at IS NULL").get(piboSessionId);
106
+ if (!session)
107
+ return minimum;
108
+ const row = this.db.prepare("SELECT high_water FROM session_output_render_high_water WHERE pibo_session_id = ?").get(piboSessionId);
109
+ const current = Math.max(row?.high_water ?? 0, this.durableOutputRenderSequenceHighWater(piboSessionId));
110
+ const next = Math.max(minimum, current + 1);
111
+ this.db.prepare(`
112
+ INSERT INTO session_output_render_high_water (pibo_session_id, high_water, updated_at)
113
+ VALUES (?, ?, ?)
114
+ ON CONFLICT(pibo_session_id) DO UPDATE SET
115
+ high_water = MAX(session_output_render_high_water.high_water, excluded.high_water),
116
+ updated_at = CASE
117
+ WHEN excluded.high_water > session_output_render_high_water.high_water THEN excluded.updated_at
118
+ ELSE session_output_render_high_water.updated_at
119
+ END
120
+ `).run(piboSessionId, next, new Date().toISOString());
121
+ return next;
122
+ });
123
+ }
124
+ durableOutputRenderSequenceHighWater(piboSessionId) {
125
+ const row = this.db.prepare(`
126
+ SELECT MAX(MAX(
127
+ COALESCE(CAST(json_extract(attributes_json, '$.renderSequence') AS INTEGER), 0),
128
+ COALESCE(session_sequence, 0)
129
+ )) AS high_water
130
+ FROM event_log
131
+ WHERE session_id = ?
132
+ `).get(piboSessionId);
133
+ const value = row?.high_water;
134
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
135
+ }
136
+ observeOutputRenderSequence(piboSessionId, sequence) {
137
+ this.dataStore.transaction(() => {
138
+ this.db.prepare(`
139
+ INSERT INTO session_output_render_high_water (pibo_session_id, high_water, updated_at)
140
+ SELECT id, ?, ? FROM sessions WHERE id = ? AND deleted_at IS NULL
141
+ ON CONFLICT(pibo_session_id) DO UPDATE SET
142
+ high_water = MAX(session_output_render_high_water.high_water, excluded.high_water),
143
+ updated_at = CASE
144
+ WHEN excluded.high_water > session_output_render_high_water.high_water THEN excluded.updated_at
145
+ ELSE session_output_render_high_water.updated_at
146
+ END
147
+ `).run(sequence, new Date().toISOString(), piboSessionId);
148
+ });
149
+ }
150
+ claimOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId) {
151
+ return this.dataStore.transaction(() => {
152
+ const durable = this.db.prepare(`
153
+ SELECT MAX(COALESCE(CAST(json_extract(attributes_json, '$.toolInvocationOrdinal') AS INTEGER), 0)) AS maximum
154
+ FROM event_log
155
+ WHERE session_id = ? AND event_id = ? AND tool_call_id = ?
156
+ `).get(piboSessionId, eventId, toolCallId);
157
+ const minimumNextOrdinal = typeof durable.maximum === "number" && Number.isSafeInteger(durable.maximum)
158
+ ? durable.maximum + 1
159
+ : 0;
160
+ const row = this.db.prepare(`
161
+ INSERT INTO session_tool_invocation_counters (
162
+ pibo_session_id, event_id, tool_call_id, next_ordinal, updated_at
163
+ ) VALUES (?, ?, ?, ? + 1, ?)
164
+ ON CONFLICT(pibo_session_id, event_id, tool_call_id) DO UPDATE SET
165
+ next_ordinal = MAX(session_tool_invocation_counters.next_ordinal, ? ) + 1,
166
+ updated_at = excluded.updated_at
167
+ RETURNING next_ordinal - 1 AS ordinal
168
+ `).get(piboSessionId, eventId, toolCallId, minimumNextOrdinal, new Date().toISOString(), minimumNextOrdinal);
169
+ return row.ordinal;
170
+ });
171
+ }
172
+ observeOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId, ordinal) {
173
+ this.dataStore.transaction(() => {
174
+ this.db.prepare(`
175
+ INSERT INTO session_tool_invocation_counters (
176
+ pibo_session_id, event_id, tool_call_id, next_ordinal, updated_at
177
+ ) VALUES (?, ?, ?, ?, ?)
178
+ ON CONFLICT(pibo_session_id, event_id, tool_call_id) DO UPDATE SET
179
+ next_ordinal = MAX(session_tool_invocation_counters.next_ordinal, excluded.next_ordinal),
180
+ updated_at = CASE
181
+ WHEN excluded.next_ordinal > session_tool_invocation_counters.next_ordinal THEN excluded.updated_at
182
+ ELSE session_tool_invocation_counters.updated_at
183
+ END
184
+ `).run(piboSessionId, eventId, toolCallId, ordinal + 1, new Date().toISOString());
185
+ });
186
+ }
187
+ claimOrAttachOutputToolInvocation(input) {
188
+ return this.dataStore.transaction(() => {
189
+ const rows = this.db.prepare(`
190
+ SELECT invocation_ordinal, call_fingerprint, status, seen_call
191
+ FROM session_tool_invocations
192
+ WHERE pibo_session_id = ? AND event_id = ? AND tool_call_id = ?
193
+ ORDER BY invocation_ordinal DESC
194
+ `).all(input.piboSessionId, input.eventId, input.toolCallId);
195
+ const latestOpen = rows.find((row) => row.status === "open");
196
+ let ordinal;
197
+ if (input.eventType === "tool_call"
198
+ && latestOpen
199
+ && (latestOpen.seen_call === 0 || latestOpen.call_fingerprint === input.callFingerprint)) {
200
+ ordinal = latestOpen.invocation_ordinal;
201
+ }
202
+ else if (input.eventType !== "tool_call" && latestOpen) {
203
+ ordinal = latestOpen.invocation_ordinal;
204
+ }
205
+ else if (input.eventType !== "tool_call" && rows[0]) {
206
+ // A lifecycle replay after completion belongs to the last proven
207
+ // invocation. Only a new tool_call may cross a closed boundary.
208
+ ordinal = rows[0].invocation_ordinal;
209
+ }
210
+ else {
211
+ ordinal = this.claimOutputToolInvocationOrdinal(input.piboSessionId, input.eventId, input.toolCallId);
212
+ }
213
+ this.persistToolInvocationTransition(input, ordinal);
214
+ return ordinal;
215
+ });
216
+ }
217
+ observeOutputToolInvocation(input) {
218
+ this.dataStore.transaction(() => {
219
+ this.observeOutputToolInvocationOrdinal(input.piboSessionId, input.eventId, input.toolCallId, input.ordinal);
220
+ this.persistToolInvocationTransition(input, input.ordinal);
221
+ });
222
+ }
223
+ persistToolInvocationTransition(input, ordinal) {
224
+ const timestamp = new Date().toISOString();
225
+ const seenCall = input.eventType === "tool_call" ? 1 : 0;
226
+ const seenStarted = input.eventType === "tool_execution_started" ? 1 : 0;
227
+ const seenUpdated = input.eventType === "tool_execution_updated" ? 1 : 0;
228
+ const seenFinished = input.eventType === "tool_execution_finished" ? 1 : 0;
229
+ this.db.prepare(`
230
+ INSERT INTO session_tool_invocations (
231
+ pibo_session_id, event_id, tool_call_id, invocation_ordinal,
232
+ call_fingerprint, status, seen_call, seen_started, seen_updated, seen_finished,
233
+ created_at, updated_at
234
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
235
+ ON CONFLICT(pibo_session_id, event_id, tool_call_id, invocation_ordinal) DO UPDATE SET
236
+ call_fingerprint = COALESCE(session_tool_invocations.call_fingerprint, excluded.call_fingerprint),
237
+ status = CASE
238
+ WHEN session_tool_invocations.status = 'closed' OR excluded.status = 'closed' THEN 'closed'
239
+ ELSE 'open'
240
+ END,
241
+ seen_call = MAX(session_tool_invocations.seen_call, excluded.seen_call),
242
+ seen_started = MAX(session_tool_invocations.seen_started, excluded.seen_started),
243
+ seen_updated = MAX(session_tool_invocations.seen_updated, excluded.seen_updated),
244
+ seen_finished = MAX(session_tool_invocations.seen_finished, excluded.seen_finished),
245
+ updated_at = excluded.updated_at
246
+ `).run(input.piboSessionId, input.eventId, input.toolCallId, ordinal, input.callFingerprint ?? null, seenFinished ? "closed" : "open", seenCall, seenStarted, seenUpdated, seenFinished, timestamp, timestamp);
247
+ }
103
248
  delete(id) {
104
249
  const result = this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
105
250
  return Number(result.changes ?? 0) > 0;
@@ -48,6 +48,8 @@ export class InMemoryPiboSessionStore {
48
48
  byId = new Map();
49
49
  byPiSessionId = new Map();
50
50
  byNativeSession = new Map();
51
+ outputRenderHighWater = new Map();
52
+ outputToolInvocationNextOrdinal = new Map();
51
53
  get(id) {
52
54
  return this.byId.get(id);
53
55
  }
@@ -101,6 +103,33 @@ export class InMemoryPiboSessionStore {
101
103
  this.set(updated, existing.piSessionId, existing.runtimeBinding);
102
104
  return updated;
103
105
  }
106
+ claimOutputRenderSequence(piboSessionId, minimum) {
107
+ const session = this.byId.get(piboSessionId);
108
+ if (!session)
109
+ return minimum;
110
+ const current = Math.max(this.outputRenderHighWater.get(piboSessionId) ?? 0, outputRenderSequenceHighWater(session.metadata));
111
+ const next = Math.max(minimum, current + 1);
112
+ this.outputRenderHighWater.set(piboSessionId, next);
113
+ return next;
114
+ }
115
+ observeOutputRenderSequence(piboSessionId, sequence) {
116
+ const session = this.byId.get(piboSessionId);
117
+ if (!session)
118
+ return;
119
+ const current = Math.max(this.outputRenderHighWater.get(piboSessionId) ?? 0, outputRenderSequenceHighWater(session.metadata));
120
+ if (sequence > current)
121
+ this.outputRenderHighWater.set(piboSessionId, sequence);
122
+ }
123
+ claimOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId) {
124
+ const key = outputToolInvocationCounterKey(piboSessionId, eventId, toolCallId);
125
+ const ordinal = this.outputToolInvocationNextOrdinal.get(key) ?? 0;
126
+ this.outputToolInvocationNextOrdinal.set(key, ordinal + 1);
127
+ return ordinal;
128
+ }
129
+ observeOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId, ordinal) {
130
+ const key = outputToolInvocationCounterKey(piboSessionId, eventId, toolCallId);
131
+ this.outputToolInvocationNextOrdinal.set(key, Math.max(this.outputToolInvocationNextOrdinal.get(key) ?? 0, ordinal + 1));
132
+ }
104
133
  getRuntimeBinding(id) {
105
134
  const session = this.byId.get(id);
106
135
  if (!session)
@@ -133,6 +162,12 @@ export class InMemoryPiboSessionStore {
133
162
  const nativeKey = runtimeBindingNativeKey(existing.runtimeBinding);
134
163
  if (nativeKey)
135
164
  this.byNativeSession.delete(nativeKey);
165
+ this.outputRenderHighWater.delete(id);
166
+ const counterPrefix = `${JSON.stringify([id]).slice(0, -1)},`;
167
+ for (const key of this.outputToolInvocationNextOrdinal.keys()) {
168
+ if (key.startsWith(counterPrefix))
169
+ this.outputToolInvocationNextOrdinal.delete(key);
170
+ }
136
171
  return true;
137
172
  }
138
173
  find(input) {
@@ -165,6 +200,13 @@ export class InMemoryPiboSessionStore {
165
200
  return sessions.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
166
201
  }
167
202
  }
203
+ function outputRenderSequenceHighWater(metadata) {
204
+ const value = metadata?.outputRenderSequenceHighWater;
205
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
206
+ }
207
+ function outputToolInvocationCounterKey(piboSessionId, eventId, toolCallId) {
208
+ return JSON.stringify([piboSessionId, eventId, toolCallId]);
209
+ }
168
210
  export function matchesFindInput(session, input) {
169
211
  if (input.ids && !input.ids.includes(session.id))
170
212
  return false;