@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
@@ -1,11 +1,20 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { createHash } from "node:crypto";
4
- import { readFile, readdir, writeFile, mkdir } from "node:fs/promises";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { copyFile, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
5
7
  import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import dockerIgnoreModule from "@balena/dockerignore";
10
+ import { piboHomePath } from "../core/pibo-home.js";
6
11
  import { COMPUTE_RESOURCE_POLICY_LABELS, buildComputeResourcePolicyLabels, buildDockerResourcePolicyArgs, resolveComputeResourcePolicy, } from "./resource-policy.js";
7
12
  const execFileAsync = promisify(execFile);
13
+ const createDockerIgnore = dockerIgnoreModule;
8
14
  export const IMAGE_NAME = "pibo:latest";
15
+ export const COMPUTE_IMAGE_ENV = "PIBO_COMPUTE_IMAGE";
16
+ export const PACKAGED_COMPUTE_DOCKERFILE = "compute-image/Dockerfile";
17
+ export const COMPUTE_PUBLISH_HOST = "127.0.0.1";
9
18
  export const LABEL_ROLE = "pibo.compute.role";
10
19
  export const LABEL_CREATED_AT = "pibo.compute.createdAt";
11
20
  export const LABEL_HOLDER = "pibo.compute.holder";
@@ -19,9 +28,67 @@ export const LABEL_CLEANUP_STATE = "pibo.compute.cleanupState";
19
28
  export const LABEL_DIRTY_REASON = "pibo.compute.dirtyReason";
20
29
  export const LABEL_RALPH_JOB_ID = "pibo.ralph.jobId";
21
30
  export const LABEL_RALPH_RUN_ID = "pibo.ralph.runId";
22
- export async function dockerBuild(workspaceDir) {
23
- const { stderr } = await execFileAsync("docker", ["build", "-t", IMAGE_NAME, "."], {
24
- cwd: workspaceDir,
31
+ export function resolveComputeImageBuildConfig(workspaceDir, options = {}) {
32
+ const packageRoot = options.packageRoot ?? fileURLToPath(new URL("../../", import.meta.url));
33
+ const env = options.env ?? process.env;
34
+ const fileExists = options.fileExists ?? existsSync;
35
+ const readTextFile = options.readTextFile ?? ((candidate) => readFileSync(candidate, "utf8"));
36
+ const imageName = env[COMPUTE_IMAGE_ENV]?.trim() || IMAGE_NAME;
37
+ const workspaceDockerfile = path.join(workspaceDir, "Dockerfile");
38
+ const workspacePackageJson = path.join(workspaceDir, "package.json");
39
+ if (fileExists(workspaceDockerfile) && fileExists(workspacePackageJson)) {
40
+ try {
41
+ const manifest = JSON.parse(readTextFile(workspacePackageJson));
42
+ if (manifest.name === "@pasko70/pibo") {
43
+ return { imageName, buildContext: workspaceDir, source: "workspace" };
44
+ }
45
+ }
46
+ catch {
47
+ // An unrelated or malformed caller package is not a Pibo source checkout.
48
+ }
49
+ }
50
+ const dockerfile = path.join(packageRoot, PACKAGED_COMPUTE_DOCKERFILE);
51
+ if (!fileExists(dockerfile)) {
52
+ throw new Error(`No compute Dockerfile found in workspace ${workspaceDir} or installed package ${dockerfile}`);
53
+ }
54
+ return { imageName, buildContext: packageRoot, dockerfile, source: "package" };
55
+ }
56
+ export function buildDockerBuildArgs(config) {
57
+ return ["build", "-t", config.imageName, ...(config.dockerfile ? ["-f", config.dockerfile] : []), "."];
58
+ }
59
+ export async function dockerBuild(config) {
60
+ if (config.source === "workspace") {
61
+ await runDockerBuild(config);
62
+ return;
63
+ }
64
+ if (!config.dockerfile)
65
+ throw new Error("Packaged compute image build requires a Dockerfile");
66
+ const stagedContext = await mkdtemp(path.join(tmpdir(), "pibo-compute-image-"));
67
+ try {
68
+ const { stdout } = await execFileAsync("npm", ["pack", "--json", "--ignore-scripts", "--pack-destination", stagedContext], {
69
+ cwd: config.buildContext,
70
+ maxBuffer: 16 * 1024 * 1024,
71
+ });
72
+ const report = JSON.parse(stdout);
73
+ const filename = report[0]?.filename;
74
+ if (typeof filename !== "string" || !filename.endsWith(".tgz")) {
75
+ throw new Error("npm pack did not return a packaged Pibo archive");
76
+ }
77
+ await rename(path.join(stagedContext, filename), path.join(stagedContext, "pibo-package.tgz"));
78
+ await copyFile(config.dockerfile, path.join(stagedContext, "Dockerfile"));
79
+ await runDockerBuild({
80
+ imageName: config.imageName,
81
+ buildContext: stagedContext,
82
+ source: "workspace",
83
+ });
84
+ }
85
+ finally {
86
+ await rm(stagedContext, { recursive: true, force: true });
87
+ }
88
+ }
89
+ async function runDockerBuild(config) {
90
+ const { stderr } = await execFileAsync("docker", buildDockerBuildArgs(config), {
91
+ cwd: config.buildContext,
25
92
  maxBuffer: 50 * 1024 * 1024,
26
93
  });
27
94
  if (stderr)
@@ -50,45 +117,83 @@ export async function getDependencyHash(workspaceDir) {
50
117
  }
51
118
  return hash.digest("hex");
52
119
  }
53
- export async function getSourceHash(workspaceDir) {
120
+ export async function getSourceHash(workspaceDir, dockerfile = path.join(workspaceDir, "Dockerfile")) {
54
121
  const hash = createHash("sha256");
55
- const files = [];
56
- async function walk(dir) {
122
+ async function readOptionalFile(filePath) {
123
+ try {
124
+ return await readFile(filePath, "utf8");
125
+ }
126
+ catch (error) {
127
+ if (error.code === "ENOENT")
128
+ return undefined;
129
+ throw error;
130
+ }
131
+ }
132
+ const dockerfileIgnorePath = `${dockerfile}.dockerignore`;
133
+ const dockerfileIgnore = await readOptionalFile(dockerfileIgnorePath);
134
+ const rootIgnore = await readOptionalFile(path.join(workspaceDir, ".dockerignore"));
135
+ const activeIgnore = dockerfileIgnore ?? rootIgnore ?? "";
136
+ const requiredContextPaths = new Set([
137
+ path.relative(workspaceDir, dockerfile).split(path.sep).join("/"),
138
+ path.relative(workspaceDir, dockerfileIgnorePath).split(path.sep).join("/"),
139
+ ".dockerignore",
140
+ ]);
141
+ const contextIgnore = createDockerIgnore({ ignorecase: false }).add(activeIgnore
142
+ .split(/\r?\n/u)
143
+ .filter((line) => line.trim() !== ".")
144
+ .join("\n"));
145
+ const hasNegatedPattern = activeIgnore.split(/\r?\n/u).some((line) => /^\/*!/u.test(line.trim()));
146
+ const contextEntries = [];
147
+ function isIgnored(relativePath, directory) {
148
+ if (requiredContextPaths.has(relativePath))
149
+ return false;
150
+ return contextIgnore.ignores(directory ? `${relativePath}/` : relativePath);
151
+ }
152
+ async function walk(dir, relativeDir = "") {
153
+ let containsIncludedEntry = false;
57
154
  const entries = await readdir(dir, { withFileTypes: true });
58
155
  for (const entry of entries) {
59
156
  const fullPath = path.join(dir, entry.name);
157
+ const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
60
158
  if (entry.isDirectory()) {
61
- if (entry.name === "node_modules" ||
62
- entry.name === ".git" ||
63
- entry.name === "dist" ||
64
- entry.name === ".pibo" ||
65
- entry.name === "plans" ||
66
- entry.name === "Reports") {
159
+ const ignored = isIgnored(relativePath, true);
160
+ if (ignored && !hasNegatedPattern)
67
161
  continue;
162
+ const containsIncludedChild = await walk(fullPath, relativePath);
163
+ if (!ignored || containsIncludedChild) {
164
+ contextEntries.push({ relativePath, type: "directory" });
165
+ containsIncludedEntry = true;
68
166
  }
69
- await walk(fullPath);
70
167
  }
71
- else if (entry.isFile()) {
72
- if (entry.name.endsWith(".ts") ||
73
- entry.name.endsWith(".tsx") ||
74
- entry.name === "package.json" ||
75
- entry.name === "package-lock.json" ||
76
- entry.name === "Dockerfile") {
77
- files.push(fullPath);
78
- }
168
+ else if (!isIgnored(relativePath, false)) {
169
+ contextEntries.push({ relativePath, type: entry.isFile() ? "file" : entry.isSymbolicLink() ? "symlink" : "other" });
170
+ containsIncludedEntry = true;
79
171
  }
80
172
  }
173
+ return containsIncludedEntry;
81
174
  }
82
175
  await walk(workspaceDir);
83
- files.sort();
84
- for (const file of files) {
85
- const content = await readFile(file);
86
- hash.update(content);
176
+ contextEntries.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0));
177
+ function update(value) {
178
+ const bytes = typeof value === "string" ? Buffer.from(value) : value;
179
+ hash.update(`${bytes.length}:`);
180
+ hash.update(bytes);
181
+ }
182
+ for (const entry of contextEntries) {
183
+ const fullPath = path.join(workspaceDir, entry.relativePath);
184
+ const stat = await lstat(fullPath);
185
+ update(entry.type);
186
+ update(entry.relativePath);
187
+ update(String(stat.mode & 0o777));
188
+ if (entry.type === "file")
189
+ update(await readFile(fullPath));
190
+ else if (entry.type === "symlink")
191
+ update(await readlink(fullPath));
87
192
  }
88
193
  return hash.digest("hex");
89
194
  }
90
- export async function shouldRebuild(workspaceDir, hashFile) {
91
- const currentHash = await getSourceHash(workspaceDir);
195
+ export async function shouldRebuild(workspaceDir, hashFile, dockerfile) {
196
+ const currentHash = await getSourceHash(workspaceDir, dockerfile);
92
197
  try {
93
198
  const savedHash = await readFile(hashFile, "utf-8");
94
199
  return savedHash.trim() !== currentHash;
@@ -107,8 +212,8 @@ export async function shouldRebuildDeps(workspaceDir, hashFile) {
107
212
  return true;
108
213
  }
109
214
  }
110
- export async function saveHash(workspaceDir, hashFile) {
111
- const hash = await getSourceHash(workspaceDir);
215
+ export async function saveHash(workspaceDir, hashFile, dockerfile) {
216
+ const hash = await getSourceHash(workspaceDir, dockerfile);
112
217
  await mkdir(path.dirname(hashFile), { recursive: true });
113
218
  await writeFile(hashFile, hash, "utf-8");
114
219
  }
@@ -119,6 +224,143 @@ export async function saveDepHash(workspaceDir, hashFile) {
119
224
  }
120
225
  const DEV_PORT_BASE = 4800;
121
226
  const DEV_PORT_BLOCK_SIZE = 10;
227
+ const DEV_PORT_ALLOCATION_LOCK_TIMEOUT_MS = 30_000;
228
+ const DEV_PORT_ALLOCATION_LOCK_POLL_MS = 25;
229
+ const DEV_PORT_ALLOCATION_MALFORMED_STALE_MS = 5_000;
230
+ function devPortAllocationLockPath() {
231
+ return piboHomePath("compute", "dev-port-allocation.lock");
232
+ }
233
+ function isErrnoException(error, code) {
234
+ return error instanceof Error && "code" in error && error.code === code;
235
+ }
236
+ function processIsAlive(pid) {
237
+ try {
238
+ process.kill(pid, 0);
239
+ return "active";
240
+ }
241
+ catch (error) {
242
+ if (isErrnoException(error, "ESRCH"))
243
+ return "dead";
244
+ return "ambiguous";
245
+ }
246
+ }
247
+ async function processIdentity(pid) {
248
+ if (process.platform !== "linux")
249
+ return { liveness: processIsAlive(pid) };
250
+ let raw;
251
+ try {
252
+ raw = await readFile(`/proc/${pid}/stat`, "utf8");
253
+ }
254
+ catch (error) {
255
+ if (isErrnoException(error, "ENOENT") || isErrnoException(error, "ESRCH"))
256
+ return { liveness: "dead" };
257
+ return { liveness: "ambiguous" };
258
+ }
259
+ const commandEnd = raw.lastIndexOf(")");
260
+ if (commandEnd < 2 || commandEnd + 2 >= raw.length)
261
+ return { liveness: "ambiguous" };
262
+ const fields = raw.slice(commandEnd + 2).trim().split(/\s+/);
263
+ if (fields[0] === "Z")
264
+ return { liveness: "dead" };
265
+ const startTicks = fields[19];
266
+ if (!startTicks || !/^[1-9][0-9]*$/.test(startTicks))
267
+ return { liveness: "ambiguous" };
268
+ return { liveness: "active", startId: `${pid}:${startTicks}` };
269
+ }
270
+ async function devPortAllocationLockOwnerLiveness(owner) {
271
+ const identity = await processIdentity(owner.pid);
272
+ if (identity.liveness !== "active")
273
+ return identity.liveness;
274
+ if (process.platform !== "linux")
275
+ return "active";
276
+ if (!owner.processStartId || !identity.startId)
277
+ return "ambiguous";
278
+ return owner.processStartId === identity.startId ? "active" : "dead";
279
+ }
280
+ async function removeAbandonedDevPortAllocationLock(lockPath) {
281
+ const raw = await readFile(lockPath, "utf8").catch(() => undefined);
282
+ if (raw === undefined)
283
+ return;
284
+ let owner = {};
285
+ try {
286
+ owner = JSON.parse(raw);
287
+ }
288
+ catch {
289
+ // A partially written lock can be reclaimed after a short grace period.
290
+ }
291
+ const validOwner = typeof owner.pid === "number"
292
+ && Number.isSafeInteger(owner.pid)
293
+ && owner.pid > 0
294
+ && typeof owner.token === "string"
295
+ && owner.token.length > 0
296
+ && typeof owner.createdAt === "string"
297
+ && (owner.processStartId === undefined || typeof owner.processStartId === "string");
298
+ if (!validOwner) {
299
+ const lockStat = await stat(lockPath).catch(() => undefined);
300
+ if (!lockStat || Date.now() - lockStat.mtimeMs <= DEV_PORT_ALLOCATION_MALFORMED_STALE_MS)
301
+ return;
302
+ }
303
+ else if (await devPortAllocationLockOwnerLiveness(owner) !== "dead") {
304
+ return;
305
+ }
306
+ if ((await readFile(lockPath, "utf8").catch(() => undefined)) === raw) {
307
+ await rm(lockPath, { force: true });
308
+ }
309
+ }
310
+ async function releaseDevPortAllocationLock(lockPath, token) {
311
+ try {
312
+ const owner = JSON.parse(await readFile(lockPath, "utf8"));
313
+ if (owner.token === token)
314
+ await rm(lockPath, { force: true });
315
+ }
316
+ catch {
317
+ // The lock was already removed or replaced.
318
+ }
319
+ }
320
+ async function withDevPortAllocationLock(run) {
321
+ const lockPath = devPortAllocationLockPath();
322
+ const token = randomUUID();
323
+ const startedAt = Date.now();
324
+ const identity = await processIdentity(process.pid);
325
+ if (identity.liveness !== "active")
326
+ throw new Error("Cannot establish compute dev port allocation lock owner identity");
327
+ while (true) {
328
+ try {
329
+ await mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
330
+ const handle = await open(lockPath, "wx", 0o600);
331
+ try {
332
+ await handle.writeFile(JSON.stringify({
333
+ pid: process.pid,
334
+ ...(identity.startId ? { processStartId: identity.startId } : {}),
335
+ token,
336
+ createdAt: new Date().toISOString(),
337
+ }), "utf8");
338
+ }
339
+ catch (error) {
340
+ await handle.close();
341
+ await rm(lockPath, { force: true });
342
+ throw error;
343
+ }
344
+ await handle.close();
345
+ break;
346
+ }
347
+ catch (error) {
348
+ if (!isErrnoException(error, "EEXIST"))
349
+ throw error;
350
+ await removeAbandonedDevPortAllocationLock(lockPath);
351
+ if (Date.now() - startedAt >= DEV_PORT_ALLOCATION_LOCK_TIMEOUT_MS) {
352
+ throw new Error(`Timed out waiting for compute dev port allocation lock ${lockPath}`);
353
+ }
354
+ await new Promise((resolve) => setTimeout(resolve, DEV_PORT_ALLOCATION_LOCK_POLL_MS));
355
+ }
356
+ }
357
+ try {
358
+ return await run();
359
+ }
360
+ finally {
361
+ await releaseDevPortAllocationLock(lockPath, token);
362
+ }
363
+ }
122
364
  async function findNextPortBlock() {
123
365
  const { stdout } = await execFileAsync("docker", [
124
366
  "ps",
@@ -205,15 +447,15 @@ export function buildDevWorkerDockerRunArgs(options) {
205
447
  "-e",
206
448
  "PIBO_COMPUTE_WORKER_ROLE=dev",
207
449
  "-p",
208
- `127.0.0.1:${options.gatewayPort}:4789`,
450
+ `${COMPUTE_PUBLISH_HOST}:${options.gatewayPort}:4789`,
209
451
  "-p",
210
- `127.0.0.1:${options.cdpPort}:56663`,
452
+ `${COMPUTE_PUBLISH_HOST}:${options.cdpPort}:56663`,
211
453
  "-p",
212
- `127.0.0.1:${options.webPort}:4788`,
454
+ `${COMPUTE_PUBLISH_HOST}:${options.webPort}:4788`,
213
455
  "-p",
214
- `127.0.0.1:${options.webUIPortChat}:4790`,
456
+ `${COMPUTE_PUBLISH_HOST}:${options.webUIPortChat}:4790`,
215
457
  "-p",
216
- `127.0.0.1:${options.webUIPortContext}:4791`,
458
+ `${COMPUTE_PUBLISH_HOST}:${options.webUIPortContext}:4791`,
217
459
  "-v",
218
460
  `${options.worktreePath}:/workspace`,
219
461
  ...(options.hostNodeModules ? ["-v", `${options.hostNodeModules}:/workspace/node_modules`] : []),
@@ -241,21 +483,14 @@ export function buildDevWorkerDockerRunArgs(options) {
241
483
  ...buildComputeResourcePolicyLabels(policy).flatMap((label) => ["--label", label]),
242
484
  "--entrypoint",
243
485
  "/bin/sh",
244
- IMAGE_NAME,
486
+ options.imageName ?? IMAGE_NAME,
245
487
  "-c",
246
- "tail -f /dev/null",
488
+ "ln -sf /workspace/dist/bin/pibo.js /usr/local/bin/pibo && exec tail -f /dev/null",
247
489
  ];
248
490
  }
249
491
  export async function spawnDevWorker(options) {
250
492
  const worktreePath = path.join(options.repoDir, ".worktrees", options.worktreeName);
251
493
  await createWorktree(options.repoDir, options.worktreeName);
252
- const block = await findNextPortBlock();
253
- const base = DEV_PORT_BASE + block * DEV_PORT_BLOCK_SIZE;
254
- const gatewayPort = base;
255
- const cdpPort = base + 1;
256
- const webPort = base + 2;
257
- const webUIPortChat = base + 3;
258
- const webUIPortContext = base + 4;
259
494
  const id = `pibo-dev-${options.worktreeName}`;
260
495
  const createdAt = new Date().toISOString();
261
496
  // Mount host node_modules into the container so dependencies are immediately available
@@ -269,35 +504,41 @@ export async function spawnDevWorker(options) {
269
504
  catch {
270
505
  // host node_modules does not exist; skip mount
271
506
  }
272
- const args = buildDevWorkerDockerRunArgs({
273
- id,
274
- worktreePath,
275
- worktreeName: options.worktreeName,
276
- block,
277
- gatewayPort,
278
- cdpPort,
279
- webPort,
280
- webUIPortChat,
281
- webUIPortContext,
282
- createdAt,
283
- holder: options.holder,
284
- ttlSeconds: options.ttlSeconds,
285
- idleSeconds: options.idleSeconds,
286
- ralphJobId: options.ralphJobId,
287
- ralphRunId: options.ralphRunId,
288
- hostNodeModules: nodeModulesMount,
507
+ const ports = await withDevPortAllocationLock(async () => {
508
+ const block = await findNextPortBlock();
509
+ const base = DEV_PORT_BASE + block * DEV_PORT_BLOCK_SIZE;
510
+ const gatewayPort = base;
511
+ const cdpPort = base + 1;
512
+ const webPort = base + 2;
513
+ const webUIPortChat = base + 3;
514
+ const webUIPortContext = base + 4;
515
+ const args = buildDevWorkerDockerRunArgs({
516
+ id,
517
+ imageName: options.imageName,
518
+ worktreePath,
519
+ worktreeName: options.worktreeName,
520
+ block,
521
+ gatewayPort,
522
+ cdpPort,
523
+ webPort,
524
+ webUIPortChat,
525
+ webUIPortContext,
526
+ createdAt,
527
+ holder: options.holder,
528
+ ttlSeconds: options.ttlSeconds,
529
+ idleSeconds: options.idleSeconds,
530
+ ralphJobId: options.ralphJobId,
531
+ ralphRunId: options.ralphRunId,
532
+ hostNodeModules: nodeModulesMount,
533
+ });
534
+ await execFileAsync("docker", args, { cwd: options.repoDir });
535
+ return { gatewayPort, cdpPort, webPort, webUIPortChat, webUIPortContext };
289
536
  });
290
- await execFileAsync("docker", args, { cwd: options.repoDir });
291
- const host = await detectHost();
292
537
  return {
293
538
  id,
294
- image: IMAGE_NAME,
295
- gatewayHost: host,
296
- gatewayPort,
297
- cdpPort,
298
- webPort,
299
- webUIPortChat,
300
- webUIPortContext,
539
+ image: options.imageName ?? IMAGE_NAME,
540
+ gatewayHost: COMPUTE_PUBLISH_HOST,
541
+ ...ports,
301
542
  connect: `docker exec -it ${id} bash`,
302
543
  worktree: worktreePath,
303
544
  };
@@ -316,11 +557,11 @@ export function buildWorkerDockerRunArgs(options) {
316
557
  "-e",
317
558
  "PIBO_COMPUTE_WORKER_ROLE=worker",
318
559
  "-p",
319
- "127.0.0.1::4789",
560
+ `${COMPUTE_PUBLISH_HOST}::4789`,
320
561
  "-p",
321
- "127.0.0.1::56663",
562
+ `${COMPUTE_PUBLISH_HOST}::56663`,
322
563
  "-p",
323
- "127.0.0.1::4788",
564
+ `${COMPUTE_PUBLISH_HOST}::4788`,
324
565
  ...buildComputeWorkerMetadataLabels({
325
566
  role: "worker",
326
567
  createdAt: options.createdAt,
@@ -335,7 +576,7 @@ export function buildWorkerDockerRunArgs(options) {
335
576
  ralphRunId: options.ralphRunId,
336
577
  }).flatMap((label) => ["--label", label]),
337
578
  ...buildComputeResourcePolicyLabels(policy).flatMap((label) => ["--label", label]),
338
- IMAGE_NAME,
579
+ options.imageName ?? IMAGE_NAME,
339
580
  "gateway:web",
340
581
  ];
341
582
  }
@@ -345,6 +586,7 @@ export async function spawnWorker(options) {
345
586
  const args = buildWorkerDockerRunArgs({
346
587
  id,
347
588
  createdAt,
589
+ imageName: options.imageName,
348
590
  holder: options.holder,
349
591
  worktreePath: options.workspaceDir,
350
592
  ttlSeconds: options.ttlSeconds,
@@ -360,12 +602,10 @@ export async function spawnWorker(options) {
360
602
  const gatewayPort = parseHostPort(port4789);
361
603
  const cdpPort = parseHostPort(port56663);
362
604
  const webPort = parseHostPort(port4788);
363
- // Detect host IP
364
- const host = await detectHost();
365
605
  return {
366
606
  id,
367
- image: IMAGE_NAME,
368
- gatewayHost: host,
607
+ image: options.imageName ?? IMAGE_NAME,
608
+ gatewayHost: COMPUTE_PUBLISH_HOST,
369
609
  gatewayPort,
370
610
  cdpPort,
371
611
  webPort,
@@ -385,19 +625,6 @@ function parseHostPort(dockerPortOutput) {
385
625
  }
386
626
  return 0;
387
627
  }
388
- async function detectHost() {
389
- try {
390
- const { stdout } = await execFileAsync("hostname", ["-I"]);
391
- const ips = stdout.trim().split(/\s+/);
392
- const nonLocal = ips.find((ip) => !ip.startsWith("127."));
393
- if (nonLocal)
394
- return nonLocal;
395
- return ips[0] || "127.0.0.1";
396
- }
397
- catch {
398
- return "127.0.0.1";
399
- }
400
- }
401
628
  export function parseDockerLabels(labelsStr) {
402
629
  const labels = {};
403
630
  for (const part of (labelsStr ?? "").split(",")) {
@@ -677,13 +904,33 @@ export function buildComputeWorkerReapPlan(workers, options = {}) {
677
904
  };
678
905
  }
679
906
  export async function releaseWorker(id) {
907
+ let inspected;
908
+ try {
909
+ const { stdout } = await execFileAsync("docker", ["inspect", id]);
910
+ inspected = JSON.parse(stdout);
911
+ }
912
+ catch {
913
+ throw new Error(`Unable to inspect Docker container "${id}" before release. No container was changed. ` +
914
+ `Run "pibo compute list --all" to find Pibo compute workers and verify Docker is available.`);
915
+ }
916
+ const container = inspected[0];
917
+ const resolvedId = container?.Id?.trim();
918
+ if (!container || inspected.length !== 1 || !resolvedId) {
919
+ throw new Error(`Unable to resolve Docker container "${id}" before release. No container was changed.`);
920
+ }
921
+ const role = container.Config?.Labels?.[LABEL_ROLE];
922
+ if (role !== "worker" && role !== "dev") {
923
+ const identity = role === undefined ? `${LABEL_ROLE} is missing` : `${LABEL_ROLE}=${JSON.stringify(role)}`;
924
+ throw new Error(`Refusing to release Docker container "${id}": ${identity}; expected "worker" or "dev". ` +
925
+ `No container was changed. Run "pibo compute list --all" to find releasable workers.`);
926
+ }
680
927
  try {
681
- await execFileAsync("docker", ["stop", "-t", "10", id]);
928
+ await execFileAsync("docker", ["stop", "-t", "10", resolvedId]);
682
929
  }
683
930
  catch {
684
931
  // might already be stopped
685
932
  }
686
- await execFileAsync("docker", ["rm", id]);
933
+ await execFileAsync("docker", ["rm", resolvedId]);
687
934
  }
688
935
  export async function applyComputeWorkerReapPlan(plan, options = {}) {
689
936
  const release = options.release ?? releaseWorker;
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { ensurePrivatePiboHomeForPath, piboHomePath } from "../core/pibo-home.js";
4
4
  import { protectPrivateFileSync } from "../core/private-path.js";
5
+ import { parsePreviewBaseURL } from "../previews/base-url.js";
5
6
  export const DEFAULT_PIBO_CONFIG_PATH = "config.json";
6
7
  export function getDefaultPiboConfigPath() {
7
8
  return piboHomePath(DEFAULT_PIBO_CONFIG_PATH);
@@ -137,6 +138,8 @@ function parseConfigValue(definition, value) {
137
138
  if (definition.key === "auth.secret" && value.length < 32) {
138
139
  throw new Error("auth.secret must be at least 32 characters");
139
140
  }
141
+ if (definition.key === "preview.baseURL")
142
+ parsePreviewBaseURL(value);
140
143
  if (definition.values && !definition.values.includes(value)) {
141
144
  throw new Error(`${definition.key} must be one of: ${definition.values.join(", ")}`);
142
145
  }
@@ -214,6 +214,7 @@ function withoutRequestedModels(profile) {
214
214
  mcpServers: profile.mcpServers,
215
215
  piPackages: profile.piPackages,
216
216
  contextFiles: profile.contextFiles,
217
+ diagnostics: profile.diagnostics,
217
218
  builtinTools: profile.builtinTools,
218
219
  builtinToolNames: profile.builtinToolNames,
219
220
  autoContextFiles: profile.autoContextFiles,
@@ -229,15 +230,15 @@ function toolDefinitionSchema(definition, toolInfo) {
229
230
  }
230
231
  function generatedOriginForTool(name, profile) {
231
232
  if (name === "runtime")
232
- return "Generated Pibo runtime tool selected by the profile.";
233
+ return "pibo-runtime";
233
234
  if (name.startsWith("pibo_agents_"))
234
- return "Generated shared agent-management tool from the profile's delegated-agent list.";
235
+ return "pibo-subagents";
235
236
  if (name.startsWith("pibo_run_"))
236
- return "Generated run-control tool from the pibo-run-control capability package.";
237
+ return "pibo-run-control";
237
238
  if (PIBO_GOAL_TOOL_NAMES.includes(name))
238
- return "Generated goal-control tool from the pibo-goal-control capability package.";
239
+ return "pibo-goal-control";
239
240
  if (name === "apply_patch" || name === "view_image")
240
- return "Generated Codex-compatible tool from the codex-compat package.";
241
+ return "codex-compat";
241
242
  if (profile.builtinToolNames.includes(name) || DEFAULT_BUILTIN_TOOL_NAMES.includes(name))
242
243
  return undefined;
243
244
  return undefined;
@@ -502,17 +503,6 @@ export async function inspectPiboContextBuild(options = {}) {
502
503
  },
503
504
  });
504
505
  }
505
- if (generatedOrigin) {
506
- children.push({
507
- id: `tools/${name}/generated-origin`,
508
- kind: "metadata",
509
- title: "Generated Origin",
510
- source: "generated",
511
- state: "active",
512
- badges: ["GENERATED", "PIBO"],
513
- hydratedText: generatedOrigin,
514
- });
515
- }
516
506
  if (providerTool?.providerTool.kind === "web_search") {
517
507
  children.push({
518
508
  id: `tools/${name}/provider-payload`,
@@ -568,6 +558,7 @@ export async function inspectPiboContextBuild(options = {}) {
568
558
  hasDefinition: Boolean(definition || info),
569
559
  description: definition?.description ?? info?.description ?? providerTool?.description,
570
560
  ...(toolSource === "pibo" || toolSource === "generated" ? { owner: "pibo", deliveryMode: "direct" } : {}),
561
+ ...(generatedOrigin ? { inspectorOrigin: { label: generatedOrigin, modelVisible: false } } : {}),
571
562
  },
572
563
  children,
573
564
  };