@pasko70/pibo 3.0.2 → 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 (69) hide show
  1. package/compute-image/Dockerfile +47 -0
  2. package/compute-image/Dockerfile.dockerignore +5 -0
  3. package/dist/agent-runtime/contract.js +78 -28
  4. package/dist/agent-runtime/portable-history.js +19 -9
  5. package/dist/agent-runtime/registry.js +8 -2
  6. package/dist/agent-runtime/routed-session.js +12 -6
  7. package/dist/agent-runtimes/pi/routed-session.js +10 -4
  8. package/dist/apps/chat/agent-store.js +55 -3
  9. package/dist/apps/chat/data/project-service.js +119 -15
  10. package/dist/apps/chat/data/room-service.js +21 -0
  11. package/dist/apps/chat/loop-api.js +10 -2
  12. package/dist/apps/chat/web-app.js +84 -25
  13. package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
  18. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  19. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  20. package/dist/apps/chat-ui/index.html +2 -2
  21. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  22. package/dist/apps/chat-vscode-web/index.html +1 -1
  23. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  24. package/dist/compute/cli.js +35 -16
  25. package/dist/compute/docker.js +341 -94
  26. package/dist/config/config.js +3 -0
  27. package/dist/core/session-router.js +33 -105
  28. package/dist/data/pibo-store.js +8 -1
  29. package/dist/data/schema.js +125 -5
  30. package/dist/debug/agents.js +91 -89
  31. package/dist/debug/index.js +2 -0
  32. package/dist/debug/pty.js +127 -48
  33. package/dist/gateway/cli.js +4 -0
  34. package/dist/loops/channel.js +3 -1
  35. package/dist/loops/cli.js +2 -1
  36. package/dist/loops/service.js +90 -11
  37. package/dist/loops/store.js +100 -74
  38. package/dist/mcp/agent-context.js +13 -26
  39. package/dist/mcp/commands/info.js +3 -1
  40. package/dist/mcp/config-command.js +46 -2
  41. package/dist/mcp/config.js +18 -4
  42. package/dist/plugins/context-files-store.js +6 -6
  43. package/dist/previews/base-url.js +34 -0
  44. package/dist/previews/config.js +2 -32
  45. package/dist/ralph/store.js +5 -0
  46. package/dist/reliability/store.js +174 -111
  47. package/dist/session-ui/terminalRows.js +2 -1
  48. package/dist/subagents/observation-query.js +121 -0
  49. package/dist/tools/agent-browser-leases.js +58 -23
  50. package/dist/tools/agent-browser-wrapper.js +1 -1
  51. package/dist/tools/browser-use-leases.js +129 -16
  52. package/dist/tools/browser-use-wrapper.js +1 -1
  53. package/dist/tools/index.js +27 -11
  54. package/dist/tools/python-runtime.js +10 -3
  55. package/dist/tools/registry.js +1 -1
  56. package/dist/tools/runtime/node-backend.js +16 -5
  57. package/dist/tools/runtime/node-worker-source.js +112 -33
  58. package/dist/tools/runtime/python-backend.js +16 -5
  59. package/dist/tools/runtime/python-worker-source.js +167 -16
  60. package/dist/tools/runtime/registry.js +23 -10
  61. package/dist/user-skills/store.js +0 -1
  62. package/npm-shrinkwrap.json +9 -2
  63. package/package.json +8 -2
  64. package/scripts/docker-entrypoint.sh +46 -0
  65. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  66. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  67. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
  68. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
  69. package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { existsSync, readFileSync } from 'node:fs';
3
- import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
4
+ import { cp, mkdir, rm, stat, writeFile } from 'node:fs/promises';
4
5
  import { join, resolve } from 'node:path';
5
6
  import { ErrorCode, formatCliError } from '../cli-errors.js';
6
7
  import { BROWSER_USE_DEFAULT_PROFILE } from './browser-use-wrapper.js';
@@ -10,6 +11,8 @@ const DEFAULT_APP = 'pibo-chat';
10
11
  const DEFAULT_TTL_MINUTES = 8 * 60;
11
12
  const DEFAULT_TEMPLATE_PROFILE = 'pibo-auth-template';
12
13
  const AUTH_POOL_DIR = 'auth-pool';
14
+ const REGISTRY_LOCK_OWNER_FILE = 'owner.json';
15
+ const LEGACY_REGISTRY_LOCK_STALE_MS = 10 * 60_000;
13
16
  function shellQuote(value) {
14
17
  return `'${value.replaceAll("'", "'\\''")}'`;
15
18
  }
@@ -32,6 +35,9 @@ function registryPath(status) {
32
35
  function lockDir(status) {
33
36
  return join(status.homeDir, AUTH_POOL_DIR, '.leases.lock');
34
37
  }
38
+ function lockOwnerPath(lock) {
39
+ return join(lock, REGISTRY_LOCK_OWNER_FILE);
40
+ }
35
41
  function appPoolDir(status, app) {
36
42
  return join(status.homeDir, AUTH_POOL_DIR, sanitizeIdPart(app));
37
43
  }
@@ -110,29 +116,135 @@ async function writeRegistry(status, registry) {
110
116
  async function withRegistryLock(status, action) {
111
117
  const lock = lockDir(status);
112
118
  await mkdir(join(status.homeDir, AUTH_POOL_DIR), { recursive: true });
113
- let acquired = false;
119
+ const token = randomUUID();
114
120
  for (let attempt = 0; attempt < 50; attempt += 1) {
115
121
  try {
116
122
  await mkdir(lock);
117
- acquired = true;
123
+ const processStartId = currentProcessStartId();
124
+ try {
125
+ await writeFile(lockOwnerPath(lock), `${JSON.stringify({
126
+ pid: process.pid,
127
+ token,
128
+ acquiredAt: Date.now(),
129
+ ...(processStartId ? { processStartId } : {}),
130
+ })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
131
+ }
132
+ catch (error) {
133
+ await rm(lock, { recursive: true, force: true });
134
+ throw error;
135
+ }
118
136
  break;
119
137
  }
120
138
  catch (error) {
121
139
  if (error.code !== 'EEXIST')
122
140
  throw error;
141
+ await recoverStaleRegistryLock(lock);
142
+ if (!existsSync(lock))
143
+ continue;
123
144
  await new Promise((resolve) => setTimeout(resolve, 50));
124
145
  }
125
146
  }
126
- if (!acquired) {
147
+ if (!await registryLockOwnedBy(lock, token)) {
127
148
  throw new Error('Timed out waiting for browser-use lease registry lock');
128
149
  }
129
150
  try {
130
151
  return await action();
131
152
  }
132
153
  finally {
154
+ await releaseRegistryLock(lock, token);
155
+ }
156
+ }
157
+ function currentProcessStartId() {
158
+ if (process.platform !== 'linux')
159
+ return undefined;
160
+ const identity = linuxProcessIdentity(process.pid);
161
+ return identity.liveness === 'active' ? identity.startId : undefined;
162
+ }
163
+ function linuxProcessIdentity(pid) {
164
+ let raw;
165
+ try {
166
+ raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
167
+ }
168
+ catch (error) {
169
+ const code = error.code;
170
+ return code === 'ENOENT' || code === 'ESRCH' ? { liveness: 'dead' } : { liveness: 'ambiguous' };
171
+ }
172
+ const commandEnd = raw.lastIndexOf(')');
173
+ if (commandEnd < 2 || commandEnd + 2 >= raw.length)
174
+ return { liveness: 'ambiguous' };
175
+ const fields = raw.slice(commandEnd + 2).trim().split(/\s+/);
176
+ if (fields[0] === 'Z')
177
+ return { liveness: 'dead' };
178
+ const startTicks = fields[19];
179
+ if (!startTicks || !/^[1-9][0-9]*$/.test(startTicks))
180
+ return { liveness: 'ambiguous' };
181
+ return { liveness: 'active', startId: `${pid}:${startTicks}` };
182
+ }
183
+ function validRegistryLockOwner(value) {
184
+ if (!value || typeof value !== 'object' || Array.isArray(value))
185
+ return undefined;
186
+ const owner = value;
187
+ if (!Number.isSafeInteger(owner.pid)
188
+ || (owner.pid ?? 0) <= 0
189
+ || typeof owner.token !== 'string'
190
+ || owner.token.length === 0
191
+ || !Number.isFinite(owner.acquiredAt)
192
+ || (owner.processStartId !== undefined && typeof owner.processStartId !== 'string'))
193
+ return undefined;
194
+ return owner;
195
+ }
196
+ function readRegistryLockOwner(lock) {
197
+ try {
198
+ return validRegistryLockOwner(JSON.parse(readFileSync(lockOwnerPath(lock), 'utf8')));
199
+ }
200
+ catch {
201
+ return undefined;
202
+ }
203
+ }
204
+ function registryLockOwnerLiveness(owner) {
205
+ if (process.platform === 'linux') {
206
+ const identity = linuxProcessIdentity(owner.pid);
207
+ if (identity.liveness !== 'active')
208
+ return identity.liveness;
209
+ if (!owner.processStartId || !identity.startId)
210
+ return 'ambiguous';
211
+ return owner.processStartId === identity.startId ? 'active' : 'dead';
212
+ }
213
+ try {
214
+ process.kill(owner.pid, 0);
215
+ return 'active';
216
+ }
217
+ catch (error) {
218
+ return error.code === 'ESRCH' ? 'dead' : 'ambiguous';
219
+ }
220
+ }
221
+ async function registryLockOwnedBy(lock, token) {
222
+ return readRegistryLockOwner(lock)?.token === token;
223
+ }
224
+ async function releaseRegistryLock(lock, token) {
225
+ if (await registryLockOwnedBy(lock, token)) {
133
226
  await rm(lock, { recursive: true, force: true });
134
227
  }
135
228
  }
229
+ async function recoverStaleRegistryLock(lock) {
230
+ const owner = readRegistryLockOwner(lock);
231
+ if (owner) {
232
+ if (registryLockOwnerLiveness(owner) === 'dead' && readRegistryLockOwner(lock)?.token === owner.token) {
233
+ await rm(lock, { recursive: true, force: true });
234
+ }
235
+ return;
236
+ }
237
+ try {
238
+ const lockStat = await stat(lock);
239
+ if (Date.now() - lockStat.mtimeMs > LEGACY_REGISTRY_LOCK_STALE_MS) {
240
+ await rm(lock, { recursive: true, force: true });
241
+ }
242
+ }
243
+ catch (error) {
244
+ if (error.code !== 'ENOENT')
245
+ throw error;
246
+ }
247
+ }
136
248
  export function isExpired(lease, now = new Date()) {
137
249
  return new Date(lease.expiresAt).getTime() <= now.getTime();
138
250
  }
@@ -346,7 +458,7 @@ export async function acquireBrowserUseLease(context, options = {}) {
346
458
  const ttlMinutes = options.ttlMinutes ?? DEFAULT_TTL_MINUTES;
347
459
  const profileName = options.profileName?.trim() || BROWSER_USE_DEFAULT_PROFILE;
348
460
  const templateDir = options.templateDir?.trim() || defaultTemplateDir(context.status);
349
- await withRegistryLock(context.status, async () => {
461
+ const { lease, reapedCount } = await withRegistryLock(context.status, async () => {
350
462
  const registry = readRegistry(context.status);
351
463
  const reapedCount = await reapStaleLeasesInRegistry(context.status, registry);
352
464
  let lease = selectReusableLease(context.status, registry.leases, app);
@@ -380,18 +492,19 @@ export async function acquireBrowserUseLease(context, options = {}) {
380
492
  registry.leases.push(lease);
381
493
  }
382
494
  await writeRegistry(context.status, registry);
383
- if (reapedCount > 0 && !options.json) {
384
- console.error(`Reaped ${reapedCount} stale lease${reapedCount === 1 ? '' : 's'}`);
385
- }
386
- if (options.json)
387
- printLeaseJson(context.status, lease);
388
- else
389
- printLeaseEnv(context.status, lease);
390
- const warmup = await warmupBrowserUseLease(context, lease, 15000, options.headed);
391
- if (!warmup.success && !options.json) {
392
- console.error(`Warning: Browser warm-up failed: ${warmup.error?.trim() || 'unknown error'}`);
393
- }
495
+ return { lease, reapedCount };
394
496
  });
497
+ if (reapedCount > 0 && !options.json) {
498
+ console.error(`Reaped ${reapedCount} stale lease${reapedCount === 1 ? '' : 's'}`);
499
+ }
500
+ if (options.json)
501
+ printLeaseJson(context.status, lease);
502
+ else
503
+ printLeaseEnv(context.status, lease);
504
+ const warmup = await warmupBrowserUseLease(context, lease, 15000, options.headed);
505
+ if (!warmup.success && !options.json) {
506
+ console.error(`Warning: Browser warm-up failed: ${warmup.error?.trim() || 'unknown error'}`);
507
+ }
395
508
  }
396
509
  async function warmupBrowserUseLease(context, lease, timeoutMs = 15000, headed = false) {
397
510
  const wrapperPath = join(context.status.homeDir, 'bin', 'browser-use');
@@ -771,7 +771,7 @@ exec "$real_browser_use" "$@"
771
771
  `;
772
772
  }
773
773
  export function ensureBrowserUseWrapper(status) {
774
- if (status.entry.name !== 'browser-use')
774
+ if (!status.installed || status.entry.name !== 'browser-use')
775
775
  return undefined;
776
776
  const wrapperPath = join(status.homeDir, 'bin', 'browser-use');
777
777
  mkdirSync(dirname(wrapperPath), { recursive: true });
@@ -60,17 +60,21 @@ function printShow(name) {
60
60
  const wrapperPath = ensureBrowserUseWrapper(status);
61
61
  console.log(` wrapper: ${wrapperPath ?? 'not generated'}`);
62
62
  console.log(` executable: ${status.executablePath}`);
63
- console.log('');
64
- console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
65
- console.log(' The wrapper manages persistent Chrome profiles and CDP automatically.');
63
+ if (wrapperPath) {
64
+ console.log('');
65
+ console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
66
+ console.log(' The wrapper manages persistent Chrome profiles and CDP automatically.');
67
+ }
66
68
  }
67
69
  else if (entry.name === 'agent-browser') {
68
70
  const wrapperPath = ensureAgentBrowserWrapper(status);
69
71
  console.log(` wrapper: ${wrapperPath ?? 'not generated'}`);
70
72
  console.log(` executable: ${status.executablePath}`);
71
- console.log('');
72
- console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
73
- console.log(' The wrapper keeps Agent Browser state under the Pibo tool home.');
73
+ if (wrapperPath) {
74
+ console.log('');
75
+ console.log(' IMPORTANT: Always use the wrapper path above, not the raw executable.');
76
+ console.log(' The wrapper keeps Agent Browser state under the Pibo tool home.');
77
+ }
74
78
  }
75
79
  else {
76
80
  console.log(` executable: ${status.executablePath}`);
@@ -88,8 +92,9 @@ function printShow(name) {
88
92
  }
89
93
  console.log('');
90
94
  console.log('Next:');
91
- if (entry.kind !== 'internal')
92
- console.log(` pibo tools env ${entry.name}`);
95
+ if (entry.kind !== 'internal') {
96
+ console.log(status.installed ? ` pibo tools env ${entry.name}` : ` pibo tools install ${entry.name}`);
97
+ }
93
98
  console.log(` pibo tools guide ${entry.name} ${entry.guides[0]?.name ?? ''}`.trimEnd());
94
99
  if (entry.name === 'browser-use') {
95
100
  console.log(' pibo tools browser-use');
@@ -133,9 +138,21 @@ function printGuide(name, guideName) {
133
138
  }
134
139
  console.log(guide.content);
135
140
  }
136
- function printPath(name) {
141
+ function requireInstalledTool(name) {
137
142
  const entry = requireEntry(name);
138
143
  const status = getCliToolStatus(entry);
144
+ if (!status.installed) {
145
+ throw new Error(formatCliError({
146
+ code: ErrorCode.CLIENT_ERROR,
147
+ type: 'CLI_TOOL_NOT_INSTALLED',
148
+ message: `Tool "${name}" is not installed`,
149
+ suggestion: `Run pibo tools install ${name}.`,
150
+ }));
151
+ }
152
+ return { entry, status };
153
+ }
154
+ function printPath(name) {
155
+ const { entry, status } = requireInstalledTool(name);
139
156
  if (entry.name === 'browser-use') {
140
157
  const wrapperPath = ensureBrowserUseWrapper(status);
141
158
  if (wrapperPath) {
@@ -153,8 +170,7 @@ function printPath(name) {
153
170
  console.log(status.executablePath);
154
171
  }
155
172
  function printEnv(name) {
156
- const entry = requireEntry(name);
157
- const status = getCliToolStatus(entry);
173
+ const { entry, status } = requireInstalledTool(name);
158
174
  const desktop = detectDesktopEnv();
159
175
  if (entry.kind === 'internal') {
160
176
  console.log(`# ${entry.name} is built into pibo; no extra environment is required.`);
@@ -52,7 +52,7 @@ function runBuffered(command, args, env = process.env) {
52
52
  function isRootUser() {
53
53
  return typeof process.getuid === 'function' && process.getuid() === 0;
54
54
  }
55
- async function resolveUvCommand() {
55
+ async function findUvCommand() {
56
56
  const uv = await runBuffered('uv', ['--version']);
57
57
  if (uv.ok)
58
58
  return 'uv';
@@ -62,7 +62,14 @@ async function resolveUvCommand() {
62
62
  if (local.ok)
63
63
  return localUv;
64
64
  }
65
+ return undefined;
66
+ }
67
+ async function ensureUvCommand() {
68
+ const uvCommand = await findUvCommand();
69
+ if (uvCommand)
70
+ return uvCommand;
65
71
  if (process.platform === 'linux' && isRootUser()) {
72
+ const localUv = join(homedir(), '.local', 'bin', 'uv');
66
73
  console.log('uv was not found on PATH. Installing uv automatically.');
67
74
  await runInheritedCommand('sh', ['-c', 'curl -LsSf https://astral.sh/uv/install.sh | sh']);
68
75
  const installed = await runBuffered(localUv, ['--version'], {
@@ -107,7 +114,7 @@ export function runInheritedCommand(command, args, env = process.env) {
107
114
  }
108
115
  export async function printToolPythonRuntimeDoctor(name, spec) {
109
116
  const paths = getToolPythonRuntimePaths(name, spec);
110
- const uvCommand = await resolveUvCommand();
117
+ const uvCommand = await findUvCommand();
111
118
  const uv = uvCommand ? await runBuffered(uvCommand, ['--version']) : { ok: false, output: 'missing' };
112
119
  const python = uv.ok
113
120
  ? await runBuffered(uvCommand, ['python', 'find', spec.pythonVersion])
@@ -148,7 +155,7 @@ export async function printToolPythonRuntimeDoctor(name, spec) {
148
155
  }
149
156
  }
150
157
  export async function installToolPythonRuntime(name, spec) {
151
- const uvCommand = await resolveUvCommand();
158
+ const uvCommand = await ensureUvCommand();
152
159
  if (!uvCommand) {
153
160
  throw new Error(formatCliError({
154
161
  code: ErrorCode.CLIENT_ERROR,
@@ -220,7 +220,7 @@ export async function installCliTool(entry, runSetup) {
220
220
  if ((entry.name === 'browser-use' || entry.name === 'agent-browser') && process.platform === 'linux' && !hasDesktopDisplay(detectDesktopEnv())) {
221
221
  printLinuxVirtualDisplayHint(' ');
222
222
  }
223
- console.log(` env: pibo tools env ${entry.name}`);
223
+ console.log(status.installed ? ` env: pibo tools env ${entry.name}` : ` install: pibo tools install ${entry.name}`);
224
224
  }
225
225
  export async function removeCliTool(entry) {
226
226
  if (entry.kind === 'internal') {
@@ -46,13 +46,23 @@ export class NodeRuntimeBackend {
46
46
  this.child = spawn(executable, [...args, "-e", NODE_RUNTIME_WORKER_SOURCE], {
47
47
  cwd,
48
48
  env: { ...process.env, ...(env ?? {}) },
49
- stdio: "pipe",
49
+ stdio: ["pipe", "pipe", "pipe", "pipe"],
50
+ });
51
+ const protocol = this.child.stdio[3];
52
+ if (!protocol)
53
+ throw new Error("Node runtime protocol pipe was not created");
54
+ const responses = createInterface({ input: protocol });
55
+ responses.on("line", (line) => this.handleLine(line));
56
+ this.child.stdout.on("data", (chunk) => {
57
+ this.diagnostics += String(chunk);
50
58
  });
51
- const stdout = createInterface({ input: this.child.stdout });
52
- stdout.on("line", (line) => this.handleLine(line));
53
59
  this.child.stderr.on("data", (chunk) => {
54
60
  this.diagnostics += String(chunk);
55
61
  });
62
+ this.child.stdin.on("error", (error) => {
63
+ this.alive = false;
64
+ this.rejectAll(error);
65
+ });
56
66
  this.child.once("error", (error) => {
57
67
  this.alive = false;
58
68
  this.readyReject(error);
@@ -78,7 +88,7 @@ export class NodeRuntimeBackend {
78
88
  }
79
89
  }
80
90
  isAlive() {
81
- return this.alive && !this.child.killed;
91
+ return this.alive;
82
92
  }
83
93
  getRecord() {
84
94
  return { pid: this.child.pid, cwd: this.cwd, executable: this.executable };
@@ -146,7 +156,8 @@ export class NodeRuntimeBackend {
146
156
  async interrupt() {
147
157
  if (!this.isAlive())
148
158
  return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
149
- this.child.kill("SIGINT");
159
+ if (!this.child.kill("SIGINT"))
160
+ return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
150
161
  return { status: "ok", sessionId: "", message: "Sent SIGINT to runtime worker" };
151
162
  }
152
163
  async close(force = false) {
@@ -1,8 +1,11 @@
1
1
  export const NODE_RUNTIME_WORKER_SOURCE = String.raw `
2
2
  const vm = require("node:vm");
3
3
  const util = require("node:util");
4
+ const fs = require("node:fs");
5
+ const childProcess = require("node:child_process");
6
+ const { AsyncLocalStorage } = require("node:async_hooks");
4
7
 
5
- let currentOutput = null;
8
+ const outputStorage = new AsyncLocalStorage();
6
9
 
7
10
  function bounded(value, maxBytes = 8192) {
8
11
  const text = String(value);
@@ -48,14 +51,91 @@ function errorSummary(error) {
48
51
  return out;
49
52
  }
50
53
 
54
+ function appendOutput(output, stream, chunk) {
55
+ if (output) output[stream] += String(chunk);
56
+ else if (stream === "stdout") process.stdout.write(String(chunk));
57
+ else process.stderr.write(String(chunk));
58
+ }
59
+
51
60
  function appendStdout(chunk) {
52
- if (currentOutput) currentOutput.stdout += String(chunk);
61
+ appendOutput(outputStorage.getStore(), "stdout", chunk);
53
62
  }
54
63
 
55
64
  function appendStderr(chunk) {
56
- if (currentOutput) currentOutput.stderr += String(chunk);
65
+ appendOutput(outputStorage.getStore(), "stderr", chunk);
66
+ }
67
+
68
+ function routedStdio(stdio) {
69
+ if (stdio === "inherit") {
70
+ return { stdio: ["inherit", "pipe", "pipe"], stdout: true, stderr: true };
71
+ }
72
+ if (!Array.isArray(stdio)) return undefined;
73
+ const next = [...stdio];
74
+ const stdout = next[1] === "inherit" || next[1] === 1;
75
+ const stderr = next[2] === "inherit" || next[2] === 2;
76
+ if (!stdout && !stderr) return undefined;
77
+ if (stdout) next[1] = "pipe";
78
+ if (stderr) next[2] = "pipe";
79
+ return { stdio: next, stdout, stderr };
80
+ }
81
+
82
+ function captureSpawnedOutput(child, output, route) {
83
+ if (route.stdout && child.stdout) child.stdout.on("data", (chunk) => appendOutput(output, "stdout", chunk));
84
+ if (route.stderr && child.stderr) child.stderr.on("data", (chunk) => appendOutput(output, "stderr", chunk));
85
+ if (child.spawnargs && child.spawnargs.length > 0 && child.stdout && typeof child.stdout.unref === "function") child.stdout.unref();
86
+ if (child.spawnargs && child.spawnargs.length > 0 && child.stderr && typeof child.stderr.unref === "function") child.stderr.unref();
87
+ }
88
+
89
+ function routedSpawn(command, args, options) {
90
+ const hasArgs = Array.isArray(args);
91
+ const actualArgs = hasArgs ? args : [];
92
+ const actualOptions = (hasArgs ? options : args) || {};
93
+ const output = outputStorage.getStore();
94
+ const route = output ? routedStdio(actualOptions.stdio) : undefined;
95
+ if (!route) return hasArgs ? childProcess.spawn(command, actualArgs, actualOptions) : childProcess.spawn(command, actualOptions);
96
+ const child = childProcess.spawn(command, actualArgs, { ...actualOptions, stdio: route.stdio });
97
+ captureSpawnedOutput(child, output, route);
98
+ return child;
99
+ }
100
+
101
+ function routedSpawnSync(command, args, options) {
102
+ const hasArgs = Array.isArray(args);
103
+ const actualArgs = hasArgs ? args : [];
104
+ const actualOptions = (hasArgs ? options : args) || {};
105
+ const output = outputStorage.getStore();
106
+ const route = output ? routedStdio(actualOptions.stdio) : undefined;
107
+ if (!route) return hasArgs ? childProcess.spawnSync(command, actualArgs, actualOptions) : childProcess.spawnSync(command, actualOptions);
108
+ const result = childProcess.spawnSync(command, actualArgs, { ...actualOptions, stdio: route.stdio });
109
+ if (route.stdout && result.stdout != null) appendOutput(output, "stdout", result.stdout);
110
+ if (route.stderr && result.stderr != null) appendOutput(output, "stderr", result.stderr);
111
+ if (route.stdout) {
112
+ result.stdout = null;
113
+ if (result.output) result.output[1] = null;
114
+ }
115
+ if (route.stderr) {
116
+ result.stderr = null;
117
+ if (result.output) result.output[2] = null;
118
+ }
119
+ return result;
57
120
  }
58
121
 
122
+ const childProcessProxy = new Proxy(childProcess, {
123
+ get(target, prop, receiver) {
124
+ if (prop === "spawn") return routedSpawn;
125
+ if (prop === "spawnSync") return routedSpawnSync;
126
+ return Reflect.get(target, prop, receiver);
127
+ },
128
+ });
129
+
130
+ function runtimeRequire(specifier) {
131
+ if (specifier === "child_process" || specifier === "node:child_process") return childProcessProxy;
132
+ return require(specifier);
133
+ }
134
+ runtimeRequire.resolve = require.resolve;
135
+ runtimeRequire.cache = require.cache;
136
+ runtimeRequire.extensions = require.extensions;
137
+ runtimeRequire.main = require.main;
138
+
59
139
  const processProxy = new Proxy(process, {
60
140
  get(target, prop, receiver) {
61
141
  if (prop === "stdout") return { write: (chunk) => { appendStdout(chunk); return true; } };
@@ -75,7 +155,7 @@ const consoleProxy = {
75
155
 
76
156
  const context = vm.createContext({
77
157
  console: consoleProxy,
78
- require,
158
+ require: runtimeRequire,
79
159
  process: processProxy,
80
160
  Buffer,
81
161
  URL,
@@ -97,34 +177,33 @@ async function execute(req) {
97
177
  const mode = req.mode || "exec";
98
178
  const code = req.code || "";
99
179
  const output = { stdout: "", stderr: "" };
100
- currentOutput = output;
101
- try {
102
- const value = await (async () => {
103
- if (mode === "eval") {
104
- return await vm.runInContext(code, context, { filename: "<pibo-runtime>", timeout: Number(req.timeoutMs || 30000) });
105
- }
106
- const result = vm.runInContext(code, context, { filename: "<pibo-runtime>", timeout: Number(req.timeoutMs || 30000) });
107
- if (result && typeof result.then === "function") await result;
108
- return undefined;
109
- })();
110
- return {
111
- id: req.id,
112
- status: "ok",
113
- stdout: output.stdout,
114
- stderr: output.stderr,
115
- result: value === undefined ? null : summarize(value),
116
- };
117
- } catch (error) {
118
- return {
119
- id: req.id,
120
- status: "error",
121
- stdout: output.stdout,
122
- stderr: output.stderr,
123
- error: errorSummary(error),
124
- };
125
- } finally {
126
- currentOutput = null;
127
- }
180
+ return await outputStorage.run(output, async () => {
181
+ try {
182
+ const value = await (async () => {
183
+ if (mode === "eval") {
184
+ return await vm.runInContext(code, context, { filename: "<pibo-runtime>", timeout: Number(req.timeoutMs || 30000) });
185
+ }
186
+ const result = vm.runInContext(code, context, { filename: "<pibo-runtime>", timeout: Number(req.timeoutMs || 30000) });
187
+ if (result && typeof result.then === "function") await result;
188
+ return undefined;
189
+ })();
190
+ return {
191
+ id: req.id,
192
+ status: "ok",
193
+ stdout: output.stdout,
194
+ stderr: output.stderr,
195
+ result: value === undefined ? null : summarize(value),
196
+ };
197
+ } catch (error) {
198
+ return {
199
+ id: req.id,
200
+ status: "error",
201
+ stdout: output.stdout,
202
+ stderr: output.stderr,
203
+ error: errorSummary(error),
204
+ };
205
+ }
206
+ });
128
207
  }
129
208
 
130
209
  async function inspectValue(req) {
@@ -167,7 +246,7 @@ function listVars(req) {
167
246
  }
168
247
 
169
248
  function writeResponse(resp) {
170
- process.stdout.write(JSON.stringify(resp) + "\n");
249
+ fs.writeSync(3, JSON.stringify(resp) + "\n");
171
250
  }
172
251
 
173
252
  async function handle(req) {
@@ -46,13 +46,23 @@ export class PythonRuntimeBackend {
46
46
  this.child = spawn(executable, [...args, "-u", "-c", PYTHON_RUNTIME_WORKER_SOURCE], {
47
47
  cwd,
48
48
  env: { ...process.env, ...(env ?? {}) },
49
- stdio: "pipe",
49
+ stdio: ["pipe", "pipe", "pipe", "pipe"],
50
+ });
51
+ const protocol = this.child.stdio[3];
52
+ if (!protocol)
53
+ throw new Error("Python runtime protocol pipe was not created");
54
+ const responses = createInterface({ input: protocol });
55
+ responses.on("line", (line) => this.handleLine(line));
56
+ this.child.stdout.on("data", (chunk) => {
57
+ this.diagnostics += String(chunk);
50
58
  });
51
- const stdout = createInterface({ input: this.child.stdout });
52
- stdout.on("line", (line) => this.handleLine(line));
53
59
  this.child.stderr.on("data", (chunk) => {
54
60
  this.diagnostics += String(chunk);
55
61
  });
62
+ this.child.stdin.on("error", (error) => {
63
+ this.alive = false;
64
+ this.rejectAll(error);
65
+ });
56
66
  this.child.once("error", (error) => {
57
67
  this.alive = false;
58
68
  this.readyReject(error);
@@ -78,7 +88,7 @@ export class PythonRuntimeBackend {
78
88
  }
79
89
  }
80
90
  isAlive() {
81
- return this.alive && !this.child.killed;
91
+ return this.alive;
82
92
  }
83
93
  getRecord() {
84
94
  return { pid: this.child.pid, cwd: this.cwd, executable: this.executable };
@@ -145,7 +155,8 @@ export class PythonRuntimeBackend {
145
155
  async interrupt() {
146
156
  if (!this.isAlive())
147
157
  return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
148
- this.child.kill("SIGINT");
158
+ if (!this.child.kill("SIGINT"))
159
+ return { status: "failed", sessionId: "", message: "Runtime worker is not alive" };
149
160
  return { status: "ok", sessionId: "", message: "Sent SIGINT to runtime worker" };
150
161
  }
151
162
  async close(force = false) {