@pasko70/pibo 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/apps/chat/chat-request-normalizers.js +7 -0
  2. package/dist/apps/chat/data/chat-data-mappers.js +2 -0
  3. package/dist/apps/chat/loop-api.js +11 -6
  4. package/dist/apps/chat/web-app.js +30 -10
  5. package/dist/apps/chat-ui/assets/{dist-LHRs1Nhr.js → dist-3Sts0Afa.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-wE9nop9V.js → dist-4NlLjLs7.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-BwKObYnX.js → dist-BA6mhAec.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-Y-AA2omI.js → dist-BPl-fzRB.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-GdEM8UW1.js → dist-BPnn9e2B.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-DlATLa-U.js → dist-BpfBSMzK.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-CS7wdk0Z.js → dist-Cvx5M97R.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-nOLTkZrJ.js → dist-DnXWNQRm.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-DUlaXAk7.js → dist-hU-2oAb6.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-CKtT8YGm.js → dist-r31x5Fcc.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-D-cxLQO1.js → dist-uF6UXzI7.js} +1 -1
  16. package/dist/apps/chat-ui/assets/index-CaTGYBOS.js +173 -0
  17. package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-al8DeEjA.css} +1 -1
  18. package/dist/apps/chat-ui/index.html +2 -2
  19. package/dist/apps/chat-vscode-web/assets/index-CK4SMZuu.js +41 -0
  20. package/dist/apps/chat-vscode-web/index.html +1 -1
  21. package/dist/cli-session/localSessionSource.js +34 -12
  22. package/dist/compute/resource-health.js +35 -2
  23. package/dist/core/events.js +6 -1
  24. package/dist/core/routed-session.js +67 -4
  25. package/dist/core/session-router.js +23 -6
  26. package/dist/data/ingest-service.js +3 -1
  27. package/dist/debug/index.js +4 -0
  28. package/dist/debug/trace.js +42 -7
  29. package/dist/index.js +1 -0
  30. package/dist/loops/accounting.js +19 -0
  31. package/dist/loops/cli.js +22 -6
  32. package/dist/loops/prompts.js +4 -1
  33. package/dist/loops/service.js +97 -8
  34. package/dist/loops/store.js +128 -17
  35. package/dist/loops/tools.js +24 -7
  36. package/dist/reliability/store.js +34 -7
  37. package/dist/runs/lifecycle.js +59 -0
  38. package/dist/runs/registry.js +47 -1
  39. package/dist/runs/tools.js +31 -13
  40. package/dist/session-ui/terminalRows.js +66 -2
  41. package/dist/shared/trace-async-agent-runs.js +4 -4
  42. package/dist/shared/trace-event-projection.js +59 -19
  43. package/dist/shared/trace-nodes.js +5 -0
  44. package/dist/shared/trace-page-merge.js +15 -0
  45. package/dist/shared/trace-run-notifications.js +3 -1
  46. package/dist/signals/projector.js +6 -2
  47. package/dist/tools/browser-pool.js +50 -0
  48. package/dist/tools/browser-use-leases.js +12 -8
  49. package/dist/tools/guides.js +8 -1
  50. package/dist/tools/index.js +1 -0
  51. package/package.json +2 -1
  52. package/skills/builtin/loop/SKILL.md +12 -3
  53. package/dist/apps/chat-ui/assets/index-8W_yMHQI.js +0 -173
  54. package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +0 -41
@@ -23,6 +23,21 @@ export function mergeOlderTracePage(current, older) {
23
23
  eventLimit: (current.eventLimit ?? 0) + (older.eventLimit ?? older.pageSize ?? 0),
24
24
  };
25
25
  }
26
+ export function mergeRefreshedTracePage(current, refreshed) {
27
+ if (current.piboSessionId !== refreshed.piboSessionId)
28
+ return refreshed;
29
+ return {
30
+ ...refreshed,
31
+ nodes: mergeTraceNodes(current.nodes, refreshed.nodes),
32
+ rawEvents: current.rawEvents.length ? current.rawEvents : refreshed.rawEvents,
33
+ beforeCursor: current.beforeCursor,
34
+ firstEventSequence: current.firstEventSequence ?? refreshed.firstEventSequence,
35
+ nextBeforeSequence: current.nextBeforeSequence,
36
+ nextBeforeCursor: current.nextBeforeCursor,
37
+ hasOlderEvents: current.hasOlderEvents,
38
+ eventLimit: current.eventLimit ?? refreshed.eventLimit,
39
+ };
40
+ }
26
41
  function mergeTraceNodes(olderNodes, currentNodes) {
27
42
  const byId = new Map();
28
43
  for (const node of flattenTraceNodes([...olderNodes])) {
@@ -19,7 +19,7 @@ export function parseRunNotificationText(text) {
19
19
  export function createRunNotificationNode(input) {
20
20
  const runs = runNotificationRuns(input.notification);
21
21
  const singleRun = runs.length === 1 ? runs[0] : undefined;
22
- const failedCount = countRunGroup(input.notification.failed);
22
+ const failedCount = countRunGroup(input.notification.failed) + countRunGroup(input.notification.timedOut);
23
23
  const runningCount = countRunGroup(input.notification.running);
24
24
  return {
25
25
  id: input.id,
@@ -43,6 +43,7 @@ function runNotificationRuns(notification) {
43
43
  return [
44
44
  ...runGroup(notification.completed),
45
45
  ...runGroup(notification.failed),
46
+ ...runGroup(notification.timedOut),
46
47
  ...runGroup(notification.cancelled),
47
48
  ...runGroup(notification.running),
48
49
  ];
@@ -57,6 +58,7 @@ function runNotificationSummary(notification) {
57
58
  const parts = [
58
59
  [countRunGroup(notification.completed), "completed"],
59
60
  [countRunGroup(notification.failed), "failed"],
61
+ [countRunGroup(notification.timedOut), "timed out"],
60
62
  [countRunGroup(notification.cancelled), "cancelled"],
61
63
  [countRunGroup(notification.running), "running"],
62
64
  ]
@@ -12,7 +12,7 @@ function node(input, context) {
12
12
  function runStatus(status) {
13
13
  if (status === "completed")
14
14
  return "done";
15
- if (status === "failed")
15
+ if (status === "failed" || status === "timed_out")
16
16
  return "error";
17
17
  return status;
18
18
  }
@@ -133,6 +133,10 @@ export const outputSignalProducer = {
133
133
  mutations.push({ type: "set_session_queue", piboSessionId, queuedMessages: event.queuedMessages });
134
134
  mutations.push({ type: "upsert_node", node: node({ id: `message:${piboSessionId}:${event.eventId ?? context.now()}`, kind: "message", status: "queued", piboSessionId, metadata: { source: event.source } }, context) });
135
135
  }
136
+ if (event.type === "message_steered") {
137
+ mutations.push({ type: "patch_node", nodeId: `session:${piboSessionId}`, patch: { status: "running" } });
138
+ mutations.push({ type: "upsert_node", node: node({ id: `message:${piboSessionId}:${event.eventId ?? context.now()}`, kind: "message", status: "done", piboSessionId, parentNodeId: event.activeEventId ? `turn:${piboSessionId}:${event.activeEventId}` : undefined, completedAt: context.now(), metadata: { source: event.source, delivery: "steer" } }, context) });
139
+ }
136
140
  if (event.type === "message_started") {
137
141
  mutations.push({ type: "patch_node", nodeId: `session:${piboSessionId}`, patch: { status: "running" } });
138
142
  if (event.eventId) {
@@ -200,7 +204,7 @@ export const runSignalProducer = {
200
204
  if (data.type !== "run_changed")
201
205
  return [];
202
206
  const run = data.run;
203
- return [{ type: "upsert_node", node: node({ id: `run:${run.runId}`, kind: "yielded_run", status: runStatus(run.status), piboSessionId: run.controllerPiboSessionId, startedAt: run.createdAt, completedAt: run.completedAt, error: run.status === "failed" ? { message: run.summary ?? "Run failed.", source: "run" } : undefined, metadata: { runId: run.runId, toolName: run.toolName, completionPolicy: run.completionPolicy, consumed: run.consumed, summary: run.summary, previousStatus: data.previousStatus, reason: data.reason } }, context) }];
207
+ return [{ type: "upsert_node", node: node({ id: `run:${run.runId}`, kind: "yielded_run", status: runStatus(run.status), piboSessionId: run.controllerPiboSessionId, startedAt: run.createdAt, completedAt: run.completedAt, error: run.status === "failed" || run.status === "timed_out" ? { message: run.summary ?? "Run failed.", source: "run" } : undefined, metadata: { runId: run.runId, toolName: run.toolName, completionPolicy: run.completionPolicy, consumed: run.consumed, summary: run.summary, timeoutMs: run.timeoutMs, timeoutAt: run.timeoutAt, timeoutPhase: run.timeoutPhase, serviceWarning: run.serviceWarning, previousStatus: data.previousStatus, reason: data.reason } }, context) }];
204
208
  },
205
209
  };
206
210
  export function createDefaultSignalProducers() {
@@ -1,6 +1,7 @@
1
1
  import { access, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { spawn } from "node:child_process";
4
5
  const DEFAULT_MAX_BROWSER_PROCESSES = 1;
5
6
  const DEFAULT_LOCK_TIMEOUT_MS = 5_000;
6
7
  const DEFAULT_LOCK_POLL_INTERVAL_MS = 50;
@@ -239,6 +240,55 @@ export async function acquireBrowserPoolLease(paths, identity, options = {}) {
239
240
  }
240
241
  }, options.lockOptions);
241
242
  }
243
+ export async function restartRecordedBrowserPoolChrome(state) {
244
+ if (!state.userDataDir)
245
+ throw new Error("Browser pool has no persisted user-data directory for reacquisition");
246
+ if (!state.cdpPort)
247
+ throw new Error("Browser pool has no persisted CDP port for reacquisition");
248
+ const candidates = [process.env.PIBO_CHROME_BIN, "/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/opt/google/chrome/chrome"].filter((value) => !!value);
249
+ let executable;
250
+ for (const candidate of candidates) {
251
+ try {
252
+ await access(candidate);
253
+ executable = candidate;
254
+ break;
255
+ }
256
+ catch { /* try next */ }
257
+ }
258
+ if (!executable)
259
+ throw new Error("No supported Chrome/Chromium executable is available to reacquire the persisted browser profile");
260
+ await removeDefaultStaleBrowserFiles(state);
261
+ const cdpUrl = `http://127.0.0.1:${state.cdpPort}`;
262
+ const child = spawn(executable, [
263
+ `--remote-debugging-port=${state.cdpPort}`,
264
+ "--remote-debugging-address=127.0.0.1",
265
+ `--user-data-dir=${state.userDataDir}`,
266
+ `--profile-directory=${process.env.PIBO_BROWSER_USE_DEFAULT_PROFILE || "PIBo"}`,
267
+ "--headless=new",
268
+ "--no-sandbox",
269
+ "--disable-gpu",
270
+ "--disable-dev-shm-usage",
271
+ "about:blank",
272
+ ], { detached: true, stdio: "ignore" });
273
+ await new Promise((resolve, reject) => {
274
+ child.once("spawn", resolve);
275
+ child.once("error", reject);
276
+ });
277
+ child.unref();
278
+ if (!child.pid)
279
+ throw new Error("Chrome reacquisition did not return a process id");
280
+ for (let attempt = 0; attempt < 100; attempt += 1) {
281
+ const health = await checkBrowserPoolCdpHealth(cdpUrl, { timeoutMs: 250 });
282
+ if (health.ok)
283
+ return { pid: child.pid, processGroupId: child.pid, cdpPort: state.cdpPort, cdpUrl, userDataDir: state.userDataDir };
284
+ await delay(100);
285
+ }
286
+ try {
287
+ process.kill(-child.pid, "SIGKILL");
288
+ }
289
+ catch { /* already exited */ }
290
+ throw new Error(`Reacquired Chrome did not expose CDP at ${cdpUrl}`);
291
+ }
242
292
  export async function releaseBrowserPoolLease(paths, identity, options = {}) {
243
293
  const now = options.now ?? (() => new Date());
244
294
  const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
@@ -365,21 +365,19 @@ export async function acquireBrowserUseLease(context, options = {}) {
365
365
  }
366
366
  await writeRegistry(context.status, registry);
367
367
  if (reapedCount > 0 && !options.json) {
368
- console.log(`Reaped ${reapedCount} stale lease${reapedCount === 1 ? '' : 's'}`);
368
+ console.error(`Reaped ${reapedCount} stale lease${reapedCount === 1 ? '' : 's'}`);
369
369
  }
370
370
  if (options.json)
371
371
  printLeaseJson(context.status, lease);
372
372
  else
373
373
  printLeaseEnv(context.status, lease);
374
- if (!options.noWarmup) {
375
- const warmup = await warmupBrowserUseLease(context, lease);
376
- if (!warmup.success && !options.json) {
377
- console.log(`Warning: Browser warm-up failed: ${warmup.error}`);
378
- }
374
+ const warmup = await warmupBrowserUseLease(context, lease, 15000, options.headed);
375
+ if (!warmup.success && !options.json) {
376
+ console.error(`Warning: Browser warm-up failed: ${warmup.error?.trim() || 'unknown error'}`);
379
377
  }
380
378
  });
381
379
  }
382
- async function warmupBrowserUseLease(context, lease, timeoutMs = 15000) {
380
+ async function warmupBrowserUseLease(context, lease, timeoutMs = 15000, headed = false) {
383
381
  const wrapperPath = join(context.status.homeDir, 'bin', 'browser-use');
384
382
  if (!existsSync(wrapperPath)) {
385
383
  return { success: false, error: 'browser-use wrapper not found' };
@@ -393,7 +391,13 @@ async function warmupBrowserUseLease(context, lease, timeoutMs = 15000) {
393
391
  ...(lease.browserPoolLeaseId ? { PIBO_BROWSER_POOL_LEASE_ID: lease.browserPoolLeaseId } : {}),
394
392
  };
395
393
  return new Promise((resolve) => {
396
- const child = spawn(wrapperPath, ['--session', lease.sessionName, '--pibo-ensure-chrome'], {
394
+ const args = [
395
+ ...(headed ? ['--headed'] : []),
396
+ '--session',
397
+ lease.sessionName,
398
+ '--pibo-ensure-chrome',
399
+ ];
400
+ const child = spawn(wrapperPath, args, {
397
401
  env,
398
402
  stdio: ['ignore', 'pipe', 'pipe'],
399
403
  });
@@ -376,11 +376,18 @@ browser-use close --all
376
376
  For the Pibo Chat Web App, prefer an isolated authenticated lease when multiple agents may use the browser at the same time:
377
377
 
378
378
  \`\`\`bash
379
+ # Default headless browser
379
380
  eval "$(pibo tools browser-use lease acquire --app pibo-chat --holder "$USER")"
381
+
382
+ # For headed mode, use this instead:
383
+ # eval "$(pibo tools browser-use lease acquire --app pibo-chat --holder "$USER" --headed)"
384
+
380
385
  browser-use state
381
386
  \`\`\`
382
387
 
383
- The lease exports \`BROWSER_USE_HOME\`, \`PIBO_BROWSER_USE_SESSION\`, \`PIBO_BROWSER_USE_CHROME_USER_DATA_DIR\`, and \`PIBO_BROWSER_USE_DEFAULT_PROFILE\`. The Pibo browser-use wrapper uses \`PIBO_BROWSER_USE_SESSION\` as the default session, so later commands can omit \`--session\` in that shell.
388
+ Acquire only one lease for the task. The warm-up fixes the mode of the managed Chrome process, so choose \`--headed\` during acquisition rather than adding it only to a later Browser Use command.
389
+
390
+ The lease exports \`BROWSER_USE_HOME\`, \`PIBO_BROWSER_USE_SESSION\`, \`PIBO_BROWSER_USE_CHROME_USER_DATA_DIR\`, and \`PIBO_BROWSER_USE_DEFAULT_PROFILE\`. Stdout contains only shell-safe exports and comments so it can be evaluated; warm-up diagnostics are written to stderr. The Pibo browser-use wrapper uses \`PIBO_BROWSER_USE_SESSION\` as the default session, so later commands can omit \`--session\` in that shell.
384
391
 
385
392
  Before acquiring leases, prepare one authenticated template profile:
386
393
 
@@ -1004,6 +1004,7 @@ Commands:
1004
1004
  .option('--max-slots <count>', 'Maximum active slots for the app', parsePositiveInteger)
1005
1005
  .option('--template-dir <path>', 'Authenticated Chrome user-data-dir template to clone')
1006
1006
  .option('--profile-name <name>', 'Chrome profile name inside each slot')
1007
+ .option('--headed', 'Warm up the leased Chrome browser in headed mode')
1007
1008
  .option('--json', 'Print machine-readable lease data')
1008
1009
  .action(async (options) => {
1009
1010
  await acquireBrowserUseLease({ status: getCliToolStatus(requireEntry('browser-use')) }, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "type": "module",
5
5
  "imports": {
6
6
  "vscode": "./src/apps/chat-vscode/extension/src/vscode-shim.js"
@@ -47,6 +47,7 @@
47
47
  "check:product-vocab": "node scripts/legacy-product-vocabulary-gate.mjs",
48
48
  "validate:ink-web-derived": "node scripts/ink-cli-web-derived-parity-validate.mjs",
49
49
  "compute:limited-worker-smoke": "node scripts/compute-limited-worker-smoke.mjs",
50
+ "validate:goal-endurance": "node scripts/goal-endurance-check.mjs",
50
51
  "typecheck": "tsc -p tsconfig.json --noEmit && npm run chat-ui:typecheck && npm run context-files-ui:typecheck && npm run vscode:typecheck",
51
52
  "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
52
53
  "prepack": "npm run build"
@@ -20,7 +20,7 @@ pibo loop conditions
20
20
 
21
21
  Goal-capable profiles expose native tools:
22
22
 
23
- - `get_goal`: inspect authoritative status, objective, token budget, tokens used, remaining tokens, and elapsed active time.
23
+ - `get_goal`: inspect authoritative status, soft-budget risk, tokens used, remaining tokens, active agent time, and elapsed wall-clock time.
24
24
  - `create_goal`: create a persistent Goal only when the user or system explicitly requests one.
25
25
  - `update_goal`: mark the current Goal `complete` after a strict completion audit, or `blocked` after the same impasse repeats for at least three consecutive Goal turns.
26
26
 
@@ -34,6 +34,7 @@ pibo loop add \
34
34
  --profile <profile> \
35
35
  --prompt "<complete objective>" \
36
36
  --token-budget <optional-positive-token-count> \
37
+ --token-reserve <optional-pre-turn-minimum> \
37
38
  --max-iterations <optional-run-fallback> \
38
39
  --start
39
40
  ```
@@ -50,9 +51,17 @@ Prefer creating the job stopped when its prompt, target, profile, or safety boun
50
51
 
51
52
  ## Token budgets
52
53
 
53
- Pibo accumulates model usage reported by completed assistant model messages. A Goal becomes `budget_limited` when reported usage reaches or exceeds its budget. One provider request can overshoot because usage is known only after the response reports it.
54
+ Goal token budgets are soft: Pibo accumulates usage reported after model responses, so the final turn can overshoot. Each Goal run records tokens used before the turn, remaining tokens before the turn, turn usage, and overshoot.
54
55
 
55
- Increase or clear the token budget before resuming a budget-limited Goal.
56
+ Set `--token-reserve <n>` to require more than `n` tokens to remain before Pibo starts another turn. Increase or clear the budget, or lower the reserve, before resuming a budget-limited Goal.
57
+
58
+ ## Time accounting
59
+
60
+ `activeAgentTimeSeconds` accumulates time spent executing Goal runs. `elapsedWallClockSeconds` starts when the Goal is first activated, includes waiting and paused periods, and freezes when the Goal enters a terminal state. A Goal created paused reports zero wall-clock elapsed time until first activation.
61
+
62
+ ## Managed browser leases
63
+
64
+ When a Goal owns `resources.browserLeaseIds`, Pibo renews those leases before each turn and while the turn is active. The same lease is retained across non-terminal Goal turns, including gateway service restart, and is released only when the Goal stops or reaches a terminal state. If the browser process disappeared, Pibo attempts to restart Chromium from the persisted managed profile. Failure to restore authenticated access marks the Goal blocked with an operator-facing resource reason.
56
65
 
57
66
  ## Operations
58
67