@pasko70/pibo 2.3.0 → 2.4.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 (44) hide show
  1. package/dist/agent-runtime/routed-session.js +1 -0
  2. package/dist/agent-runtimes/codex-native/turn.js +3 -1
  3. package/dist/agent-runtimes/omp/turn.js +36 -7
  4. package/dist/apps/chat/web-app.js +6 -8
  5. package/dist/apps/chat-ui/assets/{dist-CUcAofmV.js → dist-3YG57JXi.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-Byygd1lH.js → dist-BeqHbnGN.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-DusFwy0L.js → dist-CrDtveZB.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-C9BrS7sL.js → dist-Cw9po47P.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-D4RU6xu3.js → dist-DTRjeLwO.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{index-Bifi_kjN.js → index-AjnP3ci-.js} +89 -89
  11. package/dist/apps/chat-ui/index.html +1 -1
  12. package/dist/apps/chat-vscode-web/assets/{index-WsLm1mo3.js → index-DvTSSvzN.js} +5 -5
  13. package/dist/apps/chat-vscode-web/index.html +1 -1
  14. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  15. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.3.0.vsix → pibo-vscode-ext-2.4.1.vsix} +0 -0
  16. package/dist/compute/cli.js +13 -0
  17. package/dist/compute/pool/artifacts.js +116 -0
  18. package/dist/compute/pool/cli.js +156 -0
  19. package/dist/compute/pool/config.js +101 -0
  20. package/dist/compute/pool/docker.js +157 -0
  21. package/dist/compute/pool/seeds.js +201 -0
  22. package/dist/compute/pool/service.js +402 -0
  23. package/dist/compute/pool/store.js +239 -0
  24. package/dist/compute/pool/types.js +1 -0
  25. package/dist/gateway/web.js +8 -1
  26. package/dist/loops/accounting.js +27 -0
  27. package/dist/loops/cli.js +6 -5
  28. package/dist/loops/prompts.js +13 -5
  29. package/dist/loops/service.js +3 -1
  30. package/dist/loops/store.js +10 -6
  31. package/dist/loops/tools.js +7 -4
  32. package/dist/mcp/config-command.js +3 -2
  33. package/dist/mcp/config.js +10 -4
  34. package/dist/mcp/errors.js +1 -1
  35. package/dist/mcp/index.js +19 -33
  36. package/dist/resources/lifecycle.js +22 -2
  37. package/dist/resources/reaper.js +1 -0
  38. package/dist/session-ui/sessionActivity.js +6 -2
  39. package/dist/signals/status.js +9 -4
  40. package/dist/tools/guides.js +1 -1
  41. package/dist/web/channel.js +19 -7
  42. package/npm-shrinkwrap.json +2 -2
  43. package/package.json +1 -1
  44. package/skills/builtin/loop/SKILL.md +1 -1
package/dist/mcp/index.js CHANGED
@@ -9,9 +9,8 @@
9
9
  * mcp-cli call <server> <tool> Call tool (reads JSON from stdin if no args)
10
10
  * mcp-cli call <server> <tool> {} Call tool with JSON args
11
11
  */
12
- import { readFileSync } from 'node:fs';
13
12
  import { configCommand } from './config-command.js';
14
- import { ensureConfigExists, } from './config.js';
13
+ import { ensureConfigExists, listServerNames, loadConfigUnresolved, } from './config.js';
15
14
  import { ErrorCode, ambiguousCommandError, formatCliError, missingArgumentError, tooManyArgumentsError, unknownOptionError, unknownSubcommandError, } from './errors.js';
16
15
  import { registryCommand } from './registry.js';
17
16
  import { VERSION } from './version.js';
@@ -127,37 +126,8 @@ function parseArgs(args) {
127
126
  result.command = 'info';
128
127
  const remaining = positional.slice(1);
129
128
  const { server, tool } = parseServerTool(remaining);
130
- // info requires a server argument - show available servers in error
131
129
  if (!server) {
132
- // Try to load config synchronously to show available servers
133
- let availableServers = [];
134
- const configPaths = [
135
- result.configPath,
136
- process.env.MCP_CONFIG_PATH,
137
- './mcp_servers.json',
138
- `${process.env.HOME}/.mcp_servers.json`,
139
- `${process.env.HOME}/.config/mcp/mcp_servers.json`,
140
- ].filter(Boolean);
141
- for (const cfgPath of configPaths) {
142
- try {
143
- const content = readFileSync(cfgPath, 'utf-8');
144
- const config = JSON.parse(content);
145
- if (config.mcpServers) {
146
- availableServers = Object.keys(config.mcpServers);
147
- break;
148
- }
149
- }
150
- catch {
151
- // Try next path
152
- }
153
- }
154
- const serverList = availableServers.length > 0
155
- ? availableServers.join(', ')
156
- : '(none found)';
157
- console.error('Error [MISSING_ARGUMENT]: Missing required argument for info: server');
158
- console.error(` Available servers: ${serverList}`);
159
- console.error(` Suggestion: Use 'pibo mcp info <server>' to see server details, or just 'pibo mcp' to list all`);
160
- process.exit(ErrorCode.CLIENT_ERROR);
130
+ return result;
161
131
  }
162
132
  result.server = server;
163
133
  result.tool = tool;
@@ -371,10 +341,26 @@ export async function runMcpCli(argv = process.argv) {
371
341
  }
372
342
  break;
373
343
  case 'info':
344
+ if (!args.server) {
345
+ let availableServers = [];
346
+ try {
347
+ availableServers = listServerNames(await loadConfigUnresolved(args.configPath));
348
+ }
349
+ catch {
350
+ // Keep the missing-argument error focused when no valid config exists.
351
+ }
352
+ const serverList = availableServers.length > 0
353
+ ? availableServers.join(', ')
354
+ : '(none found)';
355
+ console.error('Error [MISSING_ARGUMENT]: Missing required argument for info: server');
356
+ console.error(` Available servers: ${serverList}`);
357
+ console.error(` Suggestion: Use 'pibo mcp info <server>' to see server details, or just 'pibo mcp' to list all`);
358
+ process.exitCode = ErrorCode.CLIENT_ERROR;
359
+ break;
360
+ }
374
361
  await ensureConfigExists(args.configPath);
375
362
  {
376
363
  const { infoCommand } = await import('./commands/info.js');
377
- // info always has a server (validated in parseArgs)
378
364
  await infoCommand({
379
365
  target: buildTarget(args.server, args.tool),
380
366
  withDescriptions: args.withDescriptions,
@@ -6,6 +6,8 @@ import { promisify } from "node:util";
6
6
  import { applyComputeWorkerReapPlan, buildComputeWorkerReapPlan, planReapWorkers, } from "../compute/docker.js";
7
7
  import { defaultBrowserPoolRoot, defaultBrowserUseHome, getComputeResourceHealth, parseProcessList, } from "../compute/resource-health.js";
8
8
  import { loadBrowserPoolState, reapIdleBrowserPool, } from "../tools/browser-pool.js";
9
+ import { resolveDeploymentPoolConfig } from "../compute/pool/config.js";
10
+ import { applyDeploymentPoolReapPlan, planDeploymentPoolReap } from "../compute/pool/service.js";
9
11
  const execFileAsync = promisify(execFile);
10
12
  export async function collectManagedBrowserPools(rootDir) {
11
13
  const records = [];
@@ -56,7 +58,7 @@ export async function getActiveResourceLeases(browserPoolRoot = defaultBrowserPo
56
58
  export async function planResourceReap(options = {}) {
57
59
  const now = options.now ?? new Date();
58
60
  const resolved = resolveReapOptions(options);
59
- const [records, staleFiles, compute, health] = await Promise.all([
61
+ const [records, staleFiles, compute, health, deploymentPool] = await Promise.all([
60
62
  collectManagedBrowserPools(resolved.browserPoolRoot),
61
63
  planStaleCdpFiles(resolved.browserUseHome),
62
64
  planComputeReapSafely({ includeDev: resolved.includeDev, maxAgeMinutes: resolved.maxAgeMinutes, now }),
@@ -66,9 +68,10 @@ export async function planResourceReap(options = {}) {
66
68
  browserUseHome: resolved.browserUseHome,
67
69
  exemptBrowserUserDataDirs: [],
68
70
  }),
71
+ planDeploymentPoolReapSafely(now),
69
72
  ]);
70
73
  const unmanagedBrowsers = buildUnmanagedBrowserPlanItems(health.browserProcesses.unassignedMainProcessDetails, resolved.unmanagedBrowserGraceMinutes, new Set(resolved.exemptBrowserPids), new Set(resolved.exemptBrowserUserDataDirs), resolved.browserUseHome);
71
- return buildResourceReapPlan({ now, options: resolved, records, staleFiles, unmanagedBrowsers, compute });
74
+ return buildResourceReapPlan({ now, options: resolved, records, staleFiles, unmanagedBrowsers, compute, deploymentPool });
72
75
  }
73
76
  export function buildResourceReapPlan(input) {
74
77
  const browserItems = input.records.map((record) => buildBrowserReapPlanItem(record, input.now, input.options.idleTimeoutMinutes));
@@ -89,6 +92,7 @@ export function buildResourceReapPlan(input) {
89
92
  skipped: unmanagedBrowsers.filter((item) => item.action === "skip").length,
90
93
  },
91
94
  compute: input.compute,
95
+ deploymentPool: input.deploymentPool,
92
96
  worktreesPreserved: true,
93
97
  };
94
98
  }
@@ -112,6 +116,9 @@ export async function applyResourceReapPlan(plan, dependencies = {}) {
112
116
  }
113
117
  const removedStaleFiles = await applyStaleCdpFilePlan(confirmed.staleFiles.items, dependencies.isPidAlive);
114
118
  const removedComputeWorkers = await (dependencies.applyCompute ?? applyComputeWorkerReapPlan)(confirmed.compute);
119
+ const deploymentPoolResult = confirmed.deploymentPool
120
+ ? await (dependencies.applyDeploymentPool ?? applyDeploymentPoolReapPlan)(confirmed.deploymentPool)
121
+ : undefined;
115
122
  return {
116
123
  applied: true,
117
124
  plan: confirmed,
@@ -119,6 +126,8 @@ export async function applyResourceReapPlan(plan, dependencies = {}) {
119
126
  terminatedUnmanagedBrowsers,
120
127
  removedStaleFiles,
121
128
  removedComputeWorkers,
129
+ deploymentPoolResult,
130
+ removedDeploymentLeases: deploymentPoolResult?.releasedLeases ?? [],
122
131
  worktreesPreserved: true,
123
132
  };
124
133
  }
@@ -287,6 +296,17 @@ export async function planComputeReapSafely(options, planCompute = planReapWorke
287
296
  return plan;
288
297
  }
289
298
  }
299
+ async function planDeploymentPoolReapSafely(now) {
300
+ const config = resolveDeploymentPoolConfig();
301
+ if (!existsSync(config.databasePath))
302
+ return undefined;
303
+ try {
304
+ return await planDeploymentPoolReap({ config, now });
305
+ }
306
+ catch {
307
+ return undefined;
308
+ }
309
+ }
290
310
  async function planStaleCdpFiles(browserUseHome, isPidAlive = defaultIsPidAlive) {
291
311
  const stateDir = join(browserUseHome, "pibo-cdp");
292
312
  let files;
@@ -95,6 +95,7 @@ export class ResourceReaperService {
95
95
  unmanagedBrowsers: result.terminatedUnmanagedBrowsers.length,
96
96
  staleFiles: result.removedStaleFiles.length,
97
97
  computeWorkers: result.removedComputeWorkers.length,
98
+ ...(result.deploymentPoolResult ? { deploymentLeases: result.removedDeploymentLeases?.length ?? 0 } : {}),
98
99
  },
99
100
  lastError: undefined,
100
101
  };
@@ -1,12 +1,16 @@
1
+ import { resolveSessionSignalStatus } from "../signals/status.js";
1
2
  export function resolveSessionActivity(signal, fallback = {}) {
2
3
  if (!signal)
3
4
  return fallbackSessionActivity(fallback);
4
5
  const latestTurn = signal.latestTurn;
5
6
  const isTurnActive = latestTurn?.state === "running";
6
7
  const isTreeActive = signal.isTreeActive || isTurnActive;
7
- const hasError = signal.hasError || signal.hasErrorDescendant || signal.aggregateStatus === "error";
8
8
  return {
9
- status: hasError ? "error" : isTreeActive ? "running" : "idle",
9
+ status: resolveSessionSignalStatus({
10
+ isTreeActive,
11
+ localStatus: signal.localStatus,
12
+ latestTurn,
13
+ }),
10
14
  isTreeActive,
11
15
  isTurnActive,
12
16
  activeTurnId: isTurnActive ? latestTurn.eventId : undefined,
@@ -1,12 +1,17 @@
1
+ export function resolveSessionSignalStatus(snapshot) {
2
+ if (snapshot.isTreeActive || snapshot.latestTurn?.state === "running")
3
+ return "running";
4
+ if (snapshot.localStatus === "error" || snapshot.latestTurn?.state === "failed")
5
+ return "error";
6
+ return "idle";
7
+ }
1
8
  export function summarizeSessionSignalStatus(snapshot) {
2
- const isTurnActive = snapshot.latestTurn?.state === "running";
3
- const hasError = snapshot.hasError || snapshot.hasErrorDescendant || snapshot.aggregateStatus === "error";
4
- const isTreeActive = snapshot.isTreeActive || isTurnActive;
9
+ const isTreeActive = snapshot.isTreeActive || snapshot.latestTurn?.state === "running";
5
10
  return {
6
11
  piboSessionId: snapshot.piboSessionId,
7
12
  rootPiboSessionId: snapshot.rootPiboSessionId,
8
13
  updatedAt: snapshot.updatedAt,
9
- status: isTreeActive ? "running" : hasError ? "error" : "idle",
14
+ status: resolveSessionSignalStatus(snapshot),
10
15
  isTreeActive,
11
16
  };
12
17
  }
@@ -46,7 +46,7 @@ pibo loop stop <job-id>
46
46
  pibo loop cancel <job-id>
47
47
  \`\`\`
48
48
 
49
- Token budgets count usage reported by completed assistant model messages. One request can overshoot because usage is known after the response returns.
49
+ New Goals persist token-accounting version \`1\` with basis \`uncached\`: their budgets count uncached input and output usage reported by completed assistant model messages, while cache-read and cache-write tokens remain telemetry and do not consume the budget. Legacy persisted Goal jobs and runs without an accounting descriptor remain on version \`1\` basis \`total\`, including cache reads and writes, so their existing counters are neither relabeled nor numerically reconstructed without source data. Inspect \`pibo loop list --all --json\` and \`pibo loop runs --job <job-id> --json\` for the persisted basis. Both bases are soft because usage arrives after the response, so one request can overshoot.
50
50
  `,
51
51
  };
52
52
  export const RALPH_GUIDE = {
@@ -78,7 +78,13 @@ export function stripSocketPeerHeaderFromResponse(response) {
78
78
  headers,
79
79
  });
80
80
  }
81
- function createRequestBaseURL(nodeRequest, host, port) {
81
+ function createRequestBaseURL(nodeRequest, host, port, canonicalBaseURL) {
82
+ const requestHost = firstHeaderValue(nodeRequest.headers.host);
83
+ if (canonicalBaseURL && requestHost) {
84
+ const canonical = new URL(canonicalBaseURL);
85
+ if (requestHost.toLowerCase() === canonical.host.toLowerCase())
86
+ return canonical.origin;
87
+ }
82
88
  if (isLoopbackAddress(nodeRequest.socket.remoteAddress)) {
83
89
  const forwardedHost = firstHeaderValue(nodeRequest.headers["x-forwarded-host"]);
84
90
  const forwardedProto = firstHeaderValue(nodeRequest.headers["x-forwarded-proto"]);
@@ -86,7 +92,7 @@ function createRequestBaseURL(nodeRequest, host, port) {
86
92
  return `${forwardedProto}://${forwardedHost}`;
87
93
  }
88
94
  }
89
- return `http://${nodeRequest.headers.host ?? `${host}:${port}`}`;
95
+ return `http://${requestHost ?? `${host}:${port}`}`;
90
96
  }
91
97
  function isActiveRunStatus(status) {
92
98
  return typeof status === "string" && ["queued", "starting", "running", "streaming", "waiting", "blocked", "retrying", "compacting", "pausing"].includes(status);
@@ -235,7 +241,7 @@ export function createWebHostChannel(options = {}) {
235
241
  };
236
242
  const handleRequest = async (nodeRequest, nodeResponse) => {
237
243
  try {
238
- const baseURL = createRequestBaseURL(nodeRequest, host, port);
244
+ const baseURL = createRequestBaseURL(nodeRequest, host, port, options.canonicalBaseURL);
239
245
  const baseRequest = await nodeRequestToWebRequest(nodeRequest, baseURL);
240
246
  // Inject the TCP socket peer into every request so the local auth
241
247
  // plugin can apply the same loopback predicate from `getSession`
@@ -278,11 +284,17 @@ export function createWebHostChannel(options = {}) {
278
284
  await sendResponse(nodeResponse, response ?? notFound());
279
285
  return;
280
286
  }
281
- if (url.pathname === "/" && apps[0]) {
282
- await sendResponse(nodeResponse, redirect(apps[0].mountPath));
283
- return;
284
- }
285
287
  if (url.pathname === "/") {
288
+ const landingApp = options.landingAppName
289
+ ? apps.find((candidate) => candidate.name === options.landingAppName)
290
+ : apps[0];
291
+ if (options.landingAppName && !landingApp) {
292
+ throw new Error(`Configured landing web app "${options.landingAppName}" is not registered`);
293
+ }
294
+ if (landingApp) {
295
+ await sendResponse(nodeResponse, redirect(`${landingApp.mountPath}${url.search}`));
296
+ return;
297
+ }
286
298
  await sendResponse(nodeResponse, responseHtml("<!doctype html><title>Pibo</title><p>No web apps registered.</p>"));
287
299
  return;
288
300
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "2.3.0",
9
+ "version": "2.4.1",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"
@@ -51,7 +51,7 @@ Prefer creating the job stopped when its prompt, target, profile, or safety boun
51
51
 
52
52
  ## Token budgets
53
53
 
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
+ Goal token budgets are soft because usage arrives after model responses, so the final turn can overshoot. New Goals use versioned `uncached` accounting: cache-read and cache-write tokens do not consume the budget. Legacy persisted Goals without an accounting descriptor remain on `total` accounting, including cache usage, because their existing counters cannot be reconstructed safely. Job status and each Goal run expose the accounting basis together with tokens used before the turn, remaining tokens, turn usage, and overshoot.
55
55
 
56
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
57