@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.
- package/compute-image/Dockerfile +47 -0
- package/compute-image/Dockerfile.dockerignore +5 -0
- package/dist/agent-runtime/contract.js +78 -28
- package/dist/agent-runtime/portable-history.js +19 -9
- package/dist/agent-runtime/registry.js +8 -2
- package/dist/agent-runtime/routed-session.js +12 -6
- package/dist/agent-runtimes/pi/routed-session.js +10 -4
- package/dist/apps/chat/agent-store.js +55 -3
- package/dist/apps/chat/data/project-service.js +119 -15
- package/dist/apps/chat/data/room-service.js +21 -0
- package/dist/apps/chat/loop-api.js +10 -2
- package/dist/apps/chat/web-app.js +84 -25
- package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
- package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/InkSessionApp.js +111 -12
- package/dist/compute/cli.js +35 -16
- package/dist/compute/docker.js +341 -94
- package/dist/config/config.js +3 -0
- package/dist/core/session-router.js +33 -105
- package/dist/data/pibo-store.js +8 -1
- package/dist/data/schema.js +125 -5
- package/dist/debug/agents.js +91 -89
- package/dist/debug/index.js +2 -0
- package/dist/debug/pty.js +127 -48
- package/dist/gateway/cli.js +4 -0
- package/dist/loops/channel.js +3 -1
- package/dist/loops/cli.js +2 -1
- package/dist/loops/service.js +90 -11
- package/dist/loops/store.js +100 -74
- package/dist/mcp/agent-context.js +13 -26
- package/dist/mcp/commands/info.js +3 -1
- package/dist/mcp/config-command.js +46 -2
- package/dist/mcp/config.js +18 -4
- package/dist/plugins/context-files-store.js +6 -6
- package/dist/previews/base-url.js +34 -0
- package/dist/previews/config.js +2 -32
- package/dist/ralph/store.js +5 -0
- package/dist/reliability/store.js +174 -111
- package/dist/session-ui/terminalRows.js +2 -1
- package/dist/subagents/observation-query.js +121 -0
- package/dist/tools/agent-browser-leases.js +58 -23
- package/dist/tools/agent-browser-wrapper.js +1 -1
- package/dist/tools/browser-use-leases.js +129 -16
- package/dist/tools/browser-use-wrapper.js +1 -1
- package/dist/tools/index.js +27 -11
- package/dist/tools/python-runtime.js +10 -3
- package/dist/tools/registry.js +1 -1
- package/dist/tools/runtime/node-backend.js +16 -5
- package/dist/tools/runtime/node-worker-source.js +112 -33
- package/dist/tools/runtime/python-backend.js +16 -5
- package/dist/tools/runtime/python-worker-source.js +167 -16
- package/dist/tools/runtime/registry.js +23 -10
- package/dist/user-skills/store.js +0 -1
- package/npm-shrinkwrap.json +9 -2
- package/package.json +8 -2
- package/scripts/docker-entrypoint.sh +46 -0
- package/scripts/prepare-agent-browser-wrapper.sh +70 -0
- package/scripts/prepare-browser-use-wrapper.sh +255 -0
- package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
- package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
- 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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
389
|
-
.
|
|
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
|
|
399
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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 ??
|
|
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.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
this.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
|
830
|
+
error,
|
|
786
831
|
summary: `${run.toolName} run failed.`,
|
|
787
832
|
completedAt: timestamp,
|
|
788
833
|
}) ?? run);
|
|
789
834
|
}
|
|
790
|
-
|
|
791
|
-
|
|
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.
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
status
|
|
149
|
-
createdAt
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 });
|