@pasko70/pibo 3.0.2 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) 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/output-render-sequence.js +148 -1
  28. package/dist/core/session-router.js +33 -105
  29. package/dist/data/ingest-service.js +14 -6
  30. package/dist/data/pibo-store.js +8 -1
  31. package/dist/data/schema.js +135 -5
  32. package/dist/debug/agents.js +91 -89
  33. package/dist/debug/index.js +2 -0
  34. package/dist/debug/pty.js +127 -48
  35. package/dist/gateway/cli.js +4 -0
  36. package/dist/loops/channel.js +3 -1
  37. package/dist/loops/cli.js +2 -1
  38. package/dist/loops/service.js +90 -11
  39. package/dist/loops/store.js +100 -74
  40. package/dist/mcp/agent-context.js +13 -26
  41. package/dist/mcp/commands/info.js +3 -1
  42. package/dist/mcp/config-command.js +46 -2
  43. package/dist/mcp/config.js +18 -4
  44. package/dist/plugins/context-files-store.js +6 -6
  45. package/dist/previews/base-url.js +34 -0
  46. package/dist/previews/config.js +2 -32
  47. package/dist/ralph/store.js +5 -0
  48. package/dist/reliability/store.js +174 -111
  49. package/dist/session-ui/terminalRows.js +2 -1
  50. package/dist/sessions/pibo-data-store.js +81 -0
  51. package/dist/subagents/observation-query.js +121 -0
  52. package/dist/tools/agent-browser-leases.js +58 -23
  53. package/dist/tools/agent-browser-wrapper.js +1 -1
  54. package/dist/tools/browser-use-leases.js +129 -16
  55. package/dist/tools/browser-use-wrapper.js +1 -1
  56. package/dist/tools/index.js +27 -11
  57. package/dist/tools/python-runtime.js +10 -3
  58. package/dist/tools/registry.js +1 -1
  59. package/dist/tools/runtime/node-backend.js +16 -5
  60. package/dist/tools/runtime/node-worker-source.js +112 -33
  61. package/dist/tools/runtime/python-backend.js +16 -5
  62. package/dist/tools/runtime/python-worker-source.js +167 -16
  63. package/dist/tools/runtime/registry.js +23 -10
  64. package/dist/user-skills/store.js +0 -1
  65. package/npm-shrinkwrap.json +9 -2
  66. package/package.json +8 -2
  67. package/scripts/docker-entrypoint.sh +46 -0
  68. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  69. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  70. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
  71. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
  72. package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
@@ -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) {
@@ -1,13 +1,151 @@
1
1
  export const PYTHON_RUNTIME_WORKER_SOURCE = String.raw `
2
- import contextlib
2
+ import contextvars
3
3
  import inspect
4
- import io
5
4
  import json
5
+ import os
6
6
  import signal
7
+ import subprocess
7
8
  import sys
9
+ import threading
8
10
  import traceback
9
11
 
10
12
  user_globals = {"__name__": "__pibo_runtime__"}
13
+ protocol_stream = os.fdopen(3, "w", encoding="utf-8", buffering=1, closefd=False)
14
+ output_context = contextvars.ContextVar("pibo_runtime_output", default=None)
15
+ original_stdout = sys.stdout
16
+ original_stderr = sys.stderr
17
+ original_popen = subprocess.Popen
18
+
19
+
20
+ def append_output(output, stream, value):
21
+ text = str(value)
22
+ if output is None:
23
+ target = original_stdout if stream == "stdout" else original_stderr
24
+ target.write(text)
25
+ target.flush()
26
+ return
27
+ with output["lock"]:
28
+ output[stream].append(text)
29
+
30
+
31
+ class BinaryOutputProxy:
32
+ def __init__(self, stream, fallback):
33
+ self.stream = stream
34
+ self.fallback = fallback
35
+
36
+ def write(self, value):
37
+ data = bytes(value)
38
+ append_output(output_context.get(), self.stream, data.decode("utf-8", "replace"))
39
+ return len(data)
40
+
41
+ def flush(self):
42
+ self.fallback.flush()
43
+
44
+ def __getattr__(self, name):
45
+ return getattr(self.fallback, name)
46
+
47
+
48
+ class OutputProxy:
49
+ def __init__(self, stream, fallback):
50
+ self.stream = stream
51
+ self.fallback = fallback
52
+ self.encoding = getattr(fallback, "encoding", "utf-8")
53
+ self.errors = getattr(fallback, "errors", "replace")
54
+ self.buffer = BinaryOutputProxy(stream, fallback.buffer)
55
+
56
+ def write(self, value):
57
+ append_output(output_context.get(), self.stream, value)
58
+ return len(str(value))
59
+
60
+ def flush(self):
61
+ self.fallback.flush()
62
+
63
+ def __getattr__(self, name):
64
+ return getattr(self.fallback, name)
65
+
66
+
67
+ sys.stdout = OutputProxy("stdout", original_stdout)
68
+ sys.stderr = OutputProxy("stderr", original_stderr)
69
+
70
+
71
+ def drain_child_output(read_fd, output, stream):
72
+ try:
73
+ with os.fdopen(read_fd, "rb", closefd=True) as source:
74
+ while True:
75
+ chunk = source.read(65536)
76
+ if not chunk:
77
+ return
78
+ append_output(output, stream, chunk.decode("utf-8", "replace"))
79
+ except Exception:
80
+ return
81
+
82
+
83
+ def join_child_output(threads):
84
+ for thread in threads:
85
+ thread.join(0.2)
86
+
87
+
88
+ def routed_popen(*args, **kwargs):
89
+ output = output_context.get()
90
+ if output is None:
91
+ return original_popen(*args, **kwargs)
92
+
93
+ options = dict(kwargs)
94
+ routes = []
95
+ if options.get("stdout") is None:
96
+ read_fd, write_fd = os.pipe()
97
+ options["stdout"] = write_fd
98
+ routes.append((read_fd, write_fd, "stdout"))
99
+ if options.get("stderr") is None:
100
+ read_fd, write_fd = os.pipe()
101
+ options["stderr"] = write_fd
102
+ routes.append((read_fd, write_fd, "stderr"))
103
+
104
+ try:
105
+ process = original_popen(*args, **options)
106
+ except Exception:
107
+ for read_fd, write_fd, _ in routes:
108
+ os.close(read_fd)
109
+ os.close(write_fd)
110
+ raise
111
+
112
+ process_threads = []
113
+ for read_fd, write_fd, stream in routes:
114
+ os.close(write_fd)
115
+ thread = threading.Thread(target=drain_child_output, args=(read_fd, output, stream), daemon=True)
116
+ with output["lock"]:
117
+ output["threads"].append(thread)
118
+ process_threads.append(thread)
119
+ thread.start()
120
+
121
+ if process_threads:
122
+ original_wait = process.wait
123
+ original_poll = process.poll
124
+ original_communicate = process.communicate
125
+
126
+ def wait_with_output(*wait_args, **wait_kwargs):
127
+ result = original_wait(*wait_args, **wait_kwargs)
128
+ join_child_output(process_threads)
129
+ return result
130
+
131
+ def poll_with_output(*poll_args, **poll_kwargs):
132
+ result = original_poll(*poll_args, **poll_kwargs)
133
+ if result is not None:
134
+ join_child_output(process_threads)
135
+ return result
136
+
137
+ def communicate_with_output(*communicate_args, **communicate_kwargs):
138
+ result = original_communicate(*communicate_args, **communicate_kwargs)
139
+ join_child_output(process_threads)
140
+ return result
141
+
142
+ process.wait = wait_with_output
143
+ process.poll = poll_with_output
144
+ process.communicate = communicate_with_output
145
+ return process
146
+
147
+
148
+ subprocess.Popen = routed_popen
11
149
 
12
150
 
13
151
  def bounded(value, max_bytes=8192):
@@ -70,31 +208,44 @@ def error_summary(exc):
70
208
  return out
71
209
 
72
210
 
211
+ def output_snapshot(output):
212
+ with output["lock"]:
213
+ threads = list(output["threads"])
214
+ for thread in threads:
215
+ thread.join(0.05)
216
+ with output["lock"]:
217
+ return "".join(output["stdout"]), "".join(output["stderr"])
218
+
219
+
73
220
  def execute(req):
74
221
  mode = req.get("mode") or "exec"
75
222
  code = req.get("code") or ""
76
223
  if mode == "auto":
77
224
  mode = "exec"
78
- stdout = io.StringIO()
79
- stderr = io.StringIO()
225
+ output = {"stdout": [], "stderr": [], "threads": [], "lock": threading.Lock()}
226
+ token = output_context.set(output)
80
227
  try:
81
- with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
82
- if mode == "eval":
83
- value = eval(compile(code, "<pibo-runtime>", "eval"), user_globals, user_globals)
84
- else:
85
- exec(compile(code, "<pibo-runtime>", "exec"), user_globals, user_globals)
86
- value = None
228
+ if mode == "eval":
229
+ value = eval(compile(code, "<pibo-runtime>", "eval"), user_globals, user_globals)
230
+ else:
231
+ exec(compile(code, "<pibo-runtime>", "exec"), user_globals, user_globals)
232
+ value = None
233
+ stdout, stderr = output_snapshot(output)
87
234
  return {
88
235
  "id": req.get("id"),
89
236
  "status": "ok",
90
- "stdout": stdout.getvalue(),
91
- "stderr": stderr.getvalue(),
237
+ "stdout": stdout,
238
+ "stderr": stderr,
92
239
  "result": summarize(value) if value is not None else None,
93
240
  }
94
241
  except KeyboardInterrupt as exc:
95
- return {"id": req.get("id"), "status": "interrupted", "stdout": stdout.getvalue(), "stderr": stderr.getvalue(), "error": error_summary(exc)}
242
+ stdout, stderr = output_snapshot(output)
243
+ return {"id": req.get("id"), "status": "interrupted", "stdout": stdout, "stderr": stderr, "error": error_summary(exc)}
96
244
  except Exception as exc:
97
- return {"id": req.get("id"), "status": "error", "stdout": stdout.getvalue(), "stderr": stderr.getvalue(), "error": error_summary(exc)}
245
+ stdout, stderr = output_snapshot(output)
246
+ return {"id": req.get("id"), "status": "error", "stdout": stdout, "stderr": stderr, "error": error_summary(exc)}
247
+ finally:
248
+ output_context.reset(token)
98
249
 
99
250
 
100
251
  def inspect_value(req):
@@ -147,8 +298,8 @@ def list_vars(req):
147
298
 
148
299
 
149
300
  def write_response(resp):
150
- sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
151
- sys.stdout.flush()
301
+ protocol_stream.write(json.dumps(resp, ensure_ascii=False) + "\n")
302
+ protocol_stream.flush()
152
303
 
153
304
 
154
305
  def main():
@@ -106,12 +106,7 @@ export class RuntimeSessionRegistry {
106
106
  result.executionCount = session.executionCount;
107
107
  session.lastExecAt = startedAt;
108
108
  session.updatedAt = nowIso();
109
- if (result.status === "failed" && !session.backend.isAlive()) {
110
- session.status = "failed";
111
- }
112
- else {
113
- session.status = "idle";
114
- }
109
+ session.status = session.backend.isAlive() ? "idle" : "failed";
115
110
  this.appendHistory(session, {
116
111
  id: randomUUID(),
117
112
  startedAt,
@@ -131,6 +126,7 @@ export class RuntimeSessionRegistry {
131
126
  if (!session)
132
127
  return notFoundInspect(input.sessionId);
133
128
  const result = await session.backend.inspect(input);
129
+ this.reconcileSession(session);
134
130
  return { ...result, sessionId: session.sessionId };
135
131
  }
136
132
  async vars(controllerPiboSessionId, input) {
@@ -138,6 +134,7 @@ export class RuntimeSessionRegistry {
138
134
  if (!session)
139
135
  return notFoundVars(input.sessionId);
140
136
  const result = await session.backend.vars(input);
137
+ this.reconcileSession(session);
141
138
  return { ...result, sessionId: session.sessionId };
142
139
  }
143
140
  async interrupt(controllerPiboSessionId, input) {
@@ -145,7 +142,12 @@ export class RuntimeSessionRegistry {
145
142
  const sessionId = input.sessionId ?? session?.sessionId ?? "auto";
146
143
  if (!session)
147
144
  return { status: "not_found", sessionId, message: `Runtime session "${sessionId}" was not found.` };
145
+ if (session.status === "closed" || session.status === "failed")
146
+ return { status: "failed", sessionId, message: "Runtime worker is not alive" };
147
+ if (session.status !== "busy")
148
+ return { status: "ok", sessionId, message: "Runtime session is idle; no active execution to interrupt" };
148
149
  const result = await session.backend.interrupt();
150
+ this.reconcileSession(session);
149
151
  return { ...result, sessionId: session.sessionId };
150
152
  }
151
153
  async close(controllerPiboSessionId, input) {
@@ -176,6 +178,7 @@ export class RuntimeSessionRegistry {
176
178
  status: "ok",
177
179
  sessions: [...this.sessions.values()]
178
180
  .filter((session) => session.controllerPiboSessionId === controllerPiboSessionId)
181
+ .map((session) => this.reconcileSession(session))
179
182
  .map(toRecord),
180
183
  };
181
184
  }
@@ -189,6 +192,7 @@ export class RuntimeSessionRegistry {
189
192
  }
190
193
  pruneIdle(now = Date.now(), idleTimeoutMs = 30 * 60 * 1000) {
191
194
  for (const session of this.sessions.values()) {
195
+ this.reconcileSession(session);
192
196
  if (session.status !== "idle")
193
197
  continue;
194
198
  if (now - Date.parse(session.updatedAt) > idleTimeoutMs) {
@@ -208,10 +212,12 @@ export class RuntimeSessionRegistry {
208
212
  };
209
213
  }
210
214
  getDefault(controllerPiboSessionId, runtime) {
211
- return [...this.sessions.values()].find((session) => session.controllerPiboSessionId === controllerPiboSessionId &&
212
- session.runtime === runtime &&
213
- session.status !== "closed" &&
214
- session.status !== "failed");
215
+ return [...this.sessions.values()].find((session) => {
216
+ if (session.controllerPiboSessionId !== controllerPiboSessionId || session.runtime !== runtime)
217
+ return false;
218
+ const status = this.reconcileSession(session).status;
219
+ return status !== "closed" && status !== "failed";
220
+ });
215
221
  }
216
222
  async getOrStartDefault(controllerPiboSessionId, input) {
217
223
  const runtime = input.runtime ?? "python";
@@ -230,6 +236,13 @@ export class RuntimeSessionRegistry {
230
236
  const session = this.sessions.get(sessionId);
231
237
  if (!session || session.controllerPiboSessionId !== controllerPiboSessionId)
232
238
  return undefined;
239
+ return this.reconcileSession(session);
240
+ }
241
+ reconcileSession(session) {
242
+ if (session.status !== "closed" && session.status !== "failed" && !session.backend.isAlive()) {
243
+ session.status = "failed";
244
+ session.updatedAt = nowIso();
245
+ }
233
246
  return session;
234
247
  }
235
248
  appendHistory(session, entry) {
@@ -148,7 +148,6 @@ export function updateUserSkill(id, input, cwd = process.cwd()) {
148
148
  writeFileSync(skillPath, markdown, "utf-8");
149
149
  }
150
150
  else if (name !== existing.name || description !== parsed.description) {
151
- const currentMarkdown = existsSync(existing.path) ? readFileSync(existing.path, "utf-8") : "";
152
151
  const { body } = parseSkillMd(currentMarkdown);
153
152
  writeFileSync(skillPath, buildSkillMd(name, description, body), "utf-8");
154
153
  }
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.0.2",
3
+ "version": "3.1.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.0.2",
9
+ "version": "3.1.1",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
13
13
  "dependencies": {
14
+ "@balena/dockerignore": "1.0.2",
14
15
  "@earendil-works/pi-agent-core": "0.84.2",
15
16
  "@earendil-works/pi-ai": "0.84.2",
16
17
  "@earendil-works/pi-coding-agent": "0.84.2",
@@ -968,6 +969,12 @@
968
969
  "node": ">=6.9.0"
969
970
  }
970
971
  },
972
+ "node_modules/@balena/dockerignore": {
973
+ "version": "1.0.2",
974
+ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
975
+ "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
976
+ "license": "Apache-2.0"
977
+ },
971
978
  "node_modules/@better-auth/core": {
972
979
  "version": "1.6.30",
973
980
  "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.30.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.0.2",
3
+ "version": "3.1.1",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"
@@ -14,7 +14,12 @@
14
14
  "docs/ops/**",
15
15
  "README.md",
16
16
  "src/mcp/LICENSE.mcp-cli",
17
- "npm-shrinkwrap.json"
17
+ "npm-shrinkwrap.json",
18
+ "compute-image/Dockerfile",
19
+ "compute-image/Dockerfile.dockerignore",
20
+ "scripts/docker-entrypoint.sh",
21
+ "scripts/prepare-browser-use-wrapper.sh",
22
+ "scripts/prepare-agent-browser-wrapper.sh"
18
23
  ],
19
24
  "bin": {
20
25
  "pibo": "dist/bin/pibo.js",
@@ -58,6 +63,7 @@
58
63
  "postpack": "node scripts/package-shrinkwrap.mjs clean"
59
64
  },
60
65
  "dependencies": {
66
+ "@balena/dockerignore": "1.0.2",
61
67
  "@earendil-works/pi-agent-core": "0.84.2",
62
68
  "@earendil-works/pi-ai": "0.84.2",
63
69
  "@earendil-works/pi-coding-agent": "0.84.2",
@@ -0,0 +1,46 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Xvfb starten (virtueller Display-Server für Browser-Automation)
5
+ if ! pgrep -x Xvfb >/dev/null 2>&1; then
6
+ echo "[docker-entrypoint] Starting Xvfb on DISPLAY=:99 ..."
7
+ Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp &
8
+ sleep 0.5
9
+ fi
10
+
11
+ export DISPLAY=:99
12
+
13
+ # Sicherstellen, dass die Browser-Wrapper existieren
14
+ if [ ! -x "$HOME/.pibo/tools/browser-use/home/bin/browser-use" ]; then
15
+ echo "[docker-entrypoint] Preparing browser-use wrapper ..."
16
+ /app/scripts/prepare-browser-use-wrapper.sh
17
+ fi
18
+
19
+ if [ ! -x "$HOME/.pibo/tools/agent-browser/home/bin/agent-browser" ]; then
20
+ echo "[docker-entrypoint] Preparing agent-browser wrapper ..."
21
+ /app/scripts/prepare-agent-browser-wrapper.sh
22
+ fi
23
+
24
+ # PATH erweitern
25
+ export PATH="$HOME/.pibo/tools/agent-browser/home/bin:$HOME/.pibo/tools/agent-browser/node/node_modules/.bin:$HOME/.pibo/tools/browser-use/home/bin:$HOME/.pibo/tools/browser-use/.venv/bin:$PATH"
26
+ export BROWSER_USE_HOME="$HOME/.pibo/tools/browser-use/home"
27
+ export AGENT_BROWSER_HOME="$HOME/.pibo/tools/agent-browser/home"
28
+
29
+ # Pibo-CLI-Argumente verarbeiten
30
+ case "${1:-gateway}" in
31
+ gateway)
32
+ echo "[docker-entrypoint] Starting Pibo gateway on 0.0.0.0:4789 ..."
33
+ exec node -e "import('./dist/gateway/server.js').then(m => m.runGatewayServer({ host: '0.0.0.0' }))"
34
+ ;;
35
+ gateway:web)
36
+ echo "[docker-entrypoint] Starting Pibo gateway:web with local auth on 0.0.0.0:4789 ..."
37
+ exec node -e "import('./dist/gateway/web.js').then(m => m.runWebGatewayServer({ authMode: 'local', web: { host: '0.0.0.0' } }))"
38
+ ;;
39
+ shell|bash|sh)
40
+ exec /bin/sh
41
+ ;;
42
+ *)
43
+ # Alles andere direkt an Pibo weiterleiten
44
+ exec node dist/bin/pibo.js "$@"
45
+ ;;
46
+ esac