@pasko70/pibo 3.2.3 → 3.2.5

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 (70) 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-CTsyojIh.js → dist-CTC-_3l0.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-BHio-Mzx.js → dist-CftLEFd5.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-DJ6Bhcuk.js → dist-Dq27Wt2q.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-CPKEoSPK.js → dist-Y4fHV5-Z.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-DNHFRMCk.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-Dc-6Xupt.css → index-DUGVw259.css} +1 -1
  24. package/dist/apps/chat-ui/index.html +2 -2
  25. package/dist/apps/chat-vscode-web/assets/{index-B1TuQNY5.js → index-B5X0NaRo.js} +4 -4
  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 +55 -7
  41. package/dist/data/schema.js +65 -23
  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/mcp-bridge.js +1 -1
  63. package/dist/tools/runtime/node-worker-source.js +39 -5
  64. package/dist/tools/runtime/python-backend.js +6 -2
  65. package/dist/tools/runtime/registry.js +38 -6
  66. package/dist/vscode/cli.js +1 -2
  67. package/dist/vscode/install.js +21 -14
  68. package/npm-shrinkwrap.json +2 -2
  69. package/package.json +3 -1
  70. package/dist/apps/chat-ui/assets/index-Y6jYzsaC.js +0 -228
@@ -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;
@@ -18,7 +34,7 @@ export class PayloadStore {
18
34
  const createdAt = input.createdAt ?? new Date().toISOString();
19
35
  const bytes = payloadToBytes(input.value, contentType);
20
36
  const sha256 = createHash("sha256").update(bytes).digest("hex");
21
- const existing = this.findBySha256(sha256);
37
+ const existing = this.findByIdentity(sha256, contentType, input.retentionClass);
22
38
  if (existing) {
23
39
  this.db.prepare("UPDATE payloads SET ref_count = ref_count + 1 WHERE id = ?").run(existing.id);
24
40
  return this.getPayload(existing.id) ?? existing;
@@ -27,7 +43,7 @@ export class PayloadStore {
27
43
  const encoding = shouldCompress ? "gzip" : "identity";
28
44
  const bytesToStore = shouldCompress ? gzipSync(bytes) : bytes;
29
45
  const compressedByteSize = shouldCompress ? bytesToStore.byteLength : null;
30
- const relativePath = buildRelativePayloadPath(sha256, contentType, encoding);
46
+ const relativePath = buildRelativePayloadPath(sha256, contentType, input.retentionClass, encoding);
31
47
  const absolutePath = this.rootDir === ":memory:" ? relativePath : join(this.rootDir, relativePath);
32
48
  writePayloadFile(absolutePath, bytesToStore);
33
49
  const id = input.id ?? `payload_${randomUUID()}`;
@@ -108,9 +124,35 @@ export class PayloadStore {
108
124
  return JSON.parse(Buffer.from(this.readPayloadBytesBounded(id, maxBytes)).toString("utf8"));
109
125
  }
110
126
  findBySha256(sha256) {
111
- const row = this.db.prepare("SELECT * FROM payloads WHERE sha256 = ?").get(sha256);
127
+ const row = this.db.prepare("SELECT * FROM payloads WHERE sha256 = ? ORDER BY created_at ASC, id ASC LIMIT 1").get(sha256);
128
+ return row ? payloadFromRow(row) : undefined;
129
+ }
130
+ findByIdentity(sha256, contentType, retentionClass) {
131
+ const row = this.db.prepare(`
132
+ SELECT * FROM payloads
133
+ WHERE sha256 = ? AND content_type = ? AND retention_class = ?
134
+ `).get(sha256, contentType, retentionClass);
112
135
  return row ? payloadFromRow(row) : undefined;
113
136
  }
137
+ releaseReferences(id, count = 1) {
138
+ if (!Number.isSafeInteger(count) || count < 1)
139
+ throw new RangeError("count must be a positive safe integer");
140
+ const row = this.db.prepare("SELECT * FROM payloads WHERE id = ?").get(id);
141
+ if (!row)
142
+ return undefined;
143
+ if (row.ref_count > count) {
144
+ this.db.prepare("UPDATE payloads SET ref_count = ref_count - ? WHERE id = ?").run(count, id);
145
+ return undefined;
146
+ }
147
+ this.db.prepare("DELETE FROM payloads WHERE id = ?").run(id);
148
+ return payloadFromRow(row);
149
+ }
150
+ removeReleasedFile(payload) {
151
+ if (payload.storageKind !== "file" || !payload.storagePath)
152
+ return;
153
+ const absolutePath = this.rootDir === ":memory:" ? payload.storagePath : join(this.rootDir, payload.storagePath);
154
+ rmSync(absolutePath, { force: true });
155
+ }
114
156
  }
115
157
  function readFileBounded(path, maxBytes) {
116
158
  const descriptor = openSync(path, "r");
@@ -147,12 +189,12 @@ function defaultContentType(value) {
147
189
  return "application/json";
148
190
  }
149
191
  function payloadToBytes(value, contentType) {
150
- if (typeof value === "string")
151
- return Buffer.from(value, "utf8");
152
192
  if (value instanceof Uint8Array)
153
193
  return value;
154
194
  if (contentType.includes("json"))
155
195
  return Buffer.from(JSON.stringify(value), "utf8");
196
+ if (typeof value === "string")
197
+ return Buffer.from(value, "utf8");
156
198
  return Buffer.from(String(value), "utf8");
157
199
  }
158
200
  function previewTextFromValue(value) {
@@ -162,10 +204,16 @@ function previewTextFromValue(value) {
162
204
  const normalized = text.replace(/\s+/g, " ").trim();
163
205
  return normalized ? normalized.slice(0, 1024) : undefined;
164
206
  }
165
- function buildRelativePayloadPath(sha256, contentType, encoding) {
207
+ function buildRelativePayloadPath(sha256, contentType, retentionClass, encoding) {
166
208
  const extension = extensionForContentType(contentType);
167
209
  const suffix = encoding === "gzip" ? `${extension}.gz` : extension;
168
- return join("sha256", sha256.slice(0, 2), sha256.slice(2, 4), `${sha256}.${suffix}`);
210
+ const metadataHash = createHash("sha256")
211
+ .update(contentType)
212
+ .update("\0")
213
+ .update(retentionClass)
214
+ .digest("hex")
215
+ .slice(0, 16);
216
+ return join("sha256", sha256.slice(0, 2), sha256.slice(2, 4), `${sha256}.${metadataHash}.${suffix}`);
169
217
  }
170
218
  function extensionForContentType(contentType) {
171
219
  if (contentType.includes("json"))
@@ -1,5 +1,27 @@
1
- export const PIBO_DATA_SCHEMA_VERSION = 7;
1
+ export const PIBO_DATA_SCHEMA_VERSION = 8;
2
+ const NATIVE_HISTORY_FALLBACK_SCHEMA_VERSION = 5;
2
3
  const retiredScopeColumn = ["owner", "scope"].join("_");
4
+ const payloadTableDefinition = `
5
+ id TEXT PRIMARY KEY,
6
+ sha256 TEXT NOT NULL,
7
+ storage_kind TEXT NOT NULL,
8
+ storage_path TEXT,
9
+ content_type TEXT NOT NULL,
10
+ encoding TEXT NOT NULL DEFAULT 'gzip',
11
+ byte_size INTEGER NOT NULL,
12
+ compressed_byte_size INTEGER,
13
+ preview_text TEXT,
14
+ retention_class TEXT NOT NULL,
15
+ ref_count INTEGER NOT NULL DEFAULT 0,
16
+ status TEXT NOT NULL DEFAULT 'committed',
17
+ created_at TEXT NOT NULL,
18
+ last_verified_at TEXT
19
+ `;
20
+ const payloadTableColumns = [
21
+ "id", "sha256", "storage_kind", "storage_path", "content_type", "encoding",
22
+ "byte_size", "compressed_byte_size", "preview_text", "retention_class", "ref_count",
23
+ "status", "created_at", "last_verified_at",
24
+ ];
3
25
  const retiredScopeTables = [
4
26
  {
5
27
  name: "sessions",
@@ -92,6 +114,26 @@ function rebuildRetiredScopeTables(db, tablesToRebuild) {
92
114
  `);
93
115
  }
94
116
  }
117
+ function payloadTableNeedsMetadataIdentityRebuild(db) {
118
+ const table = db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'payloads'").get();
119
+ if (!table)
120
+ return false;
121
+ const indexes = db.prepare("SELECT name FROM pragma_index_list('payloads') WHERE \"unique\" = 1").all();
122
+ return indexes.some((index) => {
123
+ const columns = db.prepare("SELECT name FROM pragma_index_info(?) ORDER BY seqno").all(index.name);
124
+ return columns.length === 1 && columns[0]?.name === "sha256";
125
+ });
126
+ }
127
+ function rebuildPayloadTableForMetadataIdentity(db) {
128
+ const columns = payloadTableColumns.join(", ");
129
+ db.exec(`
130
+ DROP TABLE IF EXISTS __pibo_schema_v8_payloads;
131
+ CREATE TABLE __pibo_schema_v8_payloads (${payloadTableDefinition});
132
+ INSERT INTO __pibo_schema_v8_payloads (${columns}) SELECT ${columns} FROM payloads;
133
+ DROP TABLE payloads;
134
+ ALTER TABLE __pibo_schema_v8_payloads RENAME TO payloads;
135
+ `);
136
+ }
95
137
  export function assertSupportedPiboDataSchemaVersion(db) {
96
138
  const version = Number(db.prepare("PRAGMA user_version").get()?.user_version ?? 0);
97
139
  if (version > PIBO_DATA_SCHEMA_VERSION) {
@@ -100,6 +142,7 @@ export function assertSupportedPiboDataSchemaVersion(db) {
100
142
  return version;
101
143
  }
102
144
  export const PIBO_DATA_SCHEMA_MIGRATION_STEPS = [
145
+ "payload-metadata-identity",
103
146
  "schema",
104
147
  "runtime-bindings",
105
148
  "render-high-water",
@@ -113,20 +156,22 @@ export const PIBO_DATA_SCHEMA_MIGRATION_STEPS = [
113
156
  export function applyPiboDataSchema(db, hooks = {}) {
114
157
  const previousVersion = assertSupportedPiboDataSchemaVersion(db);
115
158
  const tablesToRebuild = retiredScopeTablesToRebuild(db);
159
+ const rebuildPayloadTable = payloadTableNeedsMetadataIdentityRebuild(db);
160
+ const requiresTableRebuild = tablesToRebuild.length > 0 || rebuildPayloadTable;
116
161
  const ownsTransaction = !db.isTransaction;
117
- if (!ownsTransaction && tablesToRebuild.length > 0) {
162
+ if (!ownsTransaction && requiresTableRebuild) {
118
163
  throw new Error("Pibo data schema migration requires an independent transaction");
119
164
  }
120
165
  const foreignKeysEnabled = ownsTransaction
121
- && tablesToRebuild.length > 0
166
+ && requiresTableRebuild
122
167
  && Number(db.prepare("PRAGMA foreign_keys").get().foreign_keys ?? 0) === 1;
123
168
  if (foreignKeysEnabled)
124
169
  db.exec("PRAGMA foreign_keys = OFF");
125
170
  try {
126
171
  if (ownsTransaction)
127
172
  db.exec("BEGIN IMMEDIATE");
128
- applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild);
129
- if (tablesToRebuild.length > 0) {
173
+ applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild, rebuildPayloadTable);
174
+ if (requiresTableRebuild) {
130
175
  const violations = db.prepare("PRAGMA foreign_key_check").all();
131
176
  if (violations.length > 0) {
132
177
  throw new Error(`Pibo data schema migration would retain ${violations.length} foreign-key violation(s)`);
@@ -145,11 +190,14 @@ export function applyPiboDataSchema(db, hooks = {}) {
145
190
  db.exec("PRAGMA foreign_keys = ON");
146
191
  }
147
192
  }
148
- function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild) {
193
+ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRebuild, rebuildPayloadTable) {
149
194
  const existingSessionCount = db.prepare("SELECT COUNT(*) AS count FROM sqlite_schema WHERE type = 'table' AND name = 'sessions'").get();
150
195
  const hadSessionsBeforeMigration = existingSessionCount.count > 0
151
196
  && Number(db.prepare("SELECT COUNT(*) AS count FROM sessions").get().count) > 0;
152
197
  rebuildRetiredScopeTables(db, tablesToRebuild);
198
+ if (rebuildPayloadTable)
199
+ rebuildPayloadTableForMetadataIdentity(db);
200
+ hooks.afterStep?.("payload-metadata-identity");
153
201
  db.exec(`
154
202
  CREATE TABLE IF NOT EXISTS sessions (
155
203
  id TEXT PRIMARY KEY,
@@ -199,6 +247,13 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
199
247
  FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
200
248
  );
201
249
 
250
+ CREATE TABLE IF NOT EXISTS session_agent_observation_counters (
251
+ parent_pibo_session_id TEXT PRIMARY KEY,
252
+ next_sequence INTEGER NOT NULL CHECK(next_sequence >= 1),
253
+ updated_at TEXT NOT NULL,
254
+ FOREIGN KEY (parent_pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
255
+ );
256
+
202
257
  CREATE TABLE IF NOT EXISTS session_output_part_counters (
203
258
  pibo_session_id TEXT NOT NULL,
204
259
  event_id TEXT NOT NULL,
@@ -292,22 +347,9 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
292
347
  );
293
348
 
294
349
 
295
- CREATE TABLE IF NOT EXISTS payloads (
296
- id TEXT PRIMARY KEY,
297
- sha256 TEXT NOT NULL UNIQUE,
298
- storage_kind TEXT NOT NULL,
299
- storage_path TEXT,
300
- content_type TEXT NOT NULL,
301
- encoding TEXT NOT NULL DEFAULT 'gzip',
302
- byte_size INTEGER NOT NULL,
303
- compressed_byte_size INTEGER,
304
- preview_text TEXT,
305
- retention_class TEXT NOT NULL,
306
- ref_count INTEGER NOT NULL DEFAULT 0,
307
- status TEXT NOT NULL DEFAULT 'committed',
308
- created_at TEXT NOT NULL,
309
- last_verified_at TEXT
310
- );
350
+ CREATE TABLE IF NOT EXISTS payloads (${payloadTableDefinition});
351
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_payloads_identity
352
+ ON payloads(sha256, content_type, retention_class);
311
353
 
312
354
  CREATE TABLE IF NOT EXISTS event_log (
313
355
  stream_id INTEGER PRIMARY KEY,
@@ -784,7 +826,7 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
784
826
  hooks.afterStep?.("sequence-repair-backfill");
785
827
  db.exec("DROP TABLE pibo_v7_sequence_repair_sessions");
786
828
  hooks.afterStep?.("sequence-repair-cleanup");
787
- if (previousVersion < PIBO_DATA_SCHEMA_VERSION && hadSessionsBeforeMigration) {
829
+ if (previousVersion < NATIVE_HISTORY_FALLBACK_SCHEMA_VERSION && hadSessionsBeforeMigration) {
788
830
  const rows = db.prepare("SELECT pibo_session_id, metadata_json FROM session_runtime_bindings").all();
789
831
  const update = db.prepare("UPDATE session_runtime_bindings SET metadata_json = ? WHERE pibo_session_id = ?");
790
832
  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 }),