@pasko70/pibo 3.0.1 → 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 (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -604,6 +604,8 @@ async function runDebugEvents(args) {
604
604
  if (!selector)
605
605
  throw new Error("pibo debug events <pibo-session-id> show requires <stream-id-or-event-id>");
606
606
  const result = inspectDebugEventShow(piboSessionId, resolveDebugStore("chat"), selector, { ...detailOptions(options), payload: options.payload, raw: options.raw, field: options.field });
607
+ if (!result.event)
608
+ process.exitCode = 1;
607
609
  if (options.json)
608
610
  console.log(formatJson(result));
609
611
  else
@@ -639,7 +641,7 @@ async function runDebugJobs(args) {
639
641
  if (command === "list") {
640
642
  const jobs = reliability.listJobs({ queue: options.queue, limit: options.limit ? Number(options.limit) : undefined });
641
643
  if (options.json)
642
- console.log(formatJson({ jobs }));
644
+ console.log(formatJson({ jobs: jobs.map(observableJobRow) }));
643
645
  else
644
646
  console.log(formatRows(jobs.map(compactJobRow)));
645
647
  return;
@@ -647,7 +649,7 @@ async function runDebugJobs(args) {
647
649
  if (command === "dead") {
648
650
  const jobs = reliability.listDead({ queue: options.queue, limit: options.limit ? Number(options.limit) : undefined });
649
651
  if (options.json)
650
- console.log(formatJson({ jobs }));
652
+ console.log(formatJson({ jobs: jobs.map(observableDeadJobRow) }));
651
653
  else
652
654
  console.log(formatRows(jobs.map(compactDeadJobRow)));
653
655
  return;
@@ -976,26 +978,68 @@ function compactEventRow(event) {
976
978
  };
977
979
  }
978
980
  function compactJobRow(job) {
981
+ const correlation = outputPersistenceJobCorrelation(job.payload);
979
982
  return {
980
983
  jobId: job.jobId,
981
984
  queue: job.queue,
982
985
  state: job.state,
983
986
  runAt: job.runAt,
984
987
  attempts: `${job.attempts}/${job.maxAttempts}`,
988
+ piboSessionId: correlation.piboSessionId,
989
+ eventId: correlation.eventId,
990
+ phase: correlation.phase,
985
991
  workerId: job.workerId,
986
992
  lastError: job.lastError,
987
993
  };
988
994
  }
989
995
  function compactDeadJobRow(job) {
996
+ const correlation = outputPersistenceJobCorrelation(job.payload);
990
997
  return {
991
998
  jobId: job.jobId,
992
999
  queue: job.queue,
993
1000
  attempts: `${job.attempts}/${job.maxAttempts}`,
1001
+ piboSessionId: correlation.piboSessionId,
1002
+ eventId: correlation.eventId,
1003
+ phase: correlation.phase,
994
1004
  deadAt: job.deadAt,
995
1005
  deadReason: job.deadReason,
996
1006
  lastError: job.lastError,
997
1007
  };
998
1008
  }
1009
+ function observableJobRow(job) {
1010
+ return { ...compactJobRow(job), attempts: job.attempts, maxAttempts: job.maxAttempts };
1011
+ }
1012
+ function observableDeadJobRow(job) {
1013
+ return { ...compactDeadJobRow(job), attempts: job.attempts, maxAttempts: job.maxAttempts };
1014
+ }
1015
+ function outputPersistenceJobCorrelation(payload) {
1016
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
1017
+ return {};
1018
+ const envelope = payload;
1019
+ const state = envelope.state;
1020
+ let phase;
1021
+ if (state && typeof state === "object" && !Array.isArray(state)) {
1022
+ const deliveries = state.deliveries;
1023
+ if (Array.isArray(deliveries) && deliveries.length) {
1024
+ const rows = deliveries.filter((delivery) => !!delivery && typeof delivery === "object" && !Array.isArray(delivery));
1025
+ if (rows.length === deliveries.length) {
1026
+ if (rows.every((delivery) => delivery.sideEffectsDelivered === true || delivery.persisted === true))
1027
+ phase = "delivered";
1028
+ else if (rows.every((delivery) => delivery.reliabilityDelivered === true))
1029
+ phase = "side-effects";
1030
+ else if (rows.every((delivery) => delivery.v2 !== undefined))
1031
+ phase = "reliability";
1032
+ else
1033
+ phase = "v2";
1034
+ }
1035
+ }
1036
+ }
1037
+ return {
1038
+ ...(typeof envelope.piboSessionId === "string" ? { piboSessionId: envelope.piboSessionId } : {}),
1039
+ ...(typeof envelope.eventId === "string" ? { eventId: envelope.eventId } : {}),
1040
+ ...(phase ? { phase } : {}),
1041
+ };
1042
+ }
999
1043
  function compactRunRow(run) {
1000
1044
  return {
1001
1045
  runId: run.runId,
package/dist/debug/pty.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { performance } from "node:perf_hooks";
4
5
  const DEFAULT_ROWS = 24;
5
6
  const DEFAULT_COLS = 100;
6
7
  const DEFAULT_TIMEOUT_MS = 60_000;
@@ -95,6 +96,12 @@ if exit_code is None:
95
96
  exit_code = proc.wait(timeout=1)
96
97
  sys.exit(exit_code if exit_code is not None else 1)
97
98
  `;
99
+ class WallClockTimeoutError extends Error {
100
+ constructor(timeoutMs) {
101
+ super(`PTY command timed out after ${timeoutMs}ms`);
102
+ this.name = "WallClockTimeoutError";
103
+ }
104
+ }
98
105
  export async function runDebugPty(args) {
99
106
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
100
107
  printPtyDiscovery();
@@ -409,26 +416,31 @@ function parseSteps(value, source) {
409
416
  }
410
417
  async function executePtyScenario(input, options) {
411
418
  const scenario = normalizeScenario(input, options);
412
- const started = Date.now();
413
- const startedAt = new Date(started).toISOString();
419
+ const startedAt = new Date().toISOString();
420
+ const started = performance.now();
421
+ const deadline = createDeadline(scenario.timeoutMs, started);
414
422
  let runner;
415
423
  const assertions = [];
416
424
  let iterations = 0;
417
425
  let stopReason = "completed";
426
+ let exitCode = null;
427
+ let signal = null;
418
428
  try {
419
429
  validateProviderSafety(scenario, options);
420
430
  runner = await startRunner(scenario);
421
- let lastOutputAt = Date.now();
431
+ throwIfDeadlineExpired(deadline);
432
+ let lastOutputAt = performance.now();
422
433
  let outputLength = 0;
423
434
  const refreshOutputTimestamp = () => {
424
435
  const raw = runner?.getRawOutput() ?? "";
425
436
  if (raw.length !== outputLength) {
426
437
  outputLength = raw.length;
427
- lastOutputAt = Date.now();
438
+ lastOutputAt = performance.now();
428
439
  }
429
440
  return lastOutputAt;
430
441
  };
431
442
  for (const step of scenario.steps) {
443
+ throwIfDeadlineExpired(deadline);
432
444
  if (step.iteration === true) {
433
445
  iterations += 1;
434
446
  if (scenario.providerMode === "real" && scenario.maxIterations !== undefined && iterations > scenario.maxIterations) {
@@ -437,7 +449,8 @@ async function executePtyScenario(input, options) {
437
449
  }
438
450
  }
439
451
  await checkIdle(runner, refreshOutputTimestamp, scenario.idleTimeoutMs);
440
- await runStep(runner, scenario, step, assertions, refreshOutputTimestamp);
452
+ await runStep(runner, scenario, step, assertions, refreshOutputTimestamp, deadline);
453
+ throwIfDeadlineExpired(deadline);
441
454
  refreshOutputTimestamp();
442
455
  const clean = cleanTerminalText(runner.getRawOutput());
443
456
  const matchedStop = scenario.stopPatterns.find((pattern) => clean.includes(pattern));
@@ -446,32 +459,40 @@ async function executePtyScenario(input, options) {
446
459
  break;
447
460
  }
448
461
  }
449
- const exit = await waitForExitOrIdle(runner, Math.max(1, scenario.timeoutMs - (Date.now() - started)), scenario.idleTimeoutMs, refreshOutputTimestamp);
462
+ const stoppedByPattern = stopReason.startsWith("stop_pattern:");
463
+ if (stoppedByPattern)
464
+ await runner.terminate(stopReason);
465
+ const exit = await waitForExitOrIdle(runner, deadline, scenario.idleTimeoutMs, refreshOutputTimestamp);
466
+ exitCode = exit.exitCode;
467
+ signal = exit.signal;
450
468
  if (exit.idleTimedOut) {
451
469
  stopReason = "idle_timeout";
452
470
  throw new Error(`PTY command produced no output for ${scenario.idleTimeoutMs}ms`);
453
471
  }
454
472
  if (exit.timedOut) {
455
- stopReason = stopReason === "completed" ? "wall_clock_timeout" : stopReason;
456
- throw new Error(`PTY command timed out after ${scenario.timeoutMs}ms`);
473
+ throw new WallClockTimeoutError(scenario.timeoutMs);
457
474
  }
475
+ throwIfDeadlineExpired(deadline);
458
476
  const clean = cleanTerminalText(runner.getRawOutput());
459
477
  assertPatterns(clean, scenario.expect, scenario.reject, assertions);
460
- if (exit.exitCode !== 0) {
478
+ throwIfDeadlineExpired(deadline);
479
+ if (exit.exitCode !== 0 && !stoppedByPattern) {
461
480
  stopReason = `exit_code:${exit.exitCode ?? "signal"}`;
462
481
  throw new Error(`PTY command exited with status ${exit.exitCode ?? exit.signal ?? "unknown"}`);
463
482
  }
464
- const result = buildResult(scenario, runner, true, assertions, iterations, started, startedAt, stopReason, exit.exitCode, exit.signal);
483
+ const result = buildResult(scenario, runner, true, assertions, iterations, started, startedAt, stopReason, exitCode, signal);
465
484
  if (scenario.writeArtifactsOnSuccess)
466
485
  result.artifactDir = await writeArtifacts(scenario, result);
467
486
  return result;
468
487
  }
469
488
  catch (error) {
470
- if (stopReason === "completed")
489
+ if (error instanceof WallClockTimeoutError && stopReason === "completed")
490
+ stopReason = "wall_clock_timeout";
491
+ else if (stopReason === "completed")
471
492
  stopReason = runner ? "error" : "preflight_error";
472
493
  if (runner)
473
494
  await runner.terminate(stopReason);
474
- const result = buildResult(scenario, runner, false, assertions, iterations, started, startedAt, stopReason, null, null);
495
+ const result = buildResult(scenario, runner, false, assertions, iterations, started, startedAt, stopReason, exitCode, signal);
475
496
  result.artifactDir = await writeArtifacts(scenario, result, error instanceof Error ? error.message : String(error));
476
497
  const message = `${error instanceof Error ? error.message : String(error)}\nPTY artifacts: ${result.artifactDir}`;
477
498
  throw new Error(message);
@@ -601,6 +622,26 @@ function wrapChild(child, backend, ptyMethod) {
601
622
  driverStderr += `${error.message}\n`;
602
623
  events.push({ t: Date.now(), source: "system", kind: "error", detail: error.message });
603
624
  });
625
+ const waitForExit = async (timeoutMs) => {
626
+ if (exitState)
627
+ return { ...exitState, timedOut: false };
628
+ if (timeoutMs <= 0)
629
+ return { exitCode: null, signal: null, timedOut: true };
630
+ return new Promise((resolve) => {
631
+ let settled = false;
632
+ let timer;
633
+ const finish = (value) => {
634
+ if (settled)
635
+ return;
636
+ settled = true;
637
+ if (timer)
638
+ clearTimeout(timer);
639
+ resolve(value);
640
+ };
641
+ exitPromise.then((value) => finish({ ...value, timedOut: false }));
642
+ timer = setTimeout(() => finish({ exitCode: null, signal: null, timedOut: true }), timeoutMs);
643
+ });
644
+ };
604
645
  return {
605
646
  backend,
606
647
  ptyMethod,
@@ -615,51 +656,46 @@ function wrapChild(child, backend, ptyMethod) {
615
656
  if (exitState)
616
657
  return;
617
658
  child.kill("SIGTERM");
618
- await Promise.race([
619
- exitPromise,
620
- delay(1_000).then(() => {
621
- if (!exitState)
622
- child.kill("SIGKILL");
623
- }),
624
- ]);
625
- },
626
- async waitForExit(timeoutMs) {
627
- if (exitState)
628
- return { ...exitState, timedOut: false };
629
- const timeout = delay(timeoutMs).then(() => ({ exitCode: null, signal: null, timedOut: true }));
630
- const exit = exitPromise.then((value) => ({ ...value, timedOut: false }));
631
- return Promise.race([exit, timeout]);
659
+ if (await waitForChildExit(exitPromise, 1_000))
660
+ return;
661
+ child.kill("SIGKILL");
662
+ await waitForChildExit(exitPromise, 1_000);
632
663
  },
664
+ waitForExit,
633
665
  getRawOutput: () => rawOutput,
634
666
  getDriverStderr: () => driverStderr,
635
667
  getEvents: () => events,
636
668
  };
637
669
  }
638
- async function runStep(runner, scenario, step, assertions, markOutput) {
670
+ async function runStep(runner, scenario, step, assertions, markOutput, deadline) {
639
671
  if (step.waitFor !== undefined) {
640
- await waitForText(runner, step.waitFor, step.timeoutMs ?? scenario.timeoutMs, scenario.idleTimeoutMs, markOutput);
672
+ await waitForText(runner, step.waitFor, step.timeoutMs ?? scenario.timeoutMs, scenario.idleTimeoutMs, markOutput, deadline);
641
673
  return;
642
674
  }
643
675
  if (step.typeText !== undefined) {
644
676
  for (const char of step.typeText) {
677
+ throwIfDeadlineExpired(deadline);
645
678
  await runner.write(char);
646
- if (scenario.inputDelayMs > 0)
647
- await delay(scenario.inputDelayMs);
679
+ throwIfDeadlineExpired(deadline);
680
+ await delayWithinDeadline(scenario.inputDelayMs, deadline);
648
681
  }
649
682
  return;
650
683
  }
651
684
  if (step.writeBytes !== undefined) {
685
+ throwIfDeadlineExpired(deadline);
652
686
  await runner.write(step.writeBytes);
687
+ throwIfDeadlineExpired(deadline);
653
688
  return;
654
689
  }
655
690
  if (step.press !== undefined) {
691
+ throwIfDeadlineExpired(deadline);
656
692
  await runner.write(keySequence(step.press));
657
- if (scenario.inputDelayMs > 0)
658
- await delay(scenario.inputDelayMs);
693
+ throwIfDeadlineExpired(deadline);
694
+ await delayWithinDeadline(scenario.inputDelayMs, deadline);
659
695
  return;
660
696
  }
661
697
  if (step.sleepMs !== undefined) {
662
- await delay(step.sleepMs);
698
+ await delayWithinDeadline(step.sleepMs, deadline);
663
699
  return;
664
700
  }
665
701
  const clean = cleanTerminalText(runner.getRawOutput());
@@ -670,35 +706,41 @@ async function runStep(runner, scenario, step, assertions, markOutput) {
670
706
  if (step.reject !== undefined)
671
707
  assertPatterns(clean, [], [step.reject], assertions);
672
708
  }
673
- async function waitForText(runner, pattern, timeoutMs, idleTimeoutMs, markOutput) {
674
- const deadline = Date.now() + timeoutMs;
675
- while (Date.now() < deadline) {
709
+ async function waitForText(runner, pattern, timeoutMs, idleTimeoutMs, markOutput, globalDeadline) {
710
+ const stepDeadline = performance.now() + timeoutMs;
711
+ while (true) {
712
+ throwIfDeadlineExpired(globalDeadline);
676
713
  const lastOutputAt = markOutput();
677
714
  if (cleanTerminalText(runner.getRawOutput()).includes(pattern))
678
715
  return;
679
- if (Date.now() - lastOutputAt > idleTimeoutMs) {
716
+ const now = performance.now();
717
+ if (now - lastOutputAt > idleTimeoutMs) {
680
718
  await runner.terminate("idle_timeout");
681
719
  throw new Error(`PTY command produced no output for ${idleTimeoutMs}ms while waiting for ${JSON.stringify(pattern)}`);
682
720
  }
683
- await delay(50);
721
+ if (now >= stepDeadline)
722
+ break;
723
+ await delay(Math.max(1, Math.min(50, stepDeadline - now, remainingMs(globalDeadline), idleTimeoutMs - (now - lastOutputAt))));
684
724
  }
685
725
  throw new Error(`Timed out waiting for PTY output ${JSON.stringify(pattern)} after ${timeoutMs}ms`);
686
726
  }
687
- async function waitForExitOrIdle(runner, timeoutMs, idleTimeoutMs, markOutput) {
688
- const deadline = Date.now() + timeoutMs;
689
- while (Date.now() < deadline) {
727
+ async function waitForExitOrIdle(runner, deadline, idleTimeoutMs, markOutput) {
728
+ while (remainingMs(deadline) > 0) {
690
729
  const lastOutputAt = markOutput();
691
- const remaining = Math.max(1, Math.min(100, deadline - Date.now()));
730
+ const remaining = Math.max(1, Math.min(100, remainingMs(deadline)));
692
731
  const exit = await runner.waitForExit(remaining);
693
- if (!exit.timedOut)
732
+ if (!exit.timedOut) {
733
+ if (remainingMs(deadline) <= 0)
734
+ return { exitCode: null, signal: null, timedOut: true, idleTimedOut: false };
694
735
  return { ...exit, timedOut: false, idleTimedOut: false };
695
- if (Date.now() - lastOutputAt > idleTimeoutMs)
736
+ }
737
+ if (performance.now() - lastOutputAt > idleTimeoutMs)
696
738
  return { exitCode: null, signal: null, timedOut: false, idleTimedOut: true };
697
739
  }
698
740
  return { exitCode: null, signal: null, timedOut: true, idleTimedOut: false };
699
741
  }
700
742
  async function checkIdle(runner, lastOutputAt, idleTimeoutMs) {
701
- if (Date.now() - lastOutputAt() > idleTimeoutMs) {
743
+ if (performance.now() - lastOutputAt() > idleTimeoutMs) {
702
744
  await runner.terminate("idle_timeout");
703
745
  throw new Error(`PTY command produced no output for ${idleTimeoutMs}ms`);
704
746
  }
@@ -741,7 +783,7 @@ function cleanTerminalText(raw) {
741
783
  .replace(/\r/g, "\n");
742
784
  }
743
785
  function buildResult(scenario, runner, ok, assertions, iterations, started, startedAt, stopReason, exitCode, signal) {
744
- const ended = Date.now();
786
+ const endedAt = new Date().toISOString();
745
787
  return {
746
788
  ok,
747
789
  name: scenario.name,
@@ -750,8 +792,8 @@ function buildResult(scenario, runner, ok, assertions, iterations, started, star
750
792
  signal,
751
793
  stopReason,
752
794
  startedAt,
753
- endedAt: new Date(ended).toISOString(),
754
- durationMs: ended - started,
795
+ endedAt,
796
+ durationMs: Math.round(performance.now() - started),
755
797
  rawOutput: runner?.getRawOutput() ?? "",
756
798
  driverStderr: runner?.getDriverStderr() ?? "",
757
799
  events: runner?.getEvents() ?? [],
@@ -915,9 +957,46 @@ function optionalStringRecord(value, field, source) {
915
957
  function safeName(name) {
916
958
  return name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "pty-scenario";
917
959
  }
960
+ function createDeadline(timeoutMs, started = performance.now()) {
961
+ return { expiresAt: started + timeoutMs, timeoutMs };
962
+ }
963
+ function remainingMs(deadline) {
964
+ return Math.max(0, deadline.expiresAt - performance.now());
965
+ }
966
+ function throwIfDeadlineExpired(deadline) {
967
+ if (remainingMs(deadline) <= 0)
968
+ throw new WallClockTimeoutError(deadline.timeoutMs);
969
+ }
970
+ async function delayWithinDeadline(ms, deadline) {
971
+ throwIfDeadlineExpired(deadline);
972
+ if (ms <= 0)
973
+ return;
974
+ const remaining = remainingMs(deadline);
975
+ if (ms >= remaining) {
976
+ await delay(remaining);
977
+ throw new WallClockTimeoutError(deadline.timeoutMs);
978
+ }
979
+ await delay(ms);
980
+ throwIfDeadlineExpired(deadline);
981
+ }
918
982
  function delay(ms) {
919
983
  return new Promise((resolve) => setTimeout(resolve, ms));
920
984
  }
985
+ async function waitForChildExit(exitPromise, timeoutMs) {
986
+ let timeout;
987
+ try {
988
+ return await Promise.race([
989
+ exitPromise.then(() => true),
990
+ new Promise((resolve) => {
991
+ timeout = setTimeout(() => resolve(false), timeoutMs);
992
+ }),
993
+ ]);
994
+ }
995
+ finally {
996
+ if (timeout)
997
+ clearTimeout(timeout);
998
+ }
999
+ }
921
1000
  export const __debugPtyForTests = {
922
1001
  cleanTerminalText,
923
1002
  keySequence,
@@ -407,6 +407,10 @@ export async function runGatewayCli(argv = process.argv) {
407
407
  const subcommand = args[1];
408
408
  const hasForceFlag = args.includes("--force");
409
409
  if (subcommand === "web" || subcommand === "dev") {
410
+ if (args.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
411
+ printGatewayHelp();
412
+ return;
413
+ }
410
414
  const handled = await runManagedGatewayCommand(subcommand, args[2], args.slice(3), argv);
411
415
  if (!handled) {
412
416
  console.error(`Unknown gateway ${subcommand} subcommand: ${args[2] ?? ""}`);
@@ -1,11 +1,9 @@
1
1
  import { CHAT_WEB_APP_NAME } from "../apps/chat/web-app.js";
2
2
  import { createDefaultPiboPlugins } from "../plugins/builtin.js";
3
3
  import { createPiboBetterAuthPlugin } from "../plugins/better-auth.js";
4
- import { createPiboChatCustomAgentProfilesPlugin } from "../plugins/chat-custom-agents.js";
5
- import { createPiboChatUserSkillsPlugin } from "../plugins/chat-user-skills.js";
6
4
  import { createPiboChatWebPlugin } from "../plugins/chat-web.js";
7
5
  import { createPiboChatVscodeWebPlugin } from "../plugins/chat-vscode-web.js";
8
- import { createPiboContextFilesPlugin } from "../plugins/context-files.js";
6
+ import { createPiboUserProfileResourcePlugins } from "../plugins/user-profile-resources.js";
9
7
  import { createPiboPreviewPlugin } from "../previews/plugin.js";
10
8
  import { createPiboCronPlugin } from "../cron/plugin.js";
11
9
  import { createPiboLoopPlugin } from "../loops/plugin.js";
@@ -154,13 +152,15 @@ export function createWebPiboPluginRegistry(options = {}) {
154
152
  landingAppName: CHAT_WEB_APP_NAME,
155
153
  }),
156
154
  createPiboCronPlugin({ cronStorePath: resolvedOptions.chat?.cronStorePath, dataStorePath: resolvedOptions.chat?.dataStorePath, dataPayloadRootDir: resolvedOptions.chat?.dataPayloadRootDir }),
157
- createPiboChatUserSkillsPlugin({
158
- globalRoot: resolvedOptions.chat?.userSkillGlobalRoot,
159
- workspaceRoot: resolvedOptions.chat?.userSkillWorkspaceRoot,
155
+ ...createPiboUserProfileResourcePlugins({
156
+ userSkills: {
157
+ globalRoot: resolvedOptions.chat?.userSkillGlobalRoot,
158
+ workspaceRoot: resolvedOptions.chat?.userSkillWorkspaceRoot,
159
+ },
160
+ contextFiles: resolvedOptions.contextFiles,
161
+ customAgents: { agentStorePath: resolvedOptions.chat?.agentStorePath },
160
162
  }),
161
- createPiboChatCustomAgentProfilesPlugin({ agentStorePath: resolvedOptions.chat?.agentStorePath }),
162
163
  createPiboLoopPlugin({ loopStorePath: resolvedOptions.chat?.ralphStorePath, dataStorePath: resolvedOptions.chat?.dataStorePath, dataPayloadRootDir: resolvedOptions.chat?.dataPayloadRootDir }),
163
- createPiboContextFilesPlugin(resolvedOptions.contextFiles),
164
164
  createPiboPreviewPlugin(),
165
165
  createPiboChatWebPlugin(resolvedOptions.chat),
166
166
  createPiboChatVscodeWebPlugin(),
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { PiboSessionRouter } from "../core/session-router.js";
3
- import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault } from "../plugins/builtin.js";
3
+ import { createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault } from "../plugins/builtin.js";
4
+ import { createDefaultPiboUserProfileRegistry } from "../plugins/user-profile-resources.js";
4
5
  import { getDefaultPiboWorkspace } from "../core/workspace.js";
5
6
  import { InMemoryPiboSessionStore, } from "../sessions/store.js";
6
7
  export const LOCAL_TUI_CHANNEL_NAME = "local-tui";
@@ -52,7 +53,7 @@ export class LocalRoutedTuiClient {
52
53
  }
53
54
  }
54
55
  export function createLocalRoutedTuiClient(options = {}) {
55
- const registry = options.pluginRegistry ?? createDefaultPiboPluginRegistry();
56
+ const registry = options.pluginRegistry ?? createDefaultPiboUserProfileRegistry();
56
57
  const profileName = resolvePiboProfileNameFromRegistryOrDefault(registry, options.profile);
57
58
  const profile = createPiboProfileFromRegistryOrDefault(registry, profileName);
58
59
  const sessionName = options.sessionName ?? "default";
@@ -4,5 +4,7 @@ let currentLoopService;
4
4
  export function getPiboLoopService() { return currentLoopService; }
5
5
  export function createPiboLoopChannel(options = {}) {
6
6
  return { name: 'pibo.loop', kind: 'custom', description: 'Runs continuous Loop Pibo agent jobs.', auth: { mode: 'trusted-local' }, start(context) { if (currentLoopService)
7
- return; currentLoopService = new PiboLoopService({ ...options, context, store: createDefaultPiboLoopStore({ path: options.loopStorePath }) }); currentLoopService.start(); }, stop() { currentLoopService?.stop(); currentLoopService = undefined; } };
7
+ return; currentLoopService = new PiboLoopService({ ...options, context, store: createDefaultPiboLoopStore({ path: options.loopStorePath }) }); currentLoopService.start(); }, async stop() { const service = currentLoopService; if (!service)
8
+ return; await service.stop(); if (currentLoopService === service)
9
+ currentLoopService = undefined; } };
8
10
  }
package/dist/loops/cli.js CHANGED
@@ -27,7 +27,8 @@ Commands:
27
27
 
28
28
  Next: ${commandName} add --help`);
29
29
  }
30
- function targetFromOptions(options) { if (options.room)
30
+ function targetFromOptions(options) { if (options.room && options.defaultChat)
31
+ throw new Error('Choose either --room <room-id> or --default-chat, not both'); if (options.room)
31
32
  return { kind: 'room', roomId: options.room }; if (options.defaultChat)
32
33
  return { kind: 'default-chat' }; throw new Error('Choose a target: --room <room-id> or --default-chat'); }
33
34
  function maybeTargetFromOptions(options) { if (options.room || options.defaultChat)
@@ -84,6 +84,11 @@ export class PiboLoopService {
84
84
  random;
85
85
  timer;
86
86
  activeRuns = 0;
87
+ activeRunWork = new Set();
88
+ pendingRunStarts = 0;
89
+ schedulerWork;
90
+ shutdownPromise;
91
+ ownedWorkWaiters = new Set();
87
92
  stopped = true;
88
93
  cancelledRuns = new Set();
89
94
  browserLeaseHeartbeatTimers = new Map();
@@ -106,13 +111,52 @@ export class PiboLoopService {
106
111
  }
107
112
  start() { if (!this.stopped)
108
113
  return; this.stopped = false; this.store.recoverInterruptedRuns(); this.repairGoalSessionMetadata(); this.unsubscribeProductEvents = this.options.context.subscribeProductEvents?.((event) => this.handleProductEvent(event)); this.unsubscribeOutputEvents = this.options.context.subscribe((event) => this.handleOutputEvent(event)); this.arm(250); }
109
- stop() { this.stopped = true; if (this.timer)
110
- clearTimeout(this.timer); this.timer = undefined; for (const timer of this.browserLeaseHeartbeatTimers.values())
111
- clearInterval(timer); this.browserLeaseHeartbeatTimers.clear(); this.unsubscribeProductEvents?.(); this.unsubscribeProductEvents = undefined; this.unsubscribeOutputEvents?.(); this.unsubscribeOutputEvents = undefined; this.dataStore.close(); this.store.close(); }
114
+ stop() {
115
+ if (this.shutdownPromise)
116
+ return this.shutdownPromise;
117
+ this.stopped = true;
118
+ if (this.timer)
119
+ clearTimeout(this.timer);
120
+ this.timer = undefined;
121
+ for (const timer of this.browserLeaseHeartbeatTimers.values())
122
+ clearInterval(timer);
123
+ this.browserLeaseHeartbeatTimers.clear();
124
+ const closeStores = () => {
125
+ this.unsubscribeProductEvents?.();
126
+ this.unsubscribeProductEvents = undefined;
127
+ this.unsubscribeOutputEvents?.();
128
+ this.unsubscribeOutputEvents = undefined;
129
+ this.dataStore.close();
130
+ this.store.close();
131
+ };
132
+ if (!this.hasOwnedWork()) {
133
+ closeStores();
134
+ this.shutdownPromise = Promise.resolve();
135
+ return this.shutdownPromise;
136
+ }
137
+ this.shutdownPromise = this.drainOwnedWork().then(closeStores);
138
+ return this.shutdownPromise;
139
+ }
112
140
  status() { return { enabled: !this.stopped, ...this.store.status() }; }
113
- async startJob(id) { const job = this.store.updateJob(id, { enabled: true }); if (!job)
114
- return undefined; const reserved = await this.reserveAfterBeforeRunEvaluation(job); if (!reserved)
115
- return undefined; void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon()); return reserved.run; }
141
+ async startJob(id) {
142
+ if (this.stopped)
143
+ return undefined;
144
+ this.pendingRunStarts += 1;
145
+ try {
146
+ const job = this.store.updateJob(id, { enabled: true });
147
+ if (!job)
148
+ return undefined;
149
+ const reserved = await this.reserveAfterBeforeRunEvaluation(job);
150
+ if (!reserved)
151
+ return undefined;
152
+ this.launchReservedRun(reserved.job, reserved.run);
153
+ return reserved.run;
154
+ }
155
+ finally {
156
+ this.pendingRunStarts -= 1;
157
+ this.notifyOwnedWorkChange();
158
+ }
159
+ }
116
160
  stopJob(id) { const job = this.store.requestStop(id); this.armSoon(); return job; }
117
161
  removeJob(id) {
118
162
  const job = this.store.getJob(id);
@@ -244,9 +288,17 @@ export class PiboLoopService {
244
288
  const { loopJobId: _loopJobId, loopRunId: _loopRunId, ...metadata } = session.metadata ?? {};
245
289
  this.options.context.updateSession(piboSessionId, { metadata });
246
290
  }
247
- arm(delayMs) { if (this.stopped)
248
- return; if (this.timer)
249
- clearTimeout(this.timer); this.timer = setTimeout(() => void this.tick(), delayMs ?? this.intervalMs); }
291
+ arm(delayMs) {
292
+ if (this.stopped)
293
+ return;
294
+ if (this.timer)
295
+ clearTimeout(this.timer);
296
+ this.timer = setTimeout(() => {
297
+ const work = this.tick();
298
+ this.schedulerWork = work;
299
+ void work.then(() => this.finishSchedulerWork(work), (error) => { console.error('[loop] scheduler work failed', error); this.finishSchedulerWork(work); });
300
+ }, delayMs ?? this.intervalMs);
301
+ }
250
302
  armSoon() { this.arm(250); }
251
303
  async tick() { if (this.stopped)
252
304
  return; try {
@@ -257,7 +309,7 @@ export class PiboLoopService {
257
309
  for (const job of jobs) {
258
310
  const reserved = await this.reserveAfterBeforeRunEvaluation(job);
259
311
  if (reserved)
260
- void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon());
312
+ this.launchReservedRun(reserved.job, reserved.run);
261
313
  }
262
314
  }
263
315
  }
@@ -280,11 +332,38 @@ export class PiboLoopService {
280
332
  this.store.applyStopEvaluation({ jobId: fresh.id, evaluation, conditionStates, disable: false });
281
333
  if (fresh.mode === 'goal' && !await this.renewGoalBrowserLeases(fresh))
282
334
  return undefined;
283
- const reserved = this.store.reserveRun(fresh.id, now);
335
+ if (this.stopped)
336
+ return undefined;
337
+ const reserved = this.store.reserveAdmittedRun(fresh.id, now);
284
338
  if (reserved)
285
339
  this.startGoalBrowserLeaseHeartbeat(reserved.job, reserved.run);
286
340
  return reserved;
287
341
  }
342
+ launchReservedRun(job, run) {
343
+ const work = this.executeReserved(job, run);
344
+ this.activeRunWork.add(work);
345
+ void work.then(() => this.finishActiveRunWork(work), (error) => { console.error(`[loop] active run ${run.id} finalization failed`, error); this.finishActiveRunWork(work); });
346
+ }
347
+ finishActiveRunWork(work) {
348
+ this.activeRunWork.delete(work);
349
+ this.armSoon();
350
+ this.notifyOwnedWorkChange();
351
+ }
352
+ finishSchedulerWork(work) {
353
+ if (this.schedulerWork === work)
354
+ this.schedulerWork = undefined;
355
+ this.notifyOwnedWorkChange();
356
+ }
357
+ hasOwnedWork() { return this.pendingRunStarts > 0 || this.schedulerWork !== undefined || this.activeRunWork.size > 0; }
358
+ async drainOwnedWork() {
359
+ while (this.hasOwnedWork())
360
+ await new Promise((resolve) => this.ownedWorkWaiters.add(resolve));
361
+ }
362
+ notifyOwnedWorkChange() {
363
+ for (const resolve of this.ownedWorkWaiters)
364
+ resolve();
365
+ this.ownedWorkWaiters.clear();
366
+ }
288
367
  startGoalBrowserLeaseHeartbeat(job, run) {
289
368
  if (job.mode !== 'goal' || (job.resources?.browserLeaseIds?.length ?? 0) === 0)
290
369
  return;