@pasko70/pibo 3.0.2 → 3.1.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 (72) hide show
  1. package/compute-image/Dockerfile +47 -0
  2. package/compute-image/Dockerfile.dockerignore +5 -0
  3. package/dist/agent-runtime/contract.js +78 -28
  4. package/dist/agent-runtime/portable-history.js +19 -9
  5. package/dist/agent-runtime/registry.js +8 -2
  6. package/dist/agent-runtime/routed-session.js +12 -6
  7. package/dist/agent-runtimes/pi/routed-session.js +10 -4
  8. package/dist/apps/chat/agent-store.js +55 -3
  9. package/dist/apps/chat/data/project-service.js +119 -15
  10. package/dist/apps/chat/data/room-service.js +21 -0
  11. package/dist/apps/chat/loop-api.js +10 -2
  12. package/dist/apps/chat/web-app.js +84 -25
  13. package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
  18. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  19. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  20. package/dist/apps/chat-ui/index.html +2 -2
  21. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  22. package/dist/apps/chat-vscode-web/index.html +1 -1
  23. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  24. package/dist/compute/cli.js +35 -16
  25. package/dist/compute/docker.js +341 -94
  26. package/dist/config/config.js +3 -0
  27. package/dist/core/output-render-sequence.js +148 -1
  28. package/dist/core/session-router.js +33 -105
  29. package/dist/data/ingest-service.js +14 -6
  30. package/dist/data/pibo-store.js +8 -1
  31. package/dist/data/schema.js +135 -5
  32. package/dist/debug/agents.js +91 -89
  33. package/dist/debug/index.js +2 -0
  34. package/dist/debug/pty.js +127 -48
  35. package/dist/gateway/cli.js +4 -0
  36. package/dist/loops/channel.js +3 -1
  37. package/dist/loops/cli.js +2 -1
  38. package/dist/loops/service.js +90 -11
  39. package/dist/loops/store.js +100 -74
  40. package/dist/mcp/agent-context.js +13 -26
  41. package/dist/mcp/commands/info.js +3 -1
  42. package/dist/mcp/config-command.js +46 -2
  43. package/dist/mcp/config.js +18 -4
  44. package/dist/plugins/context-files-store.js +6 -6
  45. package/dist/previews/base-url.js +34 -0
  46. package/dist/previews/config.js +2 -32
  47. package/dist/ralph/store.js +5 -0
  48. package/dist/reliability/store.js +174 -111
  49. package/dist/session-ui/terminalRows.js +2 -1
  50. package/dist/sessions/pibo-data-store.js +81 -0
  51. package/dist/subagents/observation-query.js +121 -0
  52. package/dist/tools/agent-browser-leases.js +58 -23
  53. package/dist/tools/agent-browser-wrapper.js +1 -1
  54. package/dist/tools/browser-use-leases.js +129 -16
  55. package/dist/tools/browser-use-wrapper.js +1 -1
  56. package/dist/tools/index.js +27 -11
  57. package/dist/tools/python-runtime.js +10 -3
  58. package/dist/tools/registry.js +1 -1
  59. package/dist/tools/runtime/node-backend.js +16 -5
  60. package/dist/tools/runtime/node-worker-source.js +112 -33
  61. package/dist/tools/runtime/python-backend.js +16 -5
  62. package/dist/tools/runtime/python-worker-source.js +167 -16
  63. package/dist/tools/runtime/registry.js +23 -10
  64. package/dist/user-skills/store.js +0 -1
  65. package/npm-shrinkwrap.json +9 -2
  66. package/package.json +8 -2
  67. package/scripts/docker-entrypoint.sh +46 -0
  68. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  69. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  70. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
  71. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
  72. package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
@@ -1,45 +1,15 @@
1
- import { isIP } from "node:net";
2
1
  import { loadPiboConfig } from "../config/config.js";
2
+ import { parsePreviewBaseURL } from "./base-url.js";
3
3
  export const DEFAULT_PREVIEW_TTL_MINUTES = 8 * 60;
4
4
  export const DEFAULT_PREVIEW_TICKET_TTL_SECONDS = 60;
5
5
  export const DEFAULT_PREVIEW_SESSION_TTL_MINUTES = 8 * 60;
6
- function isLocalPreviewHostname(hostname) {
7
- return hostname === "localhost" || hostname.endsWith(".localhost");
8
- }
9
- function isValidDnsHostname(hostname) {
10
- return isIP(hostname) === 0 &&
11
- hostname.length <= 253 &&
12
- !hostname.endsWith(".") &&
13
- hostname.split(".").every((label) => label.length >= 1 &&
14
- label.length <= 63 &&
15
- /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label));
16
- }
17
6
  export function loadPreviewConfig() {
18
7
  return loadPiboConfig().preview ?? {};
19
8
  }
20
9
  export function requirePreviewBaseURL(value = loadPreviewConfig().baseURL) {
21
10
  if (!value)
22
11
  throw new Error("preview.baseURL is required. Set it with `pibo config set preview.baseURL https://preview.example.com`.");
23
- let url;
24
- try {
25
- url = new URL(value);
26
- }
27
- catch {
28
- throw new Error("preview.baseURL must be an absolute HTTP or HTTPS URL");
29
- }
30
- if (url.protocol !== "http:" && url.protocol !== "https:") {
31
- throw new Error("preview.baseURL must use http or https");
32
- }
33
- if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
34
- throw new Error("preview.baseURL must contain only scheme, hostname, and optional port");
35
- }
36
- if (!isValidDnsHostname(url.hostname)) {
37
- throw new Error("preview.baseURL hostname must be a DNS hostname without wildcards, IP literals, or a trailing dot");
38
- }
39
- if (url.protocol === "http:" && !isLocalPreviewHostname(url.hostname)) {
40
- throw new Error("preview.baseURL must use https except for localhost development");
41
- }
42
- return url;
12
+ return parsePreviewBaseURL(value);
43
13
  }
44
14
  export function previewPublicURL(previewId, baseURL = requirePreviewBaseURL()) {
45
15
  if (previewId.length > 63 || !/^pv-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(previewId)) {
@@ -407,6 +407,11 @@ export class PiboRalphStore {
407
407
  this.db.exec('COMMIT');
408
408
  return undefined;
409
409
  }
410
+ if (job.maxIterations !== undefined && (job.state.completedIterations ?? 0) >= job.maxIterations) {
411
+ this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = 0, updated_at = ? WHERE id = ?').run(timestamp, job.id);
412
+ this.db.exec('COMMIT');
413
+ return undefined;
414
+ }
410
415
  const run = this.createRunLocked(job, timestamp);
411
416
  const state = { ...job.state, runningAt: timestamp, lastRunAt: timestamp, lastRunId: run.id };
412
417
  this.updateJobStateLocked(job.id, state, timestamp);
@@ -5,6 +5,8 @@ import { ensurePrivatePiboHomeForPath, piboHomePath } from "../core/pibo-home.js
5
5
  import { protectPrivateFileSync } from "../core/private-path.js";
6
6
  import { sqliteTableColumns } from "../data/sqlite-schema.js";
7
7
  import { DatabaseSync } from "node:sqlite";
8
+ const MAX_ATTEMPTS_ERROR = "Job exhausted retry attempts.";
9
+ const MAX_ATTEMPTS_REASON = "max_attempts";
8
10
  function now() {
9
11
  return new Date().toISOString();
10
12
  }
@@ -352,41 +354,73 @@ export class PiboReliabilityStore {
352
354
  claimBatch(workerId, limit, options = {}) {
353
355
  const timestamp = now();
354
356
  const claimExpiresAt = new Date(Date.now() + (options.visibilityTimeoutMs ?? 30000)).toISOString();
355
- const clauses = [
356
- "((state = 'pending' AND run_at <= ?) OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
357
- "(expires_at IS NULL OR expires_at > ?)",
358
- ];
359
- const values = [timestamp, timestamp, timestamp];
360
- if (options.queue) {
361
- clauses.push("queue = ?");
362
- values.push(options.queue);
363
- }
364
- this.moveExpiredJobs(timestamp);
365
- const rows = this.db
366
- .prepare(`
367
- UPDATE pibo_jobs
368
- SET state = 'running',
369
- worker_id = ?,
370
- claim_expires_at = ?,
371
- claim_token = claim_token + 1,
372
- attempts = attempts + 1,
373
- updated_at = ?
374
- WHERE job_id IN (
375
- SELECT job_id FROM pibo_jobs
376
- WHERE ${clauses.join(" AND ")}
377
- ORDER BY priority DESC, run_at ASC, created_at ASC
378
- LIMIT ?
379
- )
380
- RETURNING *
381
- `)
382
- .all(workerId, claimExpiresAt, timestamp, ...values, clampLimit(limit));
383
- return rows.map(jobFromRow);
357
+ return this.inImmediateTransaction(() => {
358
+ const clauses = [
359
+ "((state = 'pending' AND run_at <= ?) OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
360
+ "(expires_at IS NULL OR expires_at > ?)",
361
+ "attempts < max_attempts",
362
+ ];
363
+ const values = [timestamp, timestamp, timestamp];
364
+ if (options.queue) {
365
+ clauses.push("queue = ?");
366
+ values.push(options.queue);
367
+ }
368
+ this.moveExpiredJobs(timestamp);
369
+ this.moveExhaustedRecoverableJobs(timestamp, { queue: options.queue });
370
+ const rows = this.db
371
+ .prepare(`
372
+ UPDATE pibo_jobs
373
+ SET state = 'running',
374
+ worker_id = ?,
375
+ claim_expires_at = ?,
376
+ claim_token = claim_token + 1,
377
+ attempts = attempts + 1,
378
+ updated_at = ?
379
+ WHERE job_id IN (
380
+ SELECT job_id FROM pibo_jobs
381
+ WHERE ${clauses.join(" AND ")}
382
+ ORDER BY priority DESC, run_at ASC, created_at ASC
383
+ LIMIT ?
384
+ )
385
+ RETURNING *
386
+ `)
387
+ .all(workerId, claimExpiresAt, timestamp, ...values, clampLimit(limit));
388
+ return rows.map(jobFromRow);
389
+ });
384
390
  }
385
391
  claimJob(jobId, workerId, visibilityTimeoutMs = 30000) {
386
392
  const timestamp = now();
387
393
  const claimExpiresAt = new Date(Date.now() + visibilityTimeoutMs).toISOString();
388
- const row = this.db
389
- .prepare(`
394
+ return this.inImmediateTransaction(() => {
395
+ this.moveExpiredJobs(timestamp);
396
+ this.moveExhaustedRecoverableJobs(timestamp, { jobId });
397
+ const row = this.db
398
+ .prepare(`
399
+ UPDATE pibo_jobs
400
+ SET state = 'running',
401
+ worker_id = ?,
402
+ claim_expires_at = ?,
403
+ claim_token = claim_token + 1,
404
+ attempts = attempts + 1,
405
+ updated_at = ?
406
+ WHERE job_id = ?
407
+ AND state = 'pending'
408
+ AND run_at <= ?
409
+ AND attempts < max_attempts
410
+ AND (expires_at IS NULL OR expires_at > ?)
411
+ RETURNING *
412
+ `)
413
+ .get(workerId, claimExpiresAt, timestamp, jobId, timestamp, timestamp);
414
+ return row ? jobFromRow(row) : undefined;
415
+ });
416
+ }
417
+ claimRecoverableJob(jobId, workerId, visibilityTimeoutMs = 30000, forcePending = false) {
418
+ const timestamp = now();
419
+ const claimExpiresAt = new Date(Date.now() + visibilityTimeoutMs).toISOString();
420
+ return this.inImmediateTransaction(() => {
421
+ this.moveExpiredJobs(timestamp);
422
+ this.moveExhaustedRecoverableJobs(timestamp, { jobId });
423
+ const row = this.db.prepare(`
390
424
  UPDATE pibo_jobs
391
425
  SET state = 'running',
392
426
  worker_id = ?,
@@ -395,34 +429,16 @@ export class PiboReliabilityStore {
395
429
  attempts = attempts + 1,
396
430
  updated_at = ?
397
431
  WHERE job_id = ?
398
- AND state = 'pending'
399
- AND run_at <= ?
432
+ AND (
433
+ (state = 'pending' AND (? = 1 OR run_at <= ?))
434
+ OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
435
+ )
436
+ AND attempts < max_attempts
400
437
  AND (expires_at IS NULL OR expires_at > ?)
401
438
  RETURNING *
402
- `)
403
- .get(workerId, claimExpiresAt, timestamp, jobId, timestamp, timestamp);
404
- return row ? jobFromRow(row) : undefined;
405
- }
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;
439
+ `).get(workerId, claimExpiresAt, timestamp, jobId, forcePending ? 1 : 0, timestamp, timestamp, timestamp);
440
+ return row ? jobFromRow(row) : undefined;
441
+ });
426
442
  }
427
443
  updateJobPayload(jobId, workerId, payload, claimToken) {
428
444
  const result = this.db.prepare(`
@@ -436,15 +452,23 @@ export class PiboReliabilityStore {
436
452
  }
437
453
  releaseJob(jobId, workerId, delayMs = 0, claimToken) {
438
454
  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;
455
+ return this.inImmediateTransaction(() => {
456
+ const row = this.getLiveWorkerJob(jobId, workerId, timestamp, claimToken);
457
+ if (!row)
458
+ return false;
459
+ if (row.attempts >= row.max_attempts) {
460
+ this.moveJobToDead(row, MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
461
+ return true;
462
+ }
463
+ const runAt = new Date(Date.parse(timestamp) + Math.max(0, delayMs)).toISOString();
464
+ const result = this.db.prepare(`
465
+ UPDATE pibo_jobs
466
+ SET state = 'pending', worker_id = NULL, claim_expires_at = NULL, run_at = ?, updated_at = ?
467
+ WHERE job_id = ? AND state = 'running' AND worker_id = ?
468
+ AND (? IS NULL OR claim_token = ?)
469
+ `).run(runAt, timestamp, jobId, workerId, claimToken ?? null, claimToken ?? null);
470
+ return Number(result.changes ?? 0) > 0;
471
+ });
448
472
  }
449
473
  ack(jobId, workerId, claimToken) {
450
474
  const timestamp = now();
@@ -469,7 +493,7 @@ export class PiboReliabilityStore {
469
493
  return false;
470
494
  const job = jobFromRow(row);
471
495
  if (job.attempts >= job.maxAttempts) {
472
- this.moveJobToDead(row, input.error ?? "Job exhausted retry attempts.", "max_attempts", timestamp);
496
+ this.moveJobToDead(row, input.error ?? MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
473
497
  return true;
474
498
  }
475
499
  const delayMs = input.delayMs ?? retryDelayMs(job.attempts, input);
@@ -602,16 +626,21 @@ export class PiboReliabilityStore {
602
626
  }
603
627
  hasRecoverableJobs(queue, includeDeferredPending = false) {
604
628
  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;
629
+ return this.inImmediateTransaction(() => {
630
+ this.moveExpiredJobs(timestamp);
631
+ this.moveExhaustedRecoverableJobs(timestamp, { queue });
632
+ return this.db.prepare(`
633
+ SELECT 1 AS found FROM pibo_jobs
634
+ WHERE queue = ?
635
+ AND (
636
+ (state = 'pending' AND (? = 1 OR run_at <= ?))
637
+ OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
638
+ )
639
+ AND attempts < max_attempts
640
+ AND (expires_at IS NULL OR expires_at > ?)
641
+ LIMIT 1
642
+ `).get(queue, includeDeferredPending ? 1 : 0, timestamp, timestamp, timestamp) !== undefined;
643
+ });
615
644
  }
616
645
  quarantineJob(jobId, reason, correlation = {}) {
617
646
  const row = this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(jobId);
@@ -749,46 +778,62 @@ export class PiboReliabilityStore {
749
778
  return Number(result.changes ?? 0);
750
779
  }
751
780
  recoverInterruptedRuns(workerId = `run-registry:${process.pid}`) {
752
- const rows = this.db.prepare("SELECT * FROM pibo_runs WHERE status = 'running'").all();
753
- const recovered = [];
754
- const timestamp = now();
755
- for (const row of rows) {
756
- if (row.job_id && this.hasUnexpiredJobClaim(row.job_id, timestamp, workerId))
757
- continue;
758
- const run = runFromRow(row);
759
- if (run.timeoutAt && run.timeoutAt <= timestamp) {
760
- const error = `Run deadline ${run.timeoutAt} elapsed before the interrupted runtime recovered.`;
761
- if (run.jobId)
762
- this.moveLiveJobToDead(run.jobId, error, "timeout", timestamp);
763
- recovered.push(this.updateRun(run.runId, {
764
- status: "timed_out",
765
- error,
766
- timeoutPhase: "lifetime",
767
- summary: `${run.toolName} run started successfully, then reached its configured timeout.`,
768
- completedAt: timestamp,
769
- }) ?? run);
770
- continue;
771
- }
772
- if (run.retryable && run.maxAttempts > 1) {
773
- if (run.jobId)
774
- this.releaseJobForRetry(run.jobId, timestamp);
775
- recovered.push(this.updateRun(run.runId, {
776
- status: "queued",
777
- summary: `${run.toolName} run is queued for retry after interruption.`,
778
- }) ?? run);
779
- }
780
- else {
781
- if (run.jobId)
782
- this.moveLiveJobToDead(run.jobId, "Run was interrupted before completion and the tool is not retryable.", "interrupted", timestamp);
781
+ return this.inImmediateTransaction(() => {
782
+ const rows = this.db.prepare("SELECT * FROM pibo_runs WHERE status = 'running'").all();
783
+ const recovered = [];
784
+ const timestamp = now();
785
+ for (const row of rows) {
786
+ if (row.job_id && this.hasUnexpiredJobClaim(row.job_id, timestamp, workerId))
787
+ continue;
788
+ const run = runFromRow(row);
789
+ if (run.timeoutAt && run.timeoutAt <= timestamp) {
790
+ const error = `Run deadline ${run.timeoutAt} elapsed before the interrupted runtime recovered.`;
791
+ if (run.jobId)
792
+ this.moveLiveJobToDead(run.jobId, error, "timeout", timestamp);
793
+ recovered.push(this.updateRun(run.runId, {
794
+ status: "timed_out",
795
+ error,
796
+ timeoutPhase: "lifetime",
797
+ summary: `${run.toolName} run started successfully, then reached its configured timeout.`,
798
+ completedAt: timestamp,
799
+ }) ?? run);
800
+ continue;
801
+ }
802
+ const jobRow = run.jobId
803
+ ? this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(run.jobId)
804
+ : undefined;
805
+ if (run.retryable && jobRow && jobRow.attempts < jobRow.max_attempts) {
806
+ this.releaseJobForRetry(jobRow.job_id, timestamp);
807
+ recovered.push(this.updateRun(run.runId, {
808
+ status: "queued",
809
+ summary: `${run.toolName} run is queued for retry after interruption.`,
810
+ }) ?? run);
811
+ continue;
812
+ }
813
+ if (run.retryable && jobRow && jobRow.attempts >= jobRow.max_attempts) {
814
+ this.moveJobToDead(jobRow, MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
815
+ recovered.push(this.updateRun(run.runId, {
816
+ status: "failed",
817
+ error: MAX_ATTEMPTS_ERROR,
818
+ summary: `${run.toolName} run failed after exhausting retry attempts.`,
819
+ completedAt: timestamp,
820
+ }) ?? run);
821
+ continue;
822
+ }
823
+ const error = run.retryable
824
+ ? "Run was interrupted before completion and its retry job is no longer available."
825
+ : "Run was interrupted before completion and the tool is not retryable.";
826
+ if (jobRow)
827
+ this.moveJobToDead(jobRow, error, "interrupted", timestamp);
783
828
  recovered.push(this.updateRun(run.runId, {
784
829
  status: "failed",
785
- error: "Run was interrupted before completion and the tool is not retryable.",
830
+ error,
786
831
  summary: `${run.toolName} run failed.`,
787
832
  completedAt: timestamp,
788
833
  }) ?? run);
789
834
  }
790
- }
791
- return recovered;
835
+ return recovered;
836
+ });
792
837
  }
793
838
  close() {
794
839
  this.db.close();
@@ -843,6 +888,24 @@ export class PiboReliabilityStore {
843
888
  for (const row of expired)
844
889
  this.moveJobToDead(row, row.last_error ?? "Job expired.", "expired", timestamp);
845
890
  }
891
+ moveExhaustedRecoverableJobs(timestamp, options = {}) {
892
+ const clauses = [
893
+ "attempts >= max_attempts",
894
+ "(state = 'pending' OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
895
+ ];
896
+ const values = [timestamp];
897
+ if (options.jobId) {
898
+ clauses.push("job_id = ?");
899
+ values.push(options.jobId);
900
+ }
901
+ if (options.queue) {
902
+ clauses.push("queue = ?");
903
+ values.push(options.queue);
904
+ }
905
+ const exhausted = this.db.prepare(`SELECT * FROM pibo_jobs WHERE ${clauses.join(" AND ")}`).all(...values);
906
+ for (const row of exhausted)
907
+ this.moveJobToDead(row, MAX_ATTEMPTS_ERROR, MAX_ATTEMPTS_REASON, timestamp);
908
+ }
846
909
  hasUnexpiredJobClaim(jobId, timestamp, workerId) {
847
910
  const row = this.db
848
911
  .prepare(`
@@ -63,7 +63,8 @@ function applyToolDisplayMode(rows, mode) {
63
63
  });
64
64
  }
65
65
  function isToolDisplayRow(row) {
66
- return row.id.startsWith("terminal:tool:")
66
+ return row.sourceNodeIds.some((nodeId) => parseTraceToolNodeIdentity(nodeId) !== undefined)
67
+ || row.id.startsWith("terminal:tool:")
67
68
  || row.kind === "tool.call"
68
69
  || row.kind === "tool.image"
69
70
  || row.kind === "tool.group.exploring"
@@ -147,6 +147,65 @@ export class PiboDataSessionStore {
147
147
  `).run(sequence, new Date().toISOString(), piboSessionId);
148
148
  });
149
149
  }
150
+ claimOrAttachOutputPart(input) {
151
+ return this.dataStore.transaction(() => {
152
+ const indexAttribute = outputPartIndexAttribute(input.kind);
153
+ const eventTypes = outputPartEventTypes(input.kind);
154
+ const placeholders = eventTypes.map(() => "?").join(", ");
155
+ const rows = this.db.prepare(`
156
+ SELECT type, attributes_json
157
+ FROM event_log
158
+ WHERE session_id = ? AND event_id = ? AND type IN (${placeholders})
159
+ ORDER BY stream_id ASC
160
+ `).all(input.piboSessionId, input.eventId, ...eventTypes);
161
+ let maximum = -1;
162
+ const latestTypeByIndex = new Map();
163
+ for (const row of rows) {
164
+ const attributes = parseJsonObject(row.attributes_json);
165
+ const index = attributes[indexAttribute];
166
+ if (typeof index !== "number" || !Number.isSafeInteger(index) || index < 0)
167
+ continue;
168
+ maximum = Math.max(maximum, index);
169
+ if (attributes.outputPartFingerprint === input.fingerprint
170
+ || attributes.identityFingerprint === input.identityFingerprint) {
171
+ this.observeOutputPartIndex(input, index);
172
+ return index;
173
+ }
174
+ latestTypeByIndex.set(index, row.type);
175
+ }
176
+ const latestOpen = [...latestTypeByIndex.entries()]
177
+ .filter(([, eventType]) => !outputPartEventIsTerminal(eventType))
178
+ .sort(([left], [right]) => right - left)[0]?.[0];
179
+ if (latestOpen !== undefined) {
180
+ this.observeOutputPartIndex(input, latestOpen);
181
+ return latestOpen;
182
+ }
183
+ const minimum = Math.max(input.proposedIndex, maximum + 1);
184
+ const row = this.db.prepare(`
185
+ INSERT INTO session_output_part_counters (
186
+ pibo_session_id, event_id, part_kind, next_index, updated_at
187
+ ) VALUES (?, ?, ?, ? + 1, ?)
188
+ ON CONFLICT(pibo_session_id, event_id, part_kind) DO UPDATE SET
189
+ next_index = MAX(session_output_part_counters.next_index, ?) + 1,
190
+ updated_at = excluded.updated_at
191
+ RETURNING next_index - 1 AS part_index
192
+ `).get(input.piboSessionId, input.eventId, input.kind, minimum, new Date().toISOString(), minimum);
193
+ return row.part_index;
194
+ });
195
+ }
196
+ observeOutputPartIndex(input, index) {
197
+ this.db.prepare(`
198
+ INSERT INTO session_output_part_counters (
199
+ pibo_session_id, event_id, part_kind, next_index, updated_at
200
+ ) VALUES (?, ?, ?, ?, ?)
201
+ ON CONFLICT(pibo_session_id, event_id, part_kind) DO UPDATE SET
202
+ next_index = MAX(session_output_part_counters.next_index, excluded.next_index),
203
+ updated_at = CASE
204
+ WHEN excluded.next_index > session_output_part_counters.next_index THEN excluded.updated_at
205
+ ELSE session_output_part_counters.updated_at
206
+ END
207
+ `).run(input.piboSessionId, input.eventId, input.kind, index + 1, new Date().toISOString());
208
+ }
150
209
  claimOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId) {
151
210
  return this.dataStore.transaction(() => {
152
211
  const durable = this.db.prepare(`
@@ -562,6 +621,28 @@ function runtimeBindingFromRow(row) {
562
621
  function isRuntimeBindingState(value) {
563
622
  return value === "unbound" || value === "bound" || value === "missing" || value === "error";
564
623
  }
624
+ function outputPartIndexAttribute(kind) {
625
+ switch (kind) {
626
+ case "assistant": return "assistantIndex";
627
+ case "thinking": return "thinkingIndex";
628
+ case "usage": return "usageIndex";
629
+ case "compaction": return "compactionIndex";
630
+ }
631
+ }
632
+ function outputPartEventTypes(kind) {
633
+ switch (kind) {
634
+ case "assistant": return ["assistant_delta", "assistant_message"];
635
+ case "thinking": return ["thinking_started", "thinking_delta", "thinking_finished"];
636
+ case "usage": return ["assistant_usage"];
637
+ case "compaction": return ["compaction_start", "compaction_end"];
638
+ }
639
+ }
640
+ function outputPartEventIsTerminal(eventType) {
641
+ return eventType === "assistant_message"
642
+ || eventType === "thinking_finished"
643
+ || eventType === "assistant_usage"
644
+ || eventType === "compaction_end";
645
+ }
565
646
  function parseJsonObject(json) {
566
647
  if (!json)
567
648
  return {};
@@ -0,0 +1,121 @@
1
+ import { PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES, normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, normalizePiboAgentObservationToolDetail, parsePiboAgentObservationTimestamp, piboAgentObservationKind, piboAgentObservationToolSummary, } from "./observations.js";
2
+ export function preparePiboAgentObservationQuery(input = {}) {
3
+ const order = normalizePiboAgentObservationOrder(input.order);
4
+ const limit = normalizePiboAgentObservationLimit(input.limit);
5
+ const toolDetail = normalizePiboAgentObservationToolDetail(input.toolDetail);
6
+ const afterSequence = normalizePiboAgentObservationCursor(input.afterSequence);
7
+ const since = parsePiboAgentObservationTimestamp(input.since, "since");
8
+ const until = parsePiboAgentObservationTimestamp(input.until, "until");
9
+ if (since !== undefined && until !== undefined && since > until) {
10
+ throw new Error("Agent observation since must not be after until.");
11
+ }
12
+ if (input.toolCallIds && input.toolCallIds.length > 50) {
13
+ throw new Error("Agent observation toolCallIds must contain at most 50 entries.");
14
+ }
15
+ const requestIds = input.requestIds ? new Set(input.requestIds) : undefined;
16
+ const toolCallIds = input.toolCallIds ? new Set(input.toolCallIds) : undefined;
17
+ const agentIds = input.agentIds ? new Set(input.agentIds) : undefined;
18
+ const names = input.names ? new Set(input.names) : undefined;
19
+ const threadKeys = input.threadKeys ? new Set(input.threadKeys) : undefined;
20
+ const eventTypes = input.eventTypes ? new Set(input.eventTypes) : undefined;
21
+ const kinds = input.kinds ? new Set(input.kinds) : undefined;
22
+ const roles = input.roles ? new Set(input.roles) : undefined;
23
+ const textContains = input.textContains?.toLowerCase();
24
+ const defaultMessageView = eventTypes === undefined && kinds === undefined;
25
+ const explicitlySelectsTools = input.eventTypes?.some((eventType) => piboAgentObservationKind(eventType) === "tool") === true
26
+ || input.kinds?.includes("tool") === true
27
+ || toolCallIds !== undefined;
28
+ const includeTools = input.includeTools === true || (input.includeTools === undefined && explicitlySelectsTools);
29
+ const defaultEventTypes = includeTools
30
+ ? [...PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, ...PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES]
31
+ : [...PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES];
32
+ const defaultEventTypeSet = new Set(defaultEventTypes);
33
+ const scanEventTypes = defaultMessageView
34
+ ? defaultEventTypes
35
+ : input.eventTypes && input.eventTypes.length > 0
36
+ ? [...input.eventTypes]
37
+ : undefined;
38
+ return {
39
+ filters: {
40
+ ...input,
41
+ ...(defaultMessageView ? { eventTypes: defaultEventTypes } : {}),
42
+ ...(afterSequence !== undefined ? { afterSequence } : {}),
43
+ order,
44
+ limit,
45
+ includeTools,
46
+ toolDetail,
47
+ includeDetails: input.includeDetails === true,
48
+ },
49
+ ...(afterSequence !== undefined ? { afterSequence } : {}),
50
+ order,
51
+ scanOrder: afterSequence !== undefined ? "asc" : order,
52
+ limit,
53
+ includeTools,
54
+ toolDetail,
55
+ ...(scanEventTypes ? { scanEventTypes } : {}),
56
+ matches(observation) {
57
+ if (requestIds && (!observation.requestId || !requestIds.has(observation.requestId)))
58
+ return false;
59
+ if (toolCallIds && (!observation.toolCallId || !toolCallIds.has(observation.toolCallId)))
60
+ return false;
61
+ if (agentIds && !agentIds.has(observation.agentId))
62
+ return false;
63
+ if (names && !names.has(observation.name))
64
+ return false;
65
+ if (threadKeys && (!observation.threadKey || !threadKeys.has(observation.threadKey)))
66
+ return false;
67
+ if (observation.kind === "tool" && !includeTools)
68
+ return false;
69
+ if (defaultMessageView && !defaultEventTypeSet.has(observation.eventType))
70
+ return false;
71
+ if (eventTypes && !eventTypes.has(observation.eventType))
72
+ return false;
73
+ if (kinds && !kinds.has(observation.kind))
74
+ return false;
75
+ if (roles && (!observation.role || !roles.has(observation.role)))
76
+ return false;
77
+ if (afterSequence !== undefined && observation.sequence <= afterSequence)
78
+ return false;
79
+ const createdAt = Date.parse(observation.createdAt);
80
+ if (since !== undefined && createdAt < since)
81
+ return false;
82
+ if (until !== undefined && createdAt > until)
83
+ return false;
84
+ if (textContains && !(observation.text ?? "").toLowerCase().includes(textContains))
85
+ return false;
86
+ return true;
87
+ },
88
+ };
89
+ }
90
+ export function selectPiboAgentObservationPage(observations, query, options = {}) {
91
+ const matches = [];
92
+ for (const observation of observations) {
93
+ if (!query.matches(observation))
94
+ continue;
95
+ matches.push(observation);
96
+ if (matches.length > query.limit)
97
+ break;
98
+ }
99
+ const pageLimited = matches.length > query.limit;
100
+ const selected = matches.slice(0, query.limit);
101
+ if (query.afterSequence !== undefined && query.order === "desc")
102
+ selected.reverse();
103
+ const projected = selected.map((observation) => {
104
+ const { details, ...visible } = observation;
105
+ const compact = visible.kind === "tool" && query.toolDetail === "summary"
106
+ ? { ...visible, text: piboAgentObservationToolSummary(visible.text, visible.isError, details) }
107
+ : visible;
108
+ return query.filters.includeDetails === true && details !== undefined ? { ...compact, details } : compact;
109
+ });
110
+ const evictedThrough = options.evictedThrough ?? 0;
111
+ const retentionTruncated = query.afterSequence === undefined
112
+ ? evictedThrough > 0
113
+ : query.afterSequence < evictedThrough;
114
+ const nextAfterSequence = projected.reduce((maximum, observation) => Math.max(maximum, observation.sequence), query.afterSequence === undefined ? 0 : Math.max(query.afterSequence, evictedThrough));
115
+ return {
116
+ filters: query.filters,
117
+ observations: projected,
118
+ nextAfterSequence,
119
+ truncated: retentionTruncated || pageLimited,
120
+ };
121
+ }