@pasko70/pibo 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) 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/session-router.js +33 -105
  28. package/dist/data/pibo-store.js +8 -1
  29. package/dist/data/schema.js +125 -5
  30. package/dist/debug/agents.js +91 -89
  31. package/dist/debug/index.js +2 -0
  32. package/dist/debug/pty.js +127 -48
  33. package/dist/gateway/cli.js +4 -0
  34. package/dist/loops/channel.js +3 -1
  35. package/dist/loops/cli.js +2 -1
  36. package/dist/loops/service.js +90 -11
  37. package/dist/loops/store.js +100 -74
  38. package/dist/mcp/agent-context.js +13 -26
  39. package/dist/mcp/commands/info.js +3 -1
  40. package/dist/mcp/config-command.js +46 -2
  41. package/dist/mcp/config.js +18 -4
  42. package/dist/plugins/context-files-store.js +6 -6
  43. package/dist/previews/base-url.js +34 -0
  44. package/dist/previews/config.js +2 -32
  45. package/dist/ralph/store.js +5 -0
  46. package/dist/reliability/store.js +174 -111
  47. package/dist/session-ui/terminalRows.js +2 -1
  48. package/dist/subagents/observation-query.js +121 -0
  49. package/dist/tools/agent-browser-leases.js +58 -23
  50. package/dist/tools/agent-browser-wrapper.js +1 -1
  51. package/dist/tools/browser-use-leases.js +129 -16
  52. package/dist/tools/browser-use-wrapper.js +1 -1
  53. package/dist/tools/index.js +27 -11
  54. package/dist/tools/python-runtime.js +10 -3
  55. package/dist/tools/registry.js +1 -1
  56. package/dist/tools/runtime/node-backend.js +16 -5
  57. package/dist/tools/runtime/node-worker-source.js +112 -33
  58. package/dist/tools/runtime/python-backend.js +16 -5
  59. package/dist/tools/runtime/python-worker-source.js +167 -16
  60. package/dist/tools/runtime/registry.js +23 -10
  61. package/dist/user-skills/store.js +0 -1
  62. package/npm-shrinkwrap.json +9 -2
  63. package/package.json +8 -2
  64. package/scripts/docker-entrypoint.sh +46 -0
  65. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  66. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  67. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
  68. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
  69. package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
@@ -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"
@@ -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
+ }
@@ -68,6 +68,28 @@ async function withRegistryLock(status, action) {
68
68
  function isExpired(lease, now = new Date()) {
69
69
  return new Date(lease.expiresAt).getTime() <= now.getTime();
70
70
  }
71
+ function leaseTimestamp(lease) {
72
+ const updatedAt = Date.parse(lease.updatedAt);
73
+ if (Number.isFinite(updatedAt))
74
+ return updatedAt;
75
+ const createdAt = Date.parse(lease.createdAt);
76
+ return Number.isFinite(createdAt) ? createdAt : 0;
77
+ }
78
+ function deduplicateLeaseIdentities(registry) {
79
+ const leasesById = new Map();
80
+ for (const lease of registry.leases) {
81
+ const existing = leasesById.get(lease.id);
82
+ if (!existing) {
83
+ leasesById.set(lease.id, lease);
84
+ continue;
85
+ }
86
+ const preferLease = lease.status === 'active' && existing.status !== 'active'
87
+ || lease.status === existing.status && leaseTimestamp(lease) > leaseTimestamp(existing);
88
+ if (preferLease)
89
+ leasesById.set(lease.id, lease);
90
+ }
91
+ registry.leases = [...leasesById.values()];
92
+ }
71
93
  function copyFilter(source) {
72
94
  const base = source.split(/[\\/]/).pop() || '';
73
95
  return ![
@@ -114,6 +136,7 @@ export async function acquireAgentBrowserLease(status, options) {
114
136
  const maxSlots = options.maxSlots ?? 4;
115
137
  const result = await withRegistryLock(status, async () => {
116
138
  const registry = readRegistry(status);
139
+ deduplicateLeaseIdentities(registry);
117
140
  const now = new Date();
118
141
  for (const lease of registry.leases) {
119
142
  if (lease.status === 'active' && isExpired(lease, now)) {
@@ -131,34 +154,44 @@ export async function acquireAgentBrowserLease(status, options) {
131
154
  suggestion: 'Release a lease or rerun with a higher --max-slots value.',
132
155
  }));
133
156
  }
134
- const used = new Set(active.map((lease) => lease.slot));
135
- let slotNumber = 1;
136
- while (used.has(`slot-${String(slotNumber).padStart(3, '0')}`))
137
- slotNumber += 1;
138
- const slot = `slot-${String(slotNumber).padStart(3, '0')}`;
139
- const sessionName = `pibo-auth-${app}-${slot}`;
140
- const profileDir = leaseProfileDir(status, app, slot);
141
- const lease = {
142
- id: `${app}-${slot}`,
143
- app,
144
- holder,
145
- slot,
146
- sessionName,
147
- profileDir,
148
- status: 'active',
149
- createdAt: nowIso(),
150
- updatedAt: nowIso(),
151
- expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
152
- };
153
- await rm(profileDir, { recursive: true, force: true });
157
+ let lease = registry.leases.find((item) => item.app === app && item.status === 'released');
158
+ if (lease) {
159
+ lease.holder = holder;
160
+ lease.status = 'active';
161
+ lease.updatedAt = nowIso();
162
+ lease.expiresAt = new Date(now.getTime() + ttlMs).toISOString();
163
+ }
164
+ else {
165
+ const used = new Set(active.map((item) => item.slot));
166
+ let slotNumber = 1;
167
+ while (used.has(`slot-${String(slotNumber).padStart(3, '0')}`))
168
+ slotNumber += 1;
169
+ const slot = `slot-${String(slotNumber).padStart(3, '0')}`;
170
+ const sessionName = `pibo-auth-${app}-${slot}`;
171
+ const profileDir = leaseProfileDir(status, app, slot);
172
+ const createdAt = nowIso();
173
+ lease = {
174
+ id: `${app}-${slot}`,
175
+ app,
176
+ holder,
177
+ slot,
178
+ sessionName,
179
+ profileDir,
180
+ status: 'active',
181
+ createdAt,
182
+ updatedAt: createdAt,
183
+ expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
184
+ };
185
+ registry.leases.push(lease);
186
+ }
187
+ await rm(lease.profileDir, { recursive: true, force: true });
154
188
  const sourceTemplate = templateDir(status, app);
155
189
  if (existsSync(sourceTemplate)) {
156
- await cp(sourceTemplate, profileDir, { recursive: true, force: true, filter: copyFilter });
190
+ await cp(sourceTemplate, lease.profileDir, { recursive: true, force: true, filter: copyFilter });
157
191
  }
158
192
  else {
159
- await mkdir(profileDir, { recursive: true });
193
+ await mkdir(lease.profileDir, { recursive: true });
160
194
  }
161
- registry.leases.push(lease);
162
195
  await writeRegistry(status, registry);
163
196
  return lease;
164
197
  });
@@ -187,6 +220,7 @@ export async function listAgentBrowserLeases(status, json = false) {
187
220
  export async function releaseAgentBrowserLease(status, id, options) {
188
221
  await withRegistryLock(status, async () => {
189
222
  const registry = readRegistry(status);
223
+ deduplicateLeaseIdentities(registry);
190
224
  const lease = registry.leases.find((item) => item.id === id);
191
225
  if (!lease)
192
226
  throw new Error(`Agent Browser lease not found: ${id}`);
@@ -202,6 +236,7 @@ export async function reapStaleAgentBrowserLeases(status, json = false) {
202
236
  let released = 0;
203
237
  await withRegistryLock(status, async () => {
204
238
  const registry = readRegistry(status);
239
+ deduplicateLeaseIdentities(registry);
205
240
  const now = new Date();
206
241
  for (const lease of registry.leases) {
207
242
  if (lease.status === 'active' && isExpired(lease, now)) {
@@ -89,7 +89,7 @@ exec "$real_agent_browser" "\${args[@]}"
89
89
  `;
90
90
  }
91
91
  export function ensureAgentBrowserWrapper(status) {
92
- if (!status.homeDir || !status.executablePath)
92
+ if (!status.installed || !status.homeDir || !status.executablePath)
93
93
  return undefined;
94
94
  const wrapperPath = join(status.homeDir, 'bin', 'agent-browser');
95
95
  mkdirSync(dirname(wrapperPath), { recursive: true });