@pasko70/pibo 3.2.2 → 3.2.4

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 (71) hide show
  1. package/dist/agent-runtime/auth.js +3 -5
  2. package/dist/agent-runtime/portable-history.js +1 -1
  3. package/dist/agent-runtimes/codex-native/client.js +2 -5
  4. package/dist/agent-runtimes/codex-native/redaction.js +3 -27
  5. package/dist/agent-runtimes/omp/client.js +2 -4
  6. package/dist/agent-runtimes/pi/adapter.js +1 -0
  7. package/dist/agent-runtimes/pi/runtime.js +1 -1
  8. package/dist/apps/chat/agent-store.js +1 -1
  9. package/dist/apps/chat/chat-files.js +3 -1
  10. package/dist/apps/chat/chat-request-normalizers.js +1 -1
  11. package/dist/apps/chat/chat-user-skill-routes.js +7 -0
  12. package/dist/apps/chat/data/project-service.js +49 -1
  13. package/dist/apps/chat/data/session-query-service.js +46 -6
  14. package/dist/apps/chat/loop-api.js +10 -1
  15. package/dist/apps/chat/ralph-api.js +10 -1
  16. package/dist/apps/chat/web-app.js +22 -5
  17. package/dist/apps/chat-ui/assets/{dist-f-76_fmW.js → dist-CTC-_3l0.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-DI39iyXK.js → dist-CftLEFd5.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-BL4oTUrk.js → dist-Dq27Wt2q.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-Drv_xdGh.js → dist-Y4fHV5-Z.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-CQF9jyQW.js → dist-dGF-PDGY.js} +1 -1
  22. package/dist/apps/chat-ui/assets/index-C_P0gXvJ.js +228 -0
  23. package/dist/apps/chat-ui/assets/index-DUGVw259.css +1 -0
  24. package/dist/apps/chat-ui/index.html +2 -2
  25. package/dist/apps/chat-vscode-web/assets/index-B5X0NaRo.js +43 -0
  26. package/dist/apps/chat-vscode-web/index.html +1 -1
  27. package/dist/apps/cli-ui/InkSessionApp.js +38 -28
  28. package/dist/auth/cli.js +4 -6
  29. package/dist/auth/machine-keys.js +20 -3
  30. package/dist/cli-session/localSessionSource.js +2 -4
  31. package/dist/compute/cli.js +16 -13
  32. package/dist/compute/docker.js +96 -48
  33. package/dist/core/base-prompt.js +27 -5
  34. package/dist/core/context-build.js +5 -15
  35. package/dist/core/sensitive-data-redaction.js +2 -2
  36. package/dist/core/session-router.js +15 -4
  37. package/dist/cron/cli.js +42 -10
  38. package/dist/cron/store.js +1 -1
  39. package/dist/data/cli.js +15 -7
  40. package/dist/data/payload-store.js +38 -0
  41. package/dist/data/schema.js +9 -1
  42. package/dist/debug/index.js +5 -0
  43. package/dist/debug/pty.js +45 -10
  44. package/dist/gateway/cli.js +18 -2
  45. package/dist/gateway/server.js +1 -0
  46. package/dist/loops/service.js +32 -16
  47. package/dist/mcp/runtime-session.js +2 -1
  48. package/dist/pi-packages/cli.js +5 -0
  49. package/dist/previews/cli.js +2 -6
  50. package/dist/previews/config.js +10 -0
  51. package/dist/previews/manager.js +2 -9
  52. package/dist/ralph/service.js +21 -10
  53. package/dist/session-ui/statusViewModel.js +2 -4
  54. package/dist/sessions/pibo-data-store.js +18 -0
  55. package/dist/sessions/store.js +9 -0
  56. package/dist/setup/cli.js +14 -7
  57. package/dist/setup/installation-profiles.js +20 -7
  58. package/dist/shared/trace-event-projection.js +32 -1
  59. package/dist/subagents/tool.js +3 -3
  60. package/dist/tools/browser-use-leases.js +11 -3
  61. package/dist/tools/index.js +0 -1
  62. package/dist/tools/runtime/node-worker-source.js +39 -5
  63. package/dist/tools/runtime/python-backend.js +6 -2
  64. package/dist/tools/runtime/registry.js +38 -6
  65. package/dist/vscode/cli.js +1 -2
  66. package/dist/vscode/install.js +21 -14
  67. package/npm-shrinkwrap.json +2 -2
  68. package/package.json +3 -1
  69. package/dist/apps/chat-ui/assets/index-BTzIdlcK.css +0 -1
  70. package/dist/apps/chat-ui/assets/index-CgFDCgyv.js +0 -228
  71. package/dist/apps/chat-vscode-web/assets/index-B-TiIWb7.js +0 -43
@@ -4,8 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { basename } from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { sanitizePreviewServerSettings } from "../core/preview-server-settings.js";
8
- import { loadPreviewConfig } from "./config.js";
7
+ import { loadEffectivePreviewServerSettings } from "./config.js";
9
8
  import { findPreviewTargetProcess, isPreviewTargetProcessCurrent, previewProcessStartTicks, probePreviewTarget, } from "./network.js";
10
9
  import { previewExposureState } from "./store.js";
11
10
  const execFileAsync = promisify(execFile);
@@ -25,13 +24,7 @@ export function validatePreviewStartCommand(value) {
25
24
  }
26
25
  export async function startManagedPreview(store, id, options = {}) {
27
26
  const controller = options.controller ?? createDefaultPreviewProcessController();
28
- const settings = options.settings ?? (() => {
29
- const previewConfig = loadPreviewConfig();
30
- return sanitizePreviewServerSettings({
31
- maxRunningServers: previewConfig.maxRunningServers,
32
- autoStopMinutes: previewConfig.autoStopMinutes,
33
- });
34
- })();
27
+ const settings = options.settings ?? loadEffectivePreviewServerSettings();
35
28
  const now = options.now?.() ?? new Date();
36
29
  await reconcileManagedPreviews(store, { ...options, controller, settings, now: () => now });
37
30
  let current = store.requireExposure(id);
@@ -10,6 +10,9 @@ import { createDefaultPiboRalphStore } from './store.js';
10
10
  import { createBuiltInRalphStopConditions, evaluateRalphStopPolicy } from './stopping.js';
11
11
  const CHAT_WEB_CHANNEL = 'pibo.chat-web';
12
12
  function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
13
+ export class PiboRalphCapacityError extends Error {
14
+ constructor() { super('Ralph run capacity is currently full'); this.name = 'PiboRalphCapacityError'; }
15
+ }
13
16
  class RalphRunTimeoutError extends Error {
14
17
  abortFailed;
15
18
  constructor(message, abortFailed = false) {
@@ -46,6 +49,7 @@ export class PiboRalphService {
46
49
  runTimeoutMs;
47
50
  timer;
48
51
  activeRuns = 0;
52
+ admissionTail = Promise.resolve();
49
53
  stopped = true;
50
54
  cancelledRuns = new Set();
51
55
  unsubscribeProductEvents;
@@ -63,9 +67,11 @@ export class PiboRalphService {
63
67
  stop() { this.stopped = true; if (this.timer)
64
68
  clearTimeout(this.timer); this.timer = undefined; this.unsubscribeProductEvents?.(); this.unsubscribeProductEvents = undefined; this.dataStore.close(); this.store.close(); }
65
69
  status() { return { enabled: !this.stopped, ...this.store.status() }; }
66
- async startJob(id) { const job = this.store.updateJob(id, { enabled: true }); if (!job)
70
+ async startJob(id) { return await this.withAdmissionLock(async () => { const existing = this.store.getJob(id); if (!existing || existing.state.runningAt)
71
+ return undefined; if (this.activeRuns >= this.maxConcurrentRuns)
72
+ throw new PiboRalphCapacityError(); const job = this.store.updateJob(id, { enabled: true }); if (!job)
67
73
  return undefined; const reserved = await this.reserveAfterBeforeRunEvaluation(job); if (!reserved)
68
- return undefined; void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon()); return reserved.run; }
74
+ return undefined; void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon()); return reserved.run; }); }
69
75
  stopJob(id) { const job = this.store.requestStop(id); this.armSoon(); return job; }
70
76
  async cancelJob(id) {
71
77
  const job = this.store.requestCancel(id);
@@ -82,14 +88,13 @@ export class PiboRalphService {
82
88
  async tick() { if (this.stopped)
83
89
  return; try {
84
90
  await this.abortCancelRequestedJobs();
85
- const capacity = this.maxConcurrentRuns - this.activeRuns;
86
- if (capacity > 0) {
87
- const jobs = this.store.listJobs({ includeDisabled: false }).filter((job) => !job.state.runningAt).slice(0, capacity);
88
- for (const job of jobs) {
89
- const reserved = await this.reserveAfterBeforeRunEvaluation(job);
90
- if (reserved)
91
- void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon());
92
- }
91
+ const jobs = this.store.listJobs({ includeDisabled: false }).filter((job) => !job.state.runningAt);
92
+ for (const job of jobs) {
93
+ const admitted = await this.withAdmissionLock(async () => { if (this.activeRuns >= this.maxConcurrentRuns)
94
+ return false; const reserved = await this.reserveAfterBeforeRunEvaluation(job); if (!reserved)
95
+ return true; void this.executeReserved(reserved.job, reserved.run).finally(() => this.armSoon()); return true; });
96
+ if (!admitted)
97
+ break;
93
98
  }
94
99
  }
95
100
  catch (error) {
@@ -98,6 +103,12 @@ export class PiboRalphService {
98
103
  finally {
99
104
  this.arm();
100
105
  } }
106
+ async withAdmissionLock(action) { const previous = this.admissionTail; let release; this.admissionTail = new Promise((resolve) => { release = resolve; }); await previous; try {
107
+ return await action();
108
+ }
109
+ finally {
110
+ release();
111
+ } }
101
112
  async reserveAfterBeforeRunEvaluation(job) {
102
113
  const fresh = this.store.getJob(job.id) ?? job;
103
114
  if (!fresh.enabled || fresh.state.runningAt)
@@ -1,3 +1,4 @@
1
+ import { redactSensitiveText } from "../core/sensitive-data-redaction.js";
1
2
  export function buildTerminalStatusViewModel(input = {}) {
2
3
  const fields = [];
3
4
  const progress = [];
@@ -56,10 +57,7 @@ export function progressBarText(descriptor, width = 20) {
56
57
  return `${"█".repeat(filled)}${"░".repeat(boundedWidth - filled)} ${descriptor.percent.toFixed(1)}%`;
57
58
  }
58
59
  export function redactTerminalSecret(text) {
59
- return text
60
- .replace(/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, "$1=[redacted]")
61
- .replace(/\b(api[_-]?key|token|secret|password)\s*[:=]\s*([^\s]+)/gi, "$1=[redacted]")
62
- .replace(/\b(?:sk|pk|pibo|ghp|github_pat)_[A-Za-z0-9_\-]{8,}\b/g, "[redacted]");
60
+ return redactSensitiveText(text).replace(/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD)[A-Z0-9_]*)\s*:\s*\[redacted\]/gi, "$1=[redacted]");
63
61
  }
64
62
  function contextProgress(usage) {
65
63
  if (!usage || (usage.tokens === undefined && usage.contextWindow === undefined && usage.percent === undefined)) {
@@ -147,6 +147,24 @@ export class PiboDataSessionStore {
147
147
  `).run(sequence, new Date().toISOString(), piboSessionId);
148
148
  });
149
149
  }
150
+ claimAgentObservationSequence(parentPiboSessionId, minimum) {
151
+ return this.dataStore.transaction(() => {
152
+ const session = this.db.prepare("SELECT 1 AS present FROM sessions WHERE id = ? AND deleted_at IS NULL").get(parentPiboSessionId);
153
+ if (!session)
154
+ return minimum;
155
+ const row = this.db.prepare("SELECT next_sequence FROM session_agent_observation_counters WHERE parent_pibo_session_id = ?").get(parentPiboSessionId);
156
+ const migrationSafeSeed = Math.trunc(Date.now()) * 1_000;
157
+ const sequence = Math.max(minimum, row?.next_sequence ?? migrationSafeSeed);
158
+ this.db.prepare(`
159
+ INSERT INTO session_agent_observation_counters (parent_pibo_session_id, next_sequence, updated_at)
160
+ VALUES (?, ?, ?)
161
+ ON CONFLICT(parent_pibo_session_id) DO UPDATE SET
162
+ next_sequence = excluded.next_sequence,
163
+ updated_at = excluded.updated_at
164
+ `).run(parentPiboSessionId, sequence + 1, new Date().toISOString());
165
+ return sequence;
166
+ });
167
+ }
150
168
  claimOrAttachOutputPart(input) {
151
169
  return this.dataStore.transaction(() => {
152
170
  const indexAttribute = outputPartIndexAttribute(input.kind);
@@ -49,6 +49,7 @@ export class InMemoryPiboSessionStore {
49
49
  byPiSessionId = new Map();
50
50
  byNativeSession = new Map();
51
51
  outputRenderHighWater = new Map();
52
+ agentObservationNextSequence = new Map();
52
53
  outputToolInvocationNextOrdinal = new Map();
53
54
  get(id) {
54
55
  return this.byId.get(id);
@@ -120,6 +121,13 @@ export class InMemoryPiboSessionStore {
120
121
  if (sequence > current)
121
122
  this.outputRenderHighWater.set(piboSessionId, sequence);
122
123
  }
124
+ claimAgentObservationSequence(parentPiboSessionId, minimum) {
125
+ if (!this.byId.has(parentPiboSessionId))
126
+ return minimum;
127
+ const sequence = Math.max(minimum, this.agentObservationNextSequence.get(parentPiboSessionId) ?? 1);
128
+ this.agentObservationNextSequence.set(parentPiboSessionId, sequence + 1);
129
+ return sequence;
130
+ }
123
131
  claimOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId) {
124
132
  const key = outputToolInvocationCounterKey(piboSessionId, eventId, toolCallId);
125
133
  const ordinal = this.outputToolInvocationNextOrdinal.get(key) ?? 0;
@@ -163,6 +171,7 @@ export class InMemoryPiboSessionStore {
163
171
  if (nativeKey)
164
172
  this.byNativeSession.delete(nativeKey);
165
173
  this.outputRenderHighWater.delete(id);
174
+ this.agentObservationNextSequence.delete(id);
166
175
  const counterPrefix = `${JSON.stringify([id]).slice(0, -1)},`;
167
176
  for (const key of this.outputToolInvocationNextOrdinal.keys()) {
168
177
  if (key.startsWith(counterPrefix))
package/dist/setup/cli.js CHANGED
@@ -7,7 +7,7 @@ import { Command } from "commander";
7
7
  import { getPiboConfigValue, loadPiboConfig } from "../config/config.js";
8
8
  import { getPiboHome } from "../core/pibo-home.js";
9
9
  import { getWslInfo, isWsl } from "../core/wsl.js";
10
- import { createInstallationPlan, inspectInstallation, installationManifestPath, materializeInstallationPlan, parseInstallationComponent, parseInstallationProfile, readInstallationManifest, runPendingInstallationActions, uninstallInstallation, validateInstallationPlanTargets, } from "./installation-profiles.js";
10
+ import { createInstallationPlan, inspectInstallation, installationManifestPath, installationOutputPath, materializeInstallationPlan, parseInstallationComponent, parseInstallationProfile, readInstallationManifest, runPendingInstallationActions, uninstallInstallation, validateInstallationPlanTargets, } from "./installation-profiles.js";
11
11
  function parsePort(value) {
12
12
  const port = Number(value);
13
13
  if (!Number.isInteger(port) || port < 1 || port > 65535)
@@ -126,7 +126,7 @@ export function createUserHostSetupPlan(options = {}) {
126
126
  const wwwDomain = options.wwwDomain ?? (options.domain ? `www.${options.domain}` : undefined);
127
127
  const warnings = [];
128
128
  if (!options.domain)
129
- warnings.push("No production domain was provided; generated Caddy/Auth examples use placeholders.");
129
+ warnings.push("No production domain was provided; the Caddyfile is omitted and Auth examples use placeholders.");
130
130
  if (process.platform === "win32" && !isWsl()) {
131
131
  warnings.push("Pibo host setup targets Linux. Native Windows is not supported. Install WSL2 (https://aka.ms/wsl) and run setup inside the WSL distribution. See docs/project/guides/pibo-on-windows-via-wsl.md.");
132
132
  }
@@ -149,7 +149,14 @@ export function createUserHostSetupPlan(options = {}) {
149
149
  content: userEnvTemplate({ domain: options.domain, piboHome }),
150
150
  },
151
151
  ];
152
- if (options.includeCaddy !== false) {
152
+ if (serviceName !== "pibo-web") {
153
+ generatedFiles.push({
154
+ path: `${piboHome}/gateway-web-service`,
155
+ purpose: "Managed production gateway service identity",
156
+ content: `${serviceName}\n`,
157
+ });
158
+ }
159
+ if (options.includeCaddy !== false && options.domain) {
153
160
  generatedFiles.push({
154
161
  path: "/etc/caddy/Caddyfile",
155
162
  purpose: "HTTPS reverse proxy for the production gateway",
@@ -213,7 +220,7 @@ export function createDeveloperHostSetupPlan(options = {}) {
213
220
  piboHome: prodHome,
214
221
  serviceKind: "prod",
215
222
  webPort: prodWebPort,
216
- execStart: `${prodEntrypoint} gateway:web --web-host 127.0.0.1 --web-port ${prodWebPort}`,
223
+ execStart: `${prodEntrypoint} gateway:web --web-host 127.0.0.1 --web-port ${prodWebPort} --gateway-port ${prodGatewayPort}`,
217
224
  }),
218
225
  },
219
226
  {
@@ -871,7 +878,7 @@ export async function runSetupCli(argv = process.argv) {
871
878
  .action((options) => {
872
879
  if (options.root && options.apply)
873
880
  throw new Error("Use either --root for staging or --apply for the host, not both");
874
- const manifestPath = options.root ? join(options.root, installationManifestPath(options.piboHome).replace(/^\/+/, "")) : installationManifestPath(options.piboHome);
881
+ const manifestPath = installationOutputPath(installationManifestPath(options.piboHome), options.root);
875
882
  const manifest = readInstallationManifest(manifestPath);
876
883
  if (!manifest)
877
884
  throw new Error(`No installation manifest found at ${manifestPath}`);
@@ -893,7 +900,7 @@ export async function runSetupCli(argv = process.argv) {
893
900
  .action((name, options) => {
894
901
  if (options.root && options.apply)
895
902
  throw new Error("Use either --root for staging or --apply for the host, not both");
896
- const manifestPath = options.root ? join(options.root, installationManifestPath(options.piboHome).replace(/^\/+/, "")) : installationManifestPath(options.piboHome);
903
+ const manifestPath = installationOutputPath(installationManifestPath(options.piboHome), options.root);
897
904
  const manifest = readInstallationManifest(manifestPath);
898
905
  if (!manifest)
899
906
  throw new Error(`No installation manifest found at ${manifestPath}`);
@@ -915,7 +922,7 @@ export async function runSetupCli(argv = process.argv) {
915
922
  .action((options) => {
916
923
  if (options.root && options.apply)
917
924
  throw new Error("Use either --root for staging or --apply for the host, not both");
918
- const manifestPath = options.root ? join(options.root, installationManifestPath(options.piboHome).replace(/^\/+/, "")) : installationManifestPath(options.piboHome);
925
+ const manifestPath = installationOutputPath(installationManifestPath(options.piboHome), options.root);
919
926
  const manifest = readInstallationManifest(manifestPath);
920
927
  if (!manifest)
921
928
  throw new Error(`No installation manifest found at ${manifestPath}`);
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, isAbsolute, join } from "node:path";
3
+ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
4
4
  const CODE_SERVER_VERSION = "4.135.0";
5
5
  const BROWSER_USE_VERSION = "0.12.6";
6
6
  const AGENT_BROWSER_VERSION = "0.27.0";
@@ -28,6 +28,9 @@ function sha256(content) {
28
28
  function shellQuote(value) {
29
29
  return `'${value.replace(/'/g, `'\\''`)}'`;
30
30
  }
31
+ function systemdQuote(value) {
32
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
33
+ }
31
34
  function normalizeContent(content) {
32
35
  return content.endsWith("\n") ? content : `${content}\n`;
33
36
  }
@@ -36,8 +39,8 @@ export function installationManifestPath(piboHome) {
36
39
  }
37
40
  function gatewayService(options) {
38
41
  const integrationEnvironment = [
39
- ...(options.hasVscode ? ["Environment=PIBO_VSCODE_WEB_URL=/apps/vscode/", `Environment=PIBO_VSCODE_WORKSPACE_ROOT=${options.workspaceRoot}`] : []),
40
- ...(options.hasMcpDefaults ? [`Environment=MCP_CONFIG_PATH=${options.piboHome}/setup/mcp-defaults.json`] : []),
42
+ ...(options.hasVscode ? ["Environment=PIBO_VSCODE_WEB_URL=/apps/vscode/", `Environment=${systemdQuote(`PIBO_VSCODE_WORKSPACE_ROOT=${options.workspaceRoot}`)}`] : []),
43
+ ...(options.hasMcpDefaults ? [`Environment=${systemdQuote(`MCP_CONFIG_PATH=${options.piboHome}/setup/mcp-defaults.json`)}`] : []),
41
44
  ];
42
45
  return `[Unit]
43
46
  Description=Pibo web gateway
@@ -49,7 +52,7 @@ Type=simple
49
52
  User=root
50
53
  WorkingDirectory=/root
51
54
  Environment=HOME=/root
52
- Environment=PIBO_HOME=${options.piboHome}
55
+ Environment=${systemdQuote(`PIBO_HOME=${options.piboHome}`)}
53
56
  Environment=NODE_ENV=production
54
57
  Environment=PIBO_GATEWAY_WEB_PORT=4788
55
58
  ${integrationEnvironment.length > 0 ? `${integrationEnvironment.join("\n")}\n` : ""}ExecStart=${options.piboCommand} gateway:web --web-host 127.0.0.1 --web-port 4788
@@ -76,14 +79,14 @@ WorkingDirectory=${workspaceRoot}
76
79
  Environment=HOME=/var/lib/pibo-code
77
80
  Environment=XDG_DATA_HOME=/var/lib/pibo-code/.local/share
78
81
  Environment=XDG_CONFIG_HOME=/var/lib/pibo-code/.config
79
- ExecStart=/opt/pibo/code-server/${CODE_SERVER_VERSION}/bin/code-server --bind-addr 127.0.0.1:4790 --auth none --disable-telemetry --disable-update-check --disable-workspace-trust ${workspaceRoot}
82
+ ExecStart=/opt/pibo/code-server/${CODE_SERVER_VERSION}/bin/code-server --bind-addr 127.0.0.1:4790 --auth none --disable-telemetry --disable-update-check --disable-workspace-trust ${systemdQuote(workspaceRoot)}
80
83
  Restart=always
81
84
  RestartSec=5
82
85
  NoNewPrivileges=true
83
86
  PrivateTmp=true
84
87
  ProtectSystem=strict
85
88
  ProtectHome=read-only
86
- ReadWritePaths=${workspaceRoot} /var/lib/pibo-code
89
+ ReadWritePaths=${systemdQuote(workspaceRoot)} /var/lib/pibo-code
87
90
 
88
91
  [Install]
89
92
  WantedBy=multi-user.target
@@ -254,7 +257,17 @@ function ownedDirectoryMarkerParent(path) {
254
257
  function outputPath(path, root) {
255
258
  if (!root)
256
259
  return path;
257
- return isAbsolute(path) ? join(root, path.replace(/^\/+/, "")) : join(root, path);
260
+ const resolvedRoot = resolve(root);
261
+ const stagedPath = isAbsolute(path) ? relative(parse(path).root, path) : path;
262
+ const destination = resolve(resolvedRoot, stagedPath);
263
+ const relativeDestination = relative(resolvedRoot, destination);
264
+ if (relativeDestination === ".." || relativeDestination.startsWith(`..${sep}`) || isAbsolute(relativeDestination)) {
265
+ throw new Error(`Refusing staged path outside root: ${path}`);
266
+ }
267
+ return destination;
268
+ }
269
+ export function installationOutputPath(path, root) {
270
+ return outputPath(path, root);
258
271
  }
259
272
  function atomicWrite(path, content, mode) {
260
273
  mkdirSync(dirname(path), { recursive: true });
@@ -55,6 +55,9 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
55
55
  if (!node)
56
56
  return;
57
57
  applyStoredPayloadRef(node, payload, storedEvent.storedPayloadRef);
58
+ if (payload.type === "session_error" && payload.eventId) {
59
+ node.parentId = settleTurnFromSessionError(byId, payload.eventId, payload, storedEvent.createdAt);
60
+ }
58
61
  if (node.type === "user.message" &&
59
62
  node.status === "running" &&
60
63
  !node.parentId &&
@@ -280,10 +283,11 @@ export function markIncompletePersistedTurns(nodes, byId, piboSessionId, events,
280
283
  turn.status = "error";
281
284
  turn.summary = INCOMPLETE_TURN_SUMMARY;
282
285
  turn.error = INCOMPLETE_TURN_ERROR;
286
+ const last = lastByEventId.get(eventId) ?? start;
287
+ settleInterruptedTurnChildren(byId, turnId, INCOMPLETE_TURN_ERROR);
283
288
  const markerId = `event:incomplete-turn:${eventId}`;
284
289
  if (byId.has(markerId))
285
290
  continue;
286
- const last = lastByEventId.get(eventId) ?? start;
287
291
  const marker = {
288
292
  id: markerId,
289
293
  parentId: turnId,
@@ -306,6 +310,33 @@ export function markIncompletePersistedTurns(nodes, byId, piboSessionId, events,
306
310
  }
307
311
  return [...incompleteEventIds].some((eventId) => byId.has(`event:incomplete-turn:${eventId}`));
308
312
  }
313
+ function settleTurnFromSessionError(byId, eventId, event, completedAt) {
314
+ const turnId = messageTurnNodeId(eventId);
315
+ const turn = byId.get(turnId) ?? [...byId.values()].find((candidate) => candidate.type === "agent.turn" && candidate.eventId === eventId);
316
+ if (!turn)
317
+ return undefined;
318
+ turn.status = "error";
319
+ turn.completedAt = completedAt ?? turn.completedAt;
320
+ turn.summary = event.errorDetails?.userMessage ?? event.error;
321
+ turn.error = event.error;
322
+ settleInterruptedTurnChildren(byId, turn.id, event.error, completedAt);
323
+ return turn.id;
324
+ }
325
+ function settleInterruptedTurnChildren(byId, turnId, error, completedAt) {
326
+ for (const node of byId.values()) {
327
+ if (node.parentId !== turnId)
328
+ continue;
329
+ const isOpenContent = node.status === "running" && (node.type === "assistant.message" ||
330
+ node.type === "model.reasoning" ||
331
+ node.type === "tool.call");
332
+ const isUnfinishedTool = node.type === "tool.call" && node.completedAt === undefined;
333
+ if (!isOpenContent && !isUnfinishedTool)
334
+ continue;
335
+ node.status = "error";
336
+ node.completedAt = completedAt;
337
+ node.error = error;
338
+ }
339
+ }
309
340
  export function eventsCanAffectAsyncAgentRunStatus(events) {
310
341
  return events.some((event) => {
311
342
  const type = event.payload.type;
@@ -34,12 +34,12 @@ function resultText(prefix, value) {
34
34
  export function normalizePiboAgentSessionName(value) {
35
35
  if (typeof value !== "string")
36
36
  throw new Error("Agent session name is required.");
37
- if ([...value].length > PIBO_AGENT_SESSION_NAME_MAX_LENGTH) {
38
- throw new Error(`Agent session name must be at most ${PIBO_AGENT_SESSION_NAME_MAX_LENGTH} characters.`);
39
- }
40
37
  const normalized = value.trim();
41
38
  if (!normalized)
42
39
  throw new Error("Agent session name must not be empty.");
40
+ if ([...normalized].length > PIBO_AGENT_SESSION_NAME_MAX_LENGTH) {
41
+ throw new Error(`Agent session name must be at most ${PIBO_AGENT_SESSION_NAME_MAX_LENGTH} characters.`);
42
+ }
43
43
  return normalized;
44
44
  }
45
45
  function preparePiboAgentSessionNameInput(input) {
@@ -289,10 +289,10 @@ function killLeaseProcess(status, lease) {
289
289
  }
290
290
  async function releaseAssociatedBrowserPoolLease(status, lease) {
291
291
  if (!lease.browserPoolLeaseId)
292
- return;
292
+ return undefined;
293
293
  const identity = browserPoolIdentityForLease(status, lease);
294
294
  const paths = browserPoolPaths(browserPoolRootForLease(status, lease), identity);
295
- await releaseBrowserPoolLease(paths, identity, { leaseId: lease.browserPoolLeaseId });
295
+ return releaseBrowserPoolLease(paths, identity, { leaseId: lease.browserPoolLeaseId });
296
296
  }
297
297
  async function reapAssociatedBrowserPoolLeaseProcess(status, lease) {
298
298
  if (!lease.browserPoolLeaseId)
@@ -613,7 +613,15 @@ export async function releaseBrowserUseLease(context, id, options = {}) {
613
613
  }));
614
614
  }
615
615
  if (lease.browserPoolLeaseId) {
616
- await releaseAssociatedBrowserPoolLease(context.status, lease);
616
+ const poolRelease = await releaseAssociatedBrowserPoolLease(context.status, lease);
617
+ if (poolRelease && (poolRelease.cleanupStatus === 'failed' || poolRelease.state.state === 'dirty')) {
618
+ throw new Error(formatCliError({
619
+ code: ErrorCode.CLIENT_ERROR,
620
+ type: 'BROWSER_USE_POOL_CLEANUP_FAILED',
621
+ message: `Browser pool cleanup failed for lease "${lease.id}": ${poolRelease.lastError ?? 'managed browser cleanup failed'}`,
622
+ suggestion: `The auth lease and profile were preserved. Run \`pibo tools browser-use pool reap --worker-id ${poolRelease.state.workerId} --pool-id ${poolRelease.state.poolId} --idle-timeout-ms 0\`, then retry release.`,
623
+ }));
624
+ }
617
625
  }
618
626
  else {
619
627
  killLeaseProcess(context.status, lease);
@@ -679,7 +679,6 @@ export async function runToolsCli(argv = process.argv) {
679
679
  program
680
680
  .name('pibo tools')
681
681
  .description('Install and inspect curated external CLI tools')
682
- .helpOption(false)
683
682
  .showHelpAfterError();
684
683
  program
685
684
  .command('list')
@@ -1,5 +1,6 @@
1
1
  export const NODE_RUNTIME_WORKER_SOURCE = String.raw `
2
2
  const vm = require("node:vm");
3
+ const inspector = require("node:inspector");
3
4
  const util = require("node:util");
4
5
  const fs = require("node:fs");
5
6
  const childProcess = require("node:child_process");
@@ -153,6 +154,7 @@ const consoleProxy = {
153
154
  dir: (value, options) => appendStdout(util.inspect(value, options) + "\n"),
154
155
  };
155
156
 
157
+ const runtimeContextName = "pibo-runtime-" + process.pid;
156
158
  const context = vm.createContext({
157
159
  console: consoleProxy,
158
160
  require: runtimeRequire,
@@ -169,10 +171,24 @@ const context = vm.createContext({
169
171
  setImmediate,
170
172
  clearImmediate,
171
173
  queueMicrotask,
172
- });
174
+ }, { name: runtimeContextName });
173
175
  context.global = context;
174
176
  context.globalThis = context;
175
177
 
178
+ const inspectorSession = new inspector.Session();
179
+ let runtimeContextId;
180
+ inspectorSession.on("Runtime.executionContextCreated", ({ params }) => {
181
+ if (params.context.name === runtimeContextName) runtimeContextId = params.context.id;
182
+ });
183
+ inspectorSession.connect();
184
+ const inspectorReady = inspectorPost("Runtime.enable");
185
+
186
+ function inspectorPost(method, params = {}) {
187
+ return new Promise((resolve, reject) => {
188
+ inspectorSession.post(method, params, (error, result) => error ? reject(error) : resolve(result));
189
+ });
190
+ }
191
+
176
192
  async function execute(req) {
177
193
  const mode = req.mode || "exec";
178
194
  const code = req.code || "";
@@ -230,16 +246,31 @@ async function inspectValue(req) {
230
246
  }
231
247
  }
232
248
 
233
- function listVars(req) {
249
+ async function listVars(req) {
234
250
  const includePrivate = Boolean(req.includePrivate);
235
251
  const maxItems = Number(req.maxItems || 100);
236
252
  const maxBytes = Number(req.maxBytes || 4096);
237
253
  const hidden = new Set(["console", "require", "process", "Buffer", "URL", "URLSearchParams", "TextEncoder", "TextDecoder", "global", "globalThis"]);
254
+ const lexicalNames = new Set();
255
+ try {
256
+ await inspectorReady;
257
+ if (runtimeContextId !== undefined) {
258
+ const result = await inspectorPost("Runtime.globalLexicalScopeNames", { executionContextId: runtimeContextId });
259
+ for (const name of result.names || []) lexicalNames.add(name);
260
+ }
261
+ } catch {}
262
+ const names = new Set([...Object.keys(context), ...lexicalNames]);
238
263
  const variables = [];
239
- for (const name of Object.keys(context).sort()) {
264
+ for (const name of [...names].sort()) {
240
265
  if (hidden.has(name)) continue;
241
266
  if (!includePrivate && name.startsWith("_")) continue;
242
- variables.push({ name, summary: summarize(context[name], maxBytes) });
267
+ let value;
268
+ try {
269
+ value = lexicalNames.has(name) ? vm.runInContext(name, context, { filename: "<pibo-runtime>", timeout: 15000 }) : context[name];
270
+ } catch {
271
+ continue;
272
+ }
273
+ variables.push({ name, summary: summarize(value, maxBytes) });
243
274
  if (variables.length >= maxItems) break;
244
275
  }
245
276
  return { id: req.id, status: "ok", variables, truncated: variables.length >= maxItems };
@@ -253,7 +284,10 @@ async function handle(req) {
253
284
  if (req.type === "exec") return await execute(req);
254
285
  if (req.type === "inspect") return await inspectValue(req);
255
286
  if (req.type === "vars") return listVars(req);
256
- if (req.type === "shutdown") return { id: req.id, status: "ok", shutdown: true };
287
+ if (req.type === "shutdown") {
288
+ inspectorSession.disconnect();
289
+ return { id: req.id, status: "ok", shutdown: true };
290
+ }
257
291
  return { id: req.id, status: "error", error: { name: "RuntimeProtocolError", message: "unknown request type " + req.type } };
258
292
  }
259
293
 
@@ -90,6 +90,9 @@ export class PythonRuntimeBackend {
90
90
  isAlive() {
91
91
  return this.alive;
92
92
  }
93
+ isBusy() {
94
+ return [...this.pending.values()].some((pending) => pending.requestType === "exec");
95
+ }
93
96
  getRecord() {
94
97
  return { pid: this.child.pid, cwd: this.cwd, executable: this.executable };
95
98
  }
@@ -194,10 +197,11 @@ export class PythonRuntimeBackend {
194
197
  const id = `req_${++this.requestCounter}`;
195
198
  return new Promise((resolveRequest, rejectRequest) => {
196
199
  const timer = setTimeout(() => {
197
- this.pending.delete(id);
200
+ if (!this.pending.has(id))
201
+ return;
198
202
  rejectRequest(new TimeoutError(`Runtime request ${type} timed out after ${timeoutMs}ms`));
199
203
  }, timeoutMs);
200
- this.pending.set(id, { resolve: resolveRequest, reject: rejectRequest, timer });
204
+ this.pending.set(id, { requestType: type, resolve: resolveRequest, reject: rejectRequest, timer });
201
205
  this.child.stdin.write(`${JSON.stringify({ id, type, ...payload })}\n`);
202
206
  });
203
207
  }
@@ -30,6 +30,7 @@ function notFoundVars(sessionId = "auto") {
30
30
  export class RuntimeSessionRegistry {
31
31
  options;
32
32
  sessions = new Map();
33
+ defaultStarts = new Map();
33
34
  maxHistoryEntries;
34
35
  constructor(options) {
35
36
  this.options = options;
@@ -79,9 +80,23 @@ export class RuntimeSessionRegistry {
79
80
  }
80
81
  }
81
82
  async exec(controllerPiboSessionId, input) {
82
- const session = input.sessionId
83
+ let session = input.sessionId
83
84
  ? this.getSessionForController(controllerPiboSessionId, input.sessionId)
84
- : await this.getOrStartDefault(controllerPiboSessionId, input);
85
+ : this.getDefault(controllerPiboSessionId, input.runtime ?? "python");
86
+ if (!input.sessionId && !session) {
87
+ const runtime = input.runtime ?? "python";
88
+ const started = await this.getOrStartDefault(controllerPiboSessionId, input);
89
+ if (started.status !== "ok" || !started.sessionId) {
90
+ return {
91
+ status: started.status === "ok" ? "failed" : started.status,
92
+ sessionId: started.sessionId ?? "auto",
93
+ runtime: started.runtime ?? runtime,
94
+ durationMs: 0,
95
+ error: started.error ?? { name: "RuntimeStartError", message: "Runtime session failed to start." },
96
+ };
97
+ }
98
+ session = this.getSessionForController(controllerPiboSessionId, started.sessionId);
99
+ }
85
100
  const sessionId = input.sessionId ?? session?.sessionId ?? "auto";
86
101
  if (!session)
87
102
  return notFoundExec(sessionId);
@@ -106,7 +121,9 @@ export class RuntimeSessionRegistry {
106
121
  result.executionCount = session.executionCount;
107
122
  session.lastExecAt = startedAt;
108
123
  session.updatedAt = nowIso();
109
- session.status = session.backend.isAlive() ? "idle" : "failed";
124
+ session.status = session.backend.isAlive()
125
+ ? session.backend.isBusy?.() === true ? "busy" : "idle"
126
+ : "failed";
110
127
  this.appendHistory(session, {
111
128
  id: randomUUID(),
112
129
  startedAt,
@@ -223,14 +240,25 @@ export class RuntimeSessionRegistry {
223
240
  const runtime = input.runtime ?? "python";
224
241
  const existing = this.getDefault(controllerPiboSessionId, runtime);
225
242
  if (existing)
226
- return existing;
227
- const started = await this.start(controllerPiboSessionId, {
243
+ return { status: "ok", sessionId: existing.sessionId, runtime: existing.runtime };
244
+ const key = `${controllerPiboSessionId}\0${runtime}`;
245
+ const pending = this.defaultStarts.get(key);
246
+ if (pending)
247
+ return pending;
248
+ const start = this.start(controllerPiboSessionId, {
228
249
  runtime,
229
250
  name: input.name,
230
251
  target: input.target,
231
252
  timeoutMs: input.timeoutMs,
232
253
  });
233
- return started.sessionId ? this.getSessionForController(controllerPiboSessionId, started.sessionId) : undefined;
254
+ this.defaultStarts.set(key, start);
255
+ try {
256
+ return await start;
257
+ }
258
+ finally {
259
+ if (this.defaultStarts.get(key) === start)
260
+ this.defaultStarts.delete(key);
261
+ }
234
262
  }
235
263
  getSessionForController(controllerPiboSessionId, sessionId) {
236
264
  const session = this.sessions.get(sessionId);
@@ -243,6 +271,10 @@ export class RuntimeSessionRegistry {
243
271
  session.status = "failed";
244
272
  session.updatedAt = nowIso();
245
273
  }
274
+ else if (session.status === "busy" && session.backend.isBusy?.() === false) {
275
+ session.status = "idle";
276
+ session.updatedAt = nowIso();
277
+ }
246
278
  return session;
247
279
  }
248
280
  appendHistory(session, entry) {
@@ -50,9 +50,8 @@ export async function runVscodeCli(argv = process.argv) {
50
50
  else if (result.status === "installed") {
51
51
  console.log(`installed\t${result.tagName}\t${result.vsixPath}`);
52
52
  }
53
- else {
53
+ if (result.status === "failed")
54
54
  process.exitCode = 1;
55
- }
56
55
  });
57
56
  program
58
57
  .command("uninstall")