@pasko70/pibo 3.2.2 → 3.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/agent-runtime/auth.js +3 -5
  2. package/dist/agent-runtime/portable-history.js +1 -1
  3. package/dist/agent-runtimes/codex-native/client.js +2 -5
  4. package/dist/agent-runtimes/codex-native/redaction.js +3 -27
  5. package/dist/agent-runtimes/omp/client.js +2 -4
  6. package/dist/agent-runtimes/pi/adapter.js +1 -0
  7. package/dist/agent-runtimes/pi/runtime.js +1 -1
  8. package/dist/apps/chat/agent-store.js +1 -1
  9. package/dist/apps/chat/chat-files.js +3 -1
  10. package/dist/apps/chat/chat-request-normalizers.js +1 -1
  11. package/dist/apps/chat/chat-user-skill-routes.js +7 -0
  12. package/dist/apps/chat/data/project-service.js +49 -1
  13. package/dist/apps/chat/data/session-query-service.js +46 -6
  14. package/dist/apps/chat/loop-api.js +10 -1
  15. package/dist/apps/chat/ralph-api.js +10 -1
  16. package/dist/apps/chat/web-app.js +22 -5
  17. package/dist/apps/chat-ui/assets/{dist-f-76_fmW.js → dist-CTC-_3l0.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-DI39iyXK.js → dist-CftLEFd5.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-BL4oTUrk.js → dist-Dq27Wt2q.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-Drv_xdGh.js → dist-Y4fHV5-Z.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-CQF9jyQW.js → dist-dGF-PDGY.js} +1 -1
  22. package/dist/apps/chat-ui/assets/index-C_P0gXvJ.js +228 -0
  23. package/dist/apps/chat-ui/assets/index-DUGVw259.css +1 -0
  24. package/dist/apps/chat-ui/index.html +2 -2
  25. package/dist/apps/chat-vscode-web/assets/index-B5X0NaRo.js +43 -0
  26. package/dist/apps/chat-vscode-web/index.html +1 -1
  27. package/dist/apps/cli-ui/InkSessionApp.js +38 -28
  28. package/dist/auth/cli.js +4 -6
  29. package/dist/auth/machine-keys.js +20 -3
  30. package/dist/cli-session/localSessionSource.js +2 -4
  31. package/dist/compute/cli.js +16 -13
  32. package/dist/compute/docker.js +96 -48
  33. package/dist/core/base-prompt.js +27 -5
  34. package/dist/core/context-build.js +5 -15
  35. package/dist/core/sensitive-data-redaction.js +2 -2
  36. package/dist/core/session-router.js +15 -4
  37. package/dist/cron/cli.js +42 -10
  38. package/dist/cron/store.js +1 -1
  39. package/dist/data/cli.js +15 -7
  40. package/dist/data/payload-store.js +38 -0
  41. package/dist/data/schema.js +9 -1
  42. package/dist/debug/index.js +5 -0
  43. package/dist/debug/pty.js +45 -10
  44. package/dist/gateway/cli.js +18 -2
  45. package/dist/gateway/server.js +1 -0
  46. package/dist/loops/service.js +32 -16
  47. package/dist/mcp/runtime-session.js +2 -1
  48. package/dist/pi-packages/cli.js +5 -0
  49. package/dist/previews/cli.js +2 -6
  50. package/dist/previews/config.js +10 -0
  51. package/dist/previews/manager.js +2 -9
  52. package/dist/ralph/service.js +21 -10
  53. package/dist/session-ui/statusViewModel.js +2 -4
  54. package/dist/sessions/pibo-data-store.js +18 -0
  55. package/dist/sessions/store.js +9 -0
  56. package/dist/setup/cli.js +14 -7
  57. package/dist/setup/installation-profiles.js +20 -7
  58. package/dist/shared/trace-event-projection.js +32 -1
  59. package/dist/subagents/tool.js +3 -3
  60. package/dist/tools/browser-use-leases.js +11 -3
  61. package/dist/tools/index.js +0 -1
  62. package/dist/tools/runtime/node-worker-source.js +39 -5
  63. package/dist/tools/runtime/python-backend.js +6 -2
  64. package/dist/tools/runtime/registry.js +38 -6
  65. package/dist/vscode/cli.js +1 -2
  66. package/dist/vscode/install.js +21 -14
  67. package/npm-shrinkwrap.json +2 -2
  68. package/package.json +3 -1
  69. package/dist/apps/chat-ui/assets/index-BTzIdlcK.css +0 -1
  70. package/dist/apps/chat-ui/assets/index-CgFDCgyv.js +0 -228
  71. package/dist/apps/chat-vscode-web/assets/index-B-TiIWb7.js +0 -43
@@ -1196,7 +1196,7 @@ export class PiboSessionRouter {
1196
1196
  const status = current.getStatus();
1197
1197
  if (status.disposed || status.processing || status.streaming || status.queuedMessages > 0)
1198
1198
  return;
1199
- await this.resetCachedSession(piboSessionId, "routed runtime idle timeout");
1199
+ await this.resetCachedSession(piboSessionId);
1200
1200
  }
1201
1201
  async getOrCreateSession(piboSessionId) {
1202
1202
  if (this.closing)
@@ -1391,6 +1391,7 @@ export class PiboSessionRouter {
1391
1391
  ...(runtimeBindingPersistence ? { runtimeBindingPersistence } : {}),
1392
1392
  compatibility: {
1393
1393
  persistSession: this.options.persistSession,
1394
+ piPackageStoreCwd: this.options.piPackageStoreCwd,
1394
1395
  thinkingLevel: initialThinkingLevel ?? this.options.thinkingLevel,
1395
1396
  retryDefaults: resolvePiboSessionRetryDefaults(piboSession.kind, this.options.retryDefaults),
1396
1397
  extensionFactories: [
@@ -2015,6 +2016,8 @@ export class PiboSessionRouter {
2015
2016
  resolveSettled = resolve;
2016
2017
  });
2017
2018
  const untrack = this.trackActiveSubagent(parentPiboSessionId, {
2019
+ agentId: child.id,
2020
+ requestId,
2018
2021
  abortController: parentAbortController,
2019
2022
  settled,
2020
2023
  });
@@ -2098,8 +2101,14 @@ export class PiboSessionRouter {
2098
2101
  const child = this.requireManagedAgent(parentPiboSessionId, agentId);
2099
2102
  const ids = [agentId, ...this.descendantSessionIds(agentId)];
2100
2103
  const idSet = new Set(ids);
2104
+ const activeParentRunIds = new Set([...(this.activeSubagentRequests.get(parentPiboSessionId) ?? [])]
2105
+ .filter((request) => idSet.has(request.agentId))
2106
+ .map((request) => request.requestId));
2101
2107
  const cancellableRuns = this.runRegistry.listAll({ includeConsumed: true, includeDetached: true })
2102
- .filter((run) => idSet.has(run.controllerPiboSessionId) && !isTerminalRunStatus(run.status));
2108
+ .filter((run) => !isTerminalRunStatus(run.status) && (idSet.has(run.controllerPiboSessionId)
2109
+ || (run.controllerPiboSessionId === parentPiboSessionId && activeParentRunIds.has(run.runId))));
2110
+ const reason = `killed by parent ${parentPiboSessionId}`;
2111
+ await this.cancelRunsAfterSettlement(cancellableRuns.filter((run) => run.controllerPiboSessionId === parentPiboSessionId), reason);
2103
2112
  await Promise.allSettled(ids.flatMap((id) => this.sessions.has(id)
2104
2113
  ? [this.emit({ type: "execution", piboSessionId: id, action: "abort", id: randomUUID() })]
2105
2114
  : []));
@@ -2112,7 +2121,7 @@ export class PiboSessionRouter {
2112
2121
  },
2113
2122
  });
2114
2123
  }
2115
- await this.disposeSessionSubtree(agentId, `killed by parent ${parentPiboSessionId}`, { cancelRuns: true });
2124
+ await this.disposeSessionSubtree(agentId, reason, { cancelRuns: true });
2116
2125
  const cancelledRuns = cancellableRuns.flatMap((run) => {
2117
2126
  const current = this.runRegistry.status(run.controllerPiboSessionId, run.runId);
2118
2127
  return current.status === "cancelled" ? [run.runId] : [];
@@ -2368,9 +2377,11 @@ export class PiboSessionRouter {
2368
2377
  : eventId
2369
2378
  ? this.subagentRequestIdsByEvent.get(subagentRequestEventKey(session.id, eventId))
2370
2379
  : undefined;
2380
+ const sequence = this.sessionStore.claimAgentObservationSequence?.(session.parentId, this.nextAgentObservationSequence) ?? this.nextAgentObservationSequence;
2381
+ this.nextAgentObservationSequence = Math.max(this.nextAgentObservationSequence, sequence + 1);
2371
2382
  const observation = {
2372
2383
  managingParentId: session.parentId,
2373
- sequence: this.nextAgentObservationSequence++,
2384
+ sequence,
2374
2385
  createdAt: new Date().toISOString(),
2375
2386
  ...(requestId ? { requestId } : {}),
2376
2387
  agentId: session.id,
package/dist/cron/cli.js CHANGED
@@ -34,18 +34,44 @@ function maybeTargetFromOptions(options) {
34
34
  return undefined;
35
35
  return targetFromOptions(options);
36
36
  }
37
+ function scheduleSelectors(options, positionalCron) {
38
+ const candidates = [
39
+ ["--in", typeof options.in === "string"],
40
+ ["--at", typeof options.at === "string"],
41
+ ["--every", typeof options.every === "string"],
42
+ ["--daily", typeof options.daily === "string"],
43
+ ["--weekly", typeof options.weekly === "string"],
44
+ ["--monthly", typeof options.monthly === "string" || typeof options.monthly === "number"],
45
+ ["--cron", typeof options.cron === "string"],
46
+ ["positional cron expression", Boolean(positionalCron)],
47
+ ];
48
+ return candidates.filter(([, selected]) => selected).map(([selector]) => selector);
49
+ }
37
50
  function hasScheduleOptions(options, positionalCron) {
38
- return Boolean(positionalCron ||
39
- typeof options.in === "string" ||
40
- typeof options.at === "string" ||
41
- typeof options.every === "string" ||
42
- typeof options.daily === "string" ||
43
- typeof options.weekly === "string" ||
44
- typeof options.monthly === "string" ||
45
- typeof options.monthly === "number" ||
46
- typeof options.cron === "string");
51
+ return scheduleSelectors(options, positionalCron).length > 0;
52
+ }
53
+ function scheduleWithTimezone(job, tz) {
54
+ if (job.schedule.kind !== "cron")
55
+ throw new Error("--tz can only update an existing wall-clock cron schedule");
56
+ let scheduleUi;
57
+ if (job.scheduleUi) {
58
+ switch (job.scheduleUi.preset) {
59
+ case "daily":
60
+ case "weekly":
61
+ case "monthly":
62
+ case "advanced":
63
+ scheduleUi = { ...job.scheduleUi, tz };
64
+ break;
65
+ default:
66
+ scheduleUi = job.scheduleUi;
67
+ }
68
+ }
69
+ return { schedule: { ...job.schedule, tz }, scheduleUi };
47
70
  }
48
71
  function scheduleFromOptions(options, positionalCron) {
72
+ const selectors = scheduleSelectors(options, positionalCron);
73
+ if (selectors.length > 1)
74
+ throw new Error(`Conflicting schedule selectors: ${selectors.join(", ")}. Choose exactly one schedule source.`);
49
75
  if (typeof options.in === "string")
50
76
  return parseFriendlySchedule({ kind: "in", value: options.in });
51
77
  if (typeof options.at === "string")
@@ -169,7 +195,13 @@ export async function runCronCli(argv = process.argv) {
169
195
  .action((id, cronExprParts, options) => {
170
196
  const store = createDefaultPiboCronStore({ path: program.opts().store });
171
197
  const positionalCron = Array.isArray(cronExprParts) && cronExprParts.length ? cronExprParts.join(" ") : undefined;
172
- const normalized = hasScheduleOptions(options, positionalCron) ? scheduleFromOptions(options, positionalCron) : undefined;
198
+ let normalized = hasScheduleOptions(options, positionalCron) ? scheduleFromOptions(options, positionalCron) : undefined;
199
+ if (!normalized && typeof options.tz === "string") {
200
+ const existing = store.getJob(id);
201
+ if (!existing)
202
+ throw new Error("Cron job not found");
203
+ normalized = scheduleWithTimezone(existing, options.tz);
204
+ }
173
205
  const target = maybeTargetFromOptions(options);
174
206
  const patch = {
175
207
  ...(options.name !== undefined ? { name: options.name } : {}),
@@ -144,7 +144,7 @@ export class PiboCronStore {
144
144
  runningAt: existing.state.runningAt,
145
145
  nextRunAt: next.enabled ? computeNextRunAt(next.schedule, now)?.toISOString() : undefined,
146
146
  };
147
- if (next.enabled && !next.state.nextRunAt && next.schedule.kind !== "at")
147
+ if (next.enabled && !next.state.nextRunAt)
148
148
  throw new Error("schedule has no future run");
149
149
  this.writeJob(next);
150
150
  return this.getJob(id);
package/dist/data/cli.js CHANGED
@@ -86,15 +86,19 @@ function migrateSessionsToV2(input) {
86
86
  if (!report.inputExists)
87
87
  return report;
88
88
  const source = new DatabaseSync(input.from, { readOnly: true });
89
- const target = new PiboDataStore(input.to);
89
+ let target;
90
90
  try {
91
- if (!hasTable(source, "pibo_sessions"))
92
- return report;
91
+ if (!hasTable(source, "pibo_sessions")) {
92
+ throw new Error(`Cannot migrate sessions from "${input.from}": required legacy table "pibo_sessions" is missing.`);
93
+ }
94
+ target = new PiboDataStore(input.to);
93
95
  const rows = source.prepare("SELECT * FROM pibo_sessions ORDER BY created_at ASC").all();
94
96
  report.read = rows.length;
95
97
  for (const row of rows) {
96
98
  const existing = target.db.prepare("SELECT updated_at FROM sessions WHERE id = ?").get(row.id);
97
99
  const metadata = parseJsonObject(row.metadata_json);
100
+ let migrateBinding = false;
101
+ const roomId = typeof metadata.chatRoomId === "string" ? metadata.chatRoomId : null;
98
102
  const rootSessionId = row.parent_id ? (typeof metadata.rootSessionId === "string" ? metadata.rootSessionId : row.parent_id) : row.id;
99
103
  if (!existing) {
100
104
  const columns = [
@@ -105,22 +109,26 @@ function migrateSessionsToV2(input) {
105
109
  target.db.prepare(`
106
110
  INSERT INTO sessions (${columns.join(", ")})
107
111
  VALUES (${columns.map(() => "?").join(", ")})
108
- `).run(row.id, row.pi_session_id, typeof metadata.chatRoomId === "string" ? metadata.chatRoomId : null, rootSessionId, row.parent_id, row.origin_id, row.channel, row.kind, row.profile, row.active_model_json, row.workspace, row.title ?? "Untitled Session", "idle", JSON.stringify(metadata), row.created_at, row.updated_at, row.updated_at);
112
+ `).run(row.id, row.pi_session_id, roomId, rootSessionId, row.parent_id, row.origin_id, row.channel, row.kind, row.profile, row.active_model_json, row.workspace, row.title ?? "Untitled Session", "idle", JSON.stringify(metadata), row.created_at, row.updated_at, row.updated_at);
109
113
  report.inserted++;
114
+ migrateBinding = true;
110
115
  }
111
116
  else if (row.updated_at > existing.updated_at) {
112
117
  target.db.prepare(`
113
118
  UPDATE sessions SET
114
- pi_session_id = ?, root_session_id = ?, parent_id = ?, origin_id = ?,
119
+ pi_session_id = ?, room_id = ?, root_session_id = ?, parent_id = ?, origin_id = ?,
115
120
  channel = ?, kind = ?, profile = ?, active_model_json = ?, workspace = ?, title = ?,
116
121
  metadata_json = ?, updated_at = ?, last_activity_at = MAX(last_activity_at, ?)
117
122
  WHERE id = ?
118
- `).run(row.pi_session_id, rootSessionId, row.parent_id, row.origin_id, row.channel, row.kind, row.profile, row.active_model_json, row.workspace, row.title ?? "Untitled Session", JSON.stringify(metadata), row.updated_at, row.updated_at, row.id);
123
+ `).run(row.pi_session_id, roomId, rootSessionId, row.parent_id, row.origin_id, row.channel, row.kind, row.profile, row.active_model_json, row.workspace, row.title ?? "Untitled Session", JSON.stringify(metadata), row.updated_at, row.updated_at, row.id);
119
124
  report.updated++;
125
+ migrateBinding = true;
120
126
  }
121
127
  else {
122
128
  report.skipped++;
123
129
  }
130
+ if (!migrateBinding)
131
+ continue;
124
132
  target.db.prepare(`
125
133
  INSERT INTO session_runtime_bindings (
126
134
  pibo_session_id, runtime_instance_id, runtime_adapter_id, native_session_id,
@@ -142,7 +150,7 @@ function migrateSessionsToV2(input) {
142
150
  }
143
151
  finally {
144
152
  source.close();
145
- target.close();
153
+ target?.close();
146
154
  }
147
155
  return report;
148
156
  }
@@ -4,6 +4,22 @@ import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, re
4
4
  import { dirname, extname, join, resolve } from "node:path";
5
5
  import { piboHomePath } from "../core/pibo-home.js";
6
6
  const MAX_SYNC_PAYLOAD_GZIP_BYTES = 512 * 1024;
7
+ export class PiboPayloadMetadataConflictError extends Error {
8
+ sha256;
9
+ existingContentType;
10
+ requestedContentType;
11
+ existingRetentionClass;
12
+ requestedRetentionClass;
13
+ constructor(sha256, existingContentType, requestedContentType, existingRetentionClass, requestedRetentionClass) {
14
+ super(`Payload bytes already exist with incompatible metadata: content type ${existingContentType} vs ${requestedContentType}; retention class ${existingRetentionClass} vs ${requestedRetentionClass}`);
15
+ this.sha256 = sha256;
16
+ this.existingContentType = existingContentType;
17
+ this.requestedContentType = requestedContentType;
18
+ this.existingRetentionClass = existingRetentionClass;
19
+ this.requestedRetentionClass = requestedRetentionClass;
20
+ this.name = "PiboPayloadMetadataConflictError";
21
+ }
22
+ }
7
23
  export class PayloadStore {
8
24
  db;
9
25
  rootDir;
@@ -20,6 +36,9 @@ export class PayloadStore {
20
36
  const sha256 = createHash("sha256").update(bytes).digest("hex");
21
37
  const existing = this.findBySha256(sha256);
22
38
  if (existing) {
39
+ if (existing.contentType !== contentType || existing.retentionClass !== input.retentionClass) {
40
+ throw new PiboPayloadMetadataConflictError(sha256, existing.contentType, contentType, existing.retentionClass, input.retentionClass);
41
+ }
23
42
  this.db.prepare("UPDATE payloads SET ref_count = ref_count + 1 WHERE id = ?").run(existing.id);
24
43
  return this.getPayload(existing.id) ?? existing;
25
44
  }
@@ -111,6 +130,25 @@ export class PayloadStore {
111
130
  const row = this.db.prepare("SELECT * FROM payloads WHERE sha256 = ?").get(sha256);
112
131
  return row ? payloadFromRow(row) : undefined;
113
132
  }
133
+ releaseReferences(id, count = 1) {
134
+ if (!Number.isSafeInteger(count) || count < 1)
135
+ throw new RangeError("count must be a positive safe integer");
136
+ const row = this.db.prepare("SELECT * FROM payloads WHERE id = ?").get(id);
137
+ if (!row)
138
+ return undefined;
139
+ if (row.ref_count > count) {
140
+ this.db.prepare("UPDATE payloads SET ref_count = ref_count - ? WHERE id = ?").run(count, id);
141
+ return undefined;
142
+ }
143
+ this.db.prepare("DELETE FROM payloads WHERE id = ?").run(id);
144
+ return payloadFromRow(row);
145
+ }
146
+ removeReleasedFile(payload) {
147
+ if (payload.storageKind !== "file" || !payload.storagePath)
148
+ return;
149
+ const absolutePath = this.rootDir === ":memory:" ? payload.storagePath : join(this.rootDir, payload.storagePath);
150
+ rmSync(absolutePath, { force: true });
151
+ }
114
152
  }
115
153
  function readFileBounded(path, maxBytes) {
116
154
  const descriptor = openSync(path, "r");
@@ -1,4 +1,5 @@
1
1
  export const PIBO_DATA_SCHEMA_VERSION = 7;
2
+ const NATIVE_HISTORY_FALLBACK_SCHEMA_VERSION = 5;
2
3
  const retiredScopeColumn = ["owner", "scope"].join("_");
3
4
  const retiredScopeTables = [
4
5
  {
@@ -199,6 +200,13 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
199
200
  FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
200
201
  );
201
202
 
203
+ CREATE TABLE IF NOT EXISTS session_agent_observation_counters (
204
+ parent_pibo_session_id TEXT PRIMARY KEY,
205
+ next_sequence INTEGER NOT NULL CHECK(next_sequence >= 1),
206
+ updated_at TEXT NOT NULL,
207
+ FOREIGN KEY (parent_pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
208
+ );
209
+
202
210
  CREATE TABLE IF NOT EXISTS session_output_part_counters (
203
211
  pibo_session_id TEXT NOT NULL,
204
212
  event_id TEXT NOT NULL,
@@ -784,7 +792,7 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
784
792
  hooks.afterStep?.("sequence-repair-backfill");
785
793
  db.exec("DROP TABLE pibo_v7_sequence_repair_sessions");
786
794
  hooks.afterStep?.("sequence-repair-cleanup");
787
- if (previousVersion < PIBO_DATA_SCHEMA_VERSION && hadSessionsBeforeMigration) {
795
+ if (previousVersion < NATIVE_HISTORY_FALLBACK_SCHEMA_VERSION && hadSessionsBeforeMigration) {
788
796
  const rows = db.prepare("SELECT pibo_session_id, metadata_json FROM session_runtime_bindings").all();
789
797
  const update = db.prepare("UPDATE session_runtime_bindings SET metadata_json = ? WHERE pibo_session_id = ?");
790
798
  for (const row of rows) {
@@ -322,6 +322,11 @@ async function runDebugDb(args) {
322
322
  const command = args[0];
323
323
  const options = parseOptions(args.slice(1));
324
324
  if (command === "stores") {
325
+ const unknownOption = options.positionals.find((value) => value.startsWith("-"));
326
+ if (unknownOption)
327
+ throw new Error(`Unknown option "${unknownOption}" for pibo debug db stores. Run pibo debug db --help.`);
328
+ if (options.positionals.length > 0)
329
+ throw new Error("pibo debug db stores accepts no positional arguments. Run pibo debug db --help.");
325
330
  const stores = resolveDebugStores();
326
331
  if (options.json) {
327
332
  console.log(JSON.stringify({ stores }, null, 2));
package/dist/debug/pty.js CHANGED
@@ -96,6 +96,26 @@ if exit_code is None:
96
96
  exit_code = proc.wait(timeout=1)
97
97
  sys.exit(exit_code if exit_code is not None else 1)
98
98
  `;
99
+ const PTY_SCENARIO_FIELDS = new Set([
100
+ "name",
101
+ "command",
102
+ "cwd",
103
+ "workdir",
104
+ "env",
105
+ "rows",
106
+ "cols",
107
+ "timeoutMs",
108
+ "idleTimeoutMs",
109
+ "inputDelayMs",
110
+ "providerMode",
111
+ "maxIterations",
112
+ "artifactDir",
113
+ "artifact",
114
+ "steps",
115
+ "expect",
116
+ "reject",
117
+ "stopPatterns",
118
+ ]);
99
119
  class WallClockTimeoutError extends Error {
100
120
  constructor(timeoutMs) {
101
121
  super(`PTY command timed out after ${timeoutMs}ms`);
@@ -234,17 +254,17 @@ function parsePtyOptions(args) {
234
254
  else if (arg === "--env")
235
255
  options.env.push(requireOptionValue(args, ++index, arg));
236
256
  else if (arg === "--expect")
237
- options.expect.push(requireOptionValue(args, ++index, arg));
257
+ options.expect.push(requireTextOptionValue(args, ++index, arg));
238
258
  else if (arg === "--reject")
239
- options.reject.push(requireOptionValue(args, ++index, arg));
259
+ options.reject.push(requireTextOptionValue(args, ++index, arg));
240
260
  else if (arg === "--stop-pattern")
241
- options.stopPatterns.push(requireOptionValue(args, ++index, arg));
261
+ options.stopPatterns.push(requireTextOptionValue(args, ++index, arg));
242
262
  else if (arg === "--type")
243
- options.steps.push({ typeText: requireOptionValue(args, ++index, arg) });
263
+ options.steps.push({ typeText: requireTextOptionValue(args, ++index, arg) });
244
264
  else if (arg === "--press")
245
265
  options.steps.push({ press: requireOptionValue(args, ++index, arg) });
246
266
  else if (arg === "--wait-for")
247
- options.steps.push({ waitFor: requireOptionValue(args, ++index, arg) });
267
+ options.steps.push({ waitFor: requireTextOptionValue(args, ++index, arg) });
248
268
  else if (arg === "--sleep-ms")
249
269
  options.steps.push({ sleepMs: parseNonNegativeInteger(requireOptionValue(args, ++index, arg), arg) });
250
270
  else if (arg === "--builtin")
@@ -269,6 +289,12 @@ function requireOptionValue(args, index, option) {
269
289
  throw new Error(`${option} requires a value`);
270
290
  return value;
271
291
  }
292
+ function requireTextOptionValue(args, index, option) {
293
+ const value = args[index];
294
+ if (value === undefined)
295
+ throw new Error(`${option} requires a value`);
296
+ return value;
297
+ }
272
298
  function scenarioFromRunOptions(options) {
273
299
  return {
274
300
  name: options.name ?? "adhoc-run",
@@ -363,6 +389,10 @@ function validateScenario(value, source) {
363
389
  if (!value || typeof value !== "object" || Array.isArray(value))
364
390
  throw new Error(`${source}: scenario must be an object`);
365
391
  const raw = value;
392
+ for (const field of Object.keys(raw)) {
393
+ if (!PTY_SCENARIO_FIELDS.has(field))
394
+ throw new Error(`${source}: unknown field "${field}"`);
395
+ }
366
396
  if (!Array.isArray(raw.command) || raw.command.some((item) => typeof item !== "string") || raw.command.length === 0) {
367
397
  throw new Error(`${source}: command must be a non-empty string array`);
368
398
  }
@@ -425,6 +455,7 @@ async function executePtyScenario(input, options) {
425
455
  let stopReason = "completed";
426
456
  let exitCode = null;
427
457
  let signal = null;
458
+ const waitCursor = { cleanOffset: 0 };
428
459
  try {
429
460
  validateProviderSafety(scenario, options);
430
461
  runner = await startRunner(scenario);
@@ -449,7 +480,7 @@ async function executePtyScenario(input, options) {
449
480
  }
450
481
  }
451
482
  await checkIdle(runner, refreshOutputTimestamp, scenario.idleTimeoutMs);
452
- await runStep(runner, scenario, step, assertions, refreshOutputTimestamp, deadline);
483
+ await runStep(runner, scenario, step, assertions, refreshOutputTimestamp, deadline, waitCursor);
453
484
  throwIfDeadlineExpired(deadline);
454
485
  refreshOutputTimestamp();
455
486
  const clean = cleanTerminalText(runner.getRawOutput());
@@ -667,9 +698,9 @@ function wrapChild(child, backend, ptyMethod) {
667
698
  getEvents: () => events,
668
699
  };
669
700
  }
670
- async function runStep(runner, scenario, step, assertions, markOutput, deadline) {
701
+ async function runStep(runner, scenario, step, assertions, markOutput, deadline, waitCursor) {
671
702
  if (step.waitFor !== undefined) {
672
- await waitForText(runner, step.waitFor, step.timeoutMs ?? scenario.timeoutMs, scenario.idleTimeoutMs, markOutput, deadline);
703
+ await waitForText(runner, step.waitFor, step.timeoutMs ?? scenario.timeoutMs, scenario.idleTimeoutMs, markOutput, deadline, waitCursor);
673
704
  return;
674
705
  }
675
706
  if (step.typeText !== undefined) {
@@ -706,13 +737,17 @@ async function runStep(runner, scenario, step, assertions, markOutput, deadline)
706
737
  if (step.reject !== undefined)
707
738
  assertPatterns(clean, [], [step.reject], assertions);
708
739
  }
709
- async function waitForText(runner, pattern, timeoutMs, idleTimeoutMs, markOutput, globalDeadline) {
740
+ async function waitForText(runner, pattern, timeoutMs, idleTimeoutMs, markOutput, globalDeadline, cursor) {
710
741
  const stepDeadline = performance.now() + timeoutMs;
711
742
  while (true) {
712
743
  throwIfDeadlineExpired(globalDeadline);
713
744
  const lastOutputAt = markOutput();
714
- if (cleanTerminalText(runner.getRawOutput()).includes(pattern))
745
+ const clean = cleanTerminalText(runner.getRawOutput());
746
+ const matchIndex = clean.indexOf(pattern, Math.min(cursor.cleanOffset, clean.length));
747
+ if (matchIndex >= 0) {
748
+ cursor.cleanOffset = matchIndex + pattern.length;
715
749
  return;
750
+ }
716
751
  const now = performance.now();
717
752
  if (now - lastOutputAt > idleTimeoutMs) {
718
753
  await runner.terminate("idle_timeout");
@@ -52,7 +52,19 @@ function expectedMode(target) {
52
52
  }
53
53
  function gatewayServiceName(target) {
54
54
  const fromEnv = target === "web" ? process.env.PIBO_GATEWAY_WEB_SERVICE : process.env.PIBO_GATEWAY_DEV_SERVICE;
55
- return fromEnv || (target === "web" ? "pibo-web" : "pibo-web-dev");
55
+ if (fromEnv)
56
+ return fromEnv;
57
+ if (target === "web") {
58
+ try {
59
+ const persisted = readFileSync(join(managedGatewayHome(target), "gateway-web-service"), "utf8").trim();
60
+ if (/^[A-Za-z0-9_.@-]+$/.test(persisted))
61
+ return persisted;
62
+ }
63
+ catch {
64
+ // Fall back to the default service name when setup has not persisted an override.
65
+ }
66
+ }
67
+ return target === "web" ? "pibo-web" : "pibo-web-dev";
56
68
  }
57
69
  function gatewayManagerCommand() {
58
70
  return process.env.PIBO_GATEWAY_MANAGER_COMMAND || "systemctl";
@@ -62,7 +74,11 @@ function shouldUseWindowsGatewayManager() {
62
74
  }
63
75
  function managedGatewayHome(target) {
64
76
  const fromEnv = target === "web" ? process.env.PIBO_GATEWAY_WEB_HOME : process.env.PIBO_GATEWAY_DEV_HOME;
65
- return fromEnv || join(homedir(), target === "web" ? ".pibo" : ".pibo-dev");
77
+ if (fromEnv)
78
+ return fromEnv;
79
+ if (target === "web" && process.env.PIBO_HOME)
80
+ return process.env.PIBO_HOME;
81
+ return join(homedir(), target === "web" ? ".pibo" : ".pibo-dev");
66
82
  }
67
83
  function targetGatewayPort(target) {
68
84
  const fromEnv = target === "web" ? process.env.PIBO_GATEWAY_WEB_AGENT_PORT : process.env.PIBO_GATEWAY_DEV_AGENT_PORT;
@@ -159,6 +159,7 @@ export class PiboGatewayServer {
159
159
  && (this.options.persistSession !== false || hasExplicitPersistentStore);
160
160
  this.router = new PiboSessionRouter({
161
161
  persistSession: this.options.persistSession,
162
+ piPackageStoreCwd: this.options.piPackageStoreCwd ?? process.cwd(),
162
163
  pluginRegistry: this.pluginRegistry,
163
164
  sessionStore: this.sessionStore,
164
165
  messagePreflight: createLoopMessagePreflight({ path: this.options.loopStorePath }),
@@ -20,6 +20,9 @@ class LoopRunTimeoutError extends Error {
20
20
  this.name = 'LoopRunTimeoutError';
21
21
  }
22
22
  }
23
+ export class PiboLoopCapacityError extends Error {
24
+ constructor() { super('Loop run capacity is currently full'); this.name = 'PiboLoopCapacityError'; }
25
+ }
23
26
  class LoopContinuationInvalidatedError extends Error {
24
27
  constructor(message) { super(message); this.name = 'LoopContinuationInvalidatedError'; }
25
28
  }
@@ -84,6 +87,7 @@ export class PiboLoopService {
84
87
  random;
85
88
  timer;
86
89
  activeRuns = 0;
90
+ admissionTail = Promise.resolve();
87
91
  activeRunWork = new Set();
88
92
  pendingRunStarts = 0;
89
93
  schedulerWork;
@@ -143,14 +147,21 @@ export class PiboLoopService {
143
147
  return undefined;
144
148
  this.pendingRunStarts += 1;
145
149
  try {
146
- const job = this.store.updateJob(id, { enabled: true });
147
- if (!job)
148
- return undefined;
149
- const reserved = await this.reserveAfterBeforeRunEvaluation(job);
150
- if (!reserved)
151
- return undefined;
152
- this.launchReservedRun(reserved.job, reserved.run);
153
- return reserved.run;
150
+ return await this.withAdmissionLock(async () => {
151
+ const existing = this.store.getJob(id);
152
+ if (!existing || existing.state.runningAt)
153
+ return undefined;
154
+ if (this.activeRuns >= this.maxConcurrentRuns)
155
+ throw new PiboLoopCapacityError();
156
+ const job = this.store.updateJob(id, { enabled: true });
157
+ if (!job)
158
+ return undefined;
159
+ const reserved = await this.reserveAfterBeforeRunEvaluation(job);
160
+ if (!reserved)
161
+ return undefined;
162
+ this.launchReservedRun(reserved.job, reserved.run);
163
+ return reserved.run;
164
+ });
154
165
  }
155
166
  finally {
156
167
  this.pendingRunStarts -= 1;
@@ -303,14 +314,13 @@ export class PiboLoopService {
303
314
  async tick() { if (this.stopped)
304
315
  return; try {
305
316
  await this.abortCancelRequestedJobs();
306
- const capacity = this.maxConcurrentRuns - this.activeRuns;
307
- if (capacity > 0) {
308
- const jobs = this.store.listJobs({ includeDisabled: false }).filter((job) => !job.state.runningAt).slice(0, capacity);
309
- for (const job of jobs) {
310
- const reserved = await this.reserveAfterBeforeRunEvaluation(job);
311
- if (reserved)
312
- this.launchReservedRun(reserved.job, reserved.run);
313
- }
317
+ const jobs = this.store.listJobs({ includeDisabled: false }).filter((job) => !job.state.runningAt);
318
+ for (const job of jobs) {
319
+ const admitted = await this.withAdmissionLock(async () => { if (this.activeRuns >= this.maxConcurrentRuns)
320
+ return false; const reserved = await this.reserveAfterBeforeRunEvaluation(job); if (!reserved)
321
+ return true; this.launchReservedRun(reserved.job, reserved.run); return true; });
322
+ if (!admitted)
323
+ break;
314
324
  }
315
325
  }
316
326
  catch (error) {
@@ -319,6 +329,12 @@ export class PiboLoopService {
319
329
  finally {
320
330
  this.arm();
321
331
  } }
332
+ async withAdmissionLock(action) { const previous = this.admissionTail; let release; this.admissionTail = new Promise((resolve) => { release = resolve; }); await previous; try {
333
+ return await action();
334
+ }
335
+ finally {
336
+ release();
337
+ } }
322
338
  async reserveAfterBeforeRunEvaluation(job) {
323
339
  const fresh = this.store.getJob(job.id) ?? job;
324
340
  const now = this.now();
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { redactSensitiveText } from "../core/sensitive-data-redaction.js";
3
4
  import { getDefaultEnvironment, StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
5
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
6
  import { filterTools, isHttpServer, } from "./config.js";
@@ -94,7 +95,7 @@ export function redactMcpRuntimeError(error, secretValues = []) {
94
95
  if (value)
95
96
  message = message.split(value).join("[REDACTED]");
96
97
  }
97
- return message.replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]");
98
+ return redactSensitiveText(message).replaceAll("[redacted]", "[REDACTED]");
98
99
  }
99
100
  export async function verifyPiboMcpServer(serverName, config, options) {
100
101
  const client = new Client({ name: "pibo-runtime-resource-verifier", version: "1.0.0" }, { capabilities: {} });
@@ -51,6 +51,7 @@ export async function runPiPackagesCli(argv = process.argv) {
51
51
  console.log("No Pi packages registered.");
52
52
  return;
53
53
  }
54
+ let hasErrors = false;
54
55
  for (const pkg of packages) {
55
56
  const diagnostics = [];
56
57
  try {
@@ -68,11 +69,15 @@ export async function runPiPackagesCli(argv = process.argv) {
68
69
  }
69
70
  }
70
71
  const status = diagnostics.length || pkg.installStatus === "error" || pkg.installStatus === "missing" ? "error" : "ok";
72
+ if (status === "error")
73
+ hasErrors = true;
71
74
  console.log(`${pkg.name}\t${status}\t${pkg.installSpec}`);
72
75
  for (const diagnostic of [...pkg.diagnostics.map((item) => item.message), ...diagnostics]) {
73
76
  console.log(` ${diagnostic}`);
74
77
  }
75
78
  }
79
+ if (hasErrors)
80
+ process.exitCode = 1;
76
81
  });
77
82
  await program.parseAsync(argv);
78
83
  }
@@ -1,9 +1,8 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { resolve } from "node:path";
3
3
  import { Command } from "commander";
4
- import { sanitizePreviewServerSettings } from "../core/preview-server-settings.js";
5
4
  import { createDefaultPiboDataSessionStore } from "../sessions/pibo-data-store.js";
6
- import { DEFAULT_PREVIEW_TTL_MINUTES, loadPreviewConfig, previewPublicURL, requirePreviewBaseURL } from "./config.js";
5
+ import { DEFAULT_PREVIEW_TTL_MINUTES, loadEffectivePreviewServerSettings, loadPreviewConfig, previewPublicURL, requirePreviewBaseURL } from "./config.js";
7
6
  import { reconcileManagedPreviews, startManagedPreview, stopManagedPreview, validatePreviewStartCommand, } from "./manager.js";
8
7
  import { findPreviewTargetProcess, isPiboControlProcess, isPiboYieldedProcess, isPreviewTargetProcessCurrent, probePreviewTarget, validatePreviewPort, } from "./network.js";
9
8
  import { createDefaultPreviewStore, previewExposureState } from "./store.js";
@@ -264,10 +263,7 @@ export async function runPreviewCli(argv = process.argv) {
264
263
  const baseURL = requirePreviewBaseURL();
265
264
  if (!id) {
266
265
  const config = loadPreviewConfig();
267
- const serverLimits = sanitizePreviewServerSettings({
268
- maxRunningServers: config.maxRunningServers,
269
- autoStopMinutes: config.autoStopMinutes,
270
- });
266
+ const serverLimits = loadEffectivePreviewServerSettings();
271
267
  const store = createDefaultPreviewStore();
272
268
  let diagnostics;
273
269
  try {
@@ -1,4 +1,6 @@
1
1
  import { loadPiboConfig } from "../config/config.js";
2
+ import { sanitizePreviewServerSettings } from "../core/preview-server-settings.js";
3
+ import { loadPiboUserSettings } from "../core/user-settings.js";
2
4
  import { parsePreviewBaseURL } from "./base-url.js";
3
5
  export const DEFAULT_PREVIEW_TTL_MINUTES = 8 * 60;
4
6
  export const DEFAULT_PREVIEW_TICKET_TTL_SECONDS = 60;
@@ -6,6 +8,14 @@ export const DEFAULT_PREVIEW_SESSION_TTL_MINUTES = 8 * 60;
6
8
  export function loadPreviewConfig() {
7
9
  return loadPiboConfig().preview ?? {};
8
10
  }
11
+ export function loadEffectivePreviewServerSettings() {
12
+ const config = loadPreviewConfig();
13
+ const userSettings = loadPiboUserSettings().previewServers;
14
+ return sanitizePreviewServerSettings({
15
+ maxRunningServers: config.maxRunningServers ?? userSettings.maxRunningServers,
16
+ autoStopMinutes: config.autoStopMinutes ?? userSettings.autoStopMinutes,
17
+ });
18
+ }
9
19
  export function requirePreviewBaseURL(value = loadPreviewConfig().baseURL) {
10
20
  if (!value)
11
21
  throw new Error("preview.baseURL is required. Set it with `pibo config set preview.baseURL https://preview.example.com`.");