@pasko70/pibo 3.4.4 → 3.5.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 (91) hide show
  1. package/dist/agent-runtime/routed-session.js +111 -25
  2. package/dist/agent-runtimes/codex-native/turn.js +27 -3
  3. package/dist/agent-runtimes/pi/adapter.js +1 -0
  4. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  5. package/dist/apps/chat/bounded-event-stream.js +98 -0
  6. package/dist/apps/chat/chat-settings-routes.js +3 -3
  7. package/dist/apps/chat/data/chat-data-mappers.js +40 -14
  8. package/dist/apps/chat/data/event-command-service.js +30 -21
  9. package/dist/apps/chat/data/history-query-service.js +34 -25
  10. package/dist/apps/chat/data/read-state-service.js +37 -3
  11. package/dist/apps/chat/data/session-query-service.js +19 -11
  12. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  13. package/dist/apps/chat/message-command-dispatcher.js +149 -0
  14. package/dist/apps/chat/output-compactor.js +9 -0
  15. package/dist/apps/chat/output-event-policy.js +9 -1
  16. package/dist/apps/chat/stream.js +21 -3
  17. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  18. package/dist/apps/chat/trace-response-cache.js +46 -0
  19. package/dist/apps/chat/trace-v2.js +12 -6
  20. package/dist/apps/chat/trace.js +1 -1
  21. package/dist/apps/chat/web-app.js +645 -297
  22. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
  27. package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
  28. package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
  29. package/dist/apps/chat-ui/index.html +2 -2
  30. package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
  31. package/dist/apps/chat-vscode-web/index.html +1 -1
  32. package/dist/cli-session/localSessionSource.js +3 -2
  33. package/dist/compute/pool/seeds.js +25 -3
  34. package/dist/core/events.js +4 -0
  35. package/dist/core/output-render-sequence.js +65 -6
  36. package/dist/core/provider-capacity.js +33 -0
  37. package/dist/core/provider-telemetry.js +21 -6
  38. package/dist/core/runtime-capacity.js +174 -0
  39. package/dist/core/runtime-telemetry.js +40 -12
  40. package/dist/core/session-router.js +178 -13
  41. package/dist/data/async-chat-reads.js +38 -0
  42. package/dist/data/async-chat-storage.js +96 -0
  43. package/dist/data/async-telemetry-maintenance.js +9 -0
  44. package/dist/data/bounded-worker-client.js +256 -0
  45. package/dist/data/chat-read-projections.js +159 -0
  46. package/dist/data/chat-read-worker.js +73 -0
  47. package/dist/data/chat-storage-worker.js +167 -0
  48. package/dist/data/ingest-service.js +152 -18
  49. package/dist/data/message-command-store.js +290 -0
  50. package/dist/data/payload-store.js +92 -11
  51. package/dist/data/pibo-store.js +10 -8
  52. package/dist/data/schema.js +39 -4
  53. package/dist/data/session-store.js +3 -1
  54. package/dist/data/storage-backup.js +278 -0
  55. package/dist/data/storage-maintenance.js +344 -0
  56. package/dist/data/storage-verification-worker.js +25 -0
  57. package/dist/data/telemetry-capture.js +188 -0
  58. package/dist/data/telemetry-command.js +3 -0
  59. package/dist/data/telemetry-maintenance-worker.js +40 -0
  60. package/dist/data/telemetry-maintenance.js +110 -0
  61. package/dist/data/telemetry-retention.js +16 -7
  62. package/dist/data/telemetry-worker.js +111 -0
  63. package/dist/data/telemetry-writer.js +150 -83
  64. package/dist/data/telemetry.js +5 -0
  65. package/dist/debug/index.js +207 -1
  66. package/dist/debug/message-queue.js +108 -0
  67. package/dist/debug/output-collision-repair.js +140 -0
  68. package/dist/debug/output-integrity.js +38 -2
  69. package/dist/debug/output-repair.js +1 -0
  70. package/dist/debug/storage-backup.js +41 -0
  71. package/dist/debug/storage-maintenance.js +78 -0
  72. package/dist/debug/telemetry-capture.js +66 -0
  73. package/dist/gateway/cli.js +71 -7
  74. package/dist/gateway/server.js +3 -0
  75. package/dist/providers/openai-gpt56.js +11 -6
  76. package/dist/reliability/store.js +119 -23
  77. package/dist/session-ui/terminalRows.js +54 -13
  78. package/dist/sessions/pibo-data-store.js +19 -14
  79. package/dist/shared/debug-features.js +4 -0
  80. package/dist/shared/model-inference-metrics.js +23 -0
  81. package/dist/shared/trace-event-projection.js +69 -2
  82. package/dist/shared/trace-history.js +9 -0
  83. package/dist/shared/trace-live-reducer.js +1 -0
  84. package/dist/shared/trace-patch-nodes.js +19 -0
  85. package/dist/web/channel.js +117 -12
  86. package/dist/web/http.js +135 -41
  87. package/npm-shrinkwrap.json +2 -2
  88. package/package.json +1 -1
  89. package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -1
  90. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  91. package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
@@ -0,0 +1,344 @@
1
+ import { fork } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { appendFileSync, existsSync, readFileSync, statSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ import { fileURLToPath } from "node:url";
7
+ const ROW_SAMPLE_LIMIT = 10_000;
8
+ const TABLE_LIMIT = 64;
9
+ const DEFAULT_DB_WARN_BYTES = 8 * 1024 ** 3;
10
+ const DEFAULT_WAL_WARN_BYTES = 256 * 1024 ** 2;
11
+ const DEFAULT_PAYLOAD_WARN_BYTES = 8 * 1024 ** 3;
12
+ const MAINTENANCE_LOG_SUFFIX = ".maintenance.jsonl";
13
+ export function inspectStorageStatus(input) {
14
+ const path = resolve(input.path);
15
+ const thresholds = {
16
+ databaseBytes: validThreshold(input.databaseWarnBytes, DEFAULT_DB_WARN_BYTES),
17
+ walBytes: validThreshold(input.walWarnBytes, DEFAULT_WAL_WARN_BYTES),
18
+ payloadBytes: validThreshold(input.payloadWarnBytes, DEFAULT_PAYLOAD_WARN_BYTES),
19
+ };
20
+ if (!existsSync(path))
21
+ return { resultType: "storage.status", readOnly: true, path, exists: false, health: "degraded", sizes: { database: 0, wal: 0, shm: 0, payloadStoreMetadataSample: 0, payloadStoreSampleComplete: true }, pages: { pageSize: 0, pageCount: 0, freelistCount: 0, freelistRatio: 0 }, wal: { busy: 0, logPages: 0, checkpointedPages: 0, pressure: false }, rows: [], payloads: { rows: 0, rowsComplete: true, sampledRows: 0, referencedRows: 0, metadataOrphans: 0, brokenReferences: 0, integrityComplete: true }, thresholds, last: {}, warnings: ["Database does not exist"] };
22
+ const db = new DatabaseSync(path, { readOnly: true });
23
+ try {
24
+ db.exec("PRAGMA busy_timeout = 50");
25
+ const pageSize = pragmaNumber(db, "page_size"), pageCount = pragmaNumber(db, "page_count"), freelistCount = pragmaNumber(db, "freelist_count");
26
+ const walRow = { busy: 0, logPages: pageSize ? Math.ceil(fileSize(`${path}-wal`) / pageSize) : 0, checkpointedPages: 0 };
27
+ const schemas = db.prepare("SELECT name, type FROM sqlite_schema WHERE type IN ('table','index') AND name NOT LIKE 'sqlite_%' ORDER BY CASE type WHEN 'table' THEN 0 ELSE 1 END, name LIMIT ?").all(TABLE_LIMIT);
28
+ const estimates = statEstimates(db);
29
+ const rows = schemas.map((schema) => {
30
+ if (schema.type === "index")
31
+ return { name: schema.name, kind: "index", ...(estimates.get(schema.name) !== undefined ? { estimatedRows: estimates.get(schema.name) } : {}) };
32
+ const count = boundedCount(db, schema.name);
33
+ return { name: schema.name, kind: "table", boundedCount: count.count, countComplete: count.complete, ...(estimates.get(schema.name) !== undefined ? { estimatedRows: estimates.get(schema.name) } : {}) };
34
+ });
35
+ const payload = inspectPayloadReferencesBounded(db);
36
+ const sizes = {
37
+ database: fileSize(path),
38
+ wal: fileSize(`${path}-wal`),
39
+ shm: fileSize(`${path}-shm`),
40
+ payloadStoreMetadataSample: payload.metadataBytes,
41
+ payloadStoreSampleComplete: payload.rowsComplete && payload.sampledRows === payload.rows,
42
+ };
43
+ const walPressure = sizes.wal >= thresholds.walBytes;
44
+ const warnings = [
45
+ ...(sizes.database >= thresholds.databaseBytes ? ["database_size_threshold"] : []),
46
+ ...(sizes.wal >= thresholds.walBytes ? ["wal_size_threshold"] : []),
47
+ ...(sizes.payloadStoreSampleComplete && sizes.payloadStoreMetadataSample >= thresholds.payloadBytes ? ["payload_size_threshold"] : []),
48
+ ...(!sizes.payloadStoreSampleComplete ? ["payload_size_threshold_indeterminate"] : []),
49
+ ...(walPressure ? ["wal_checkpoint_pressure"] : []),
50
+ ...(payload.metadataOrphans ? ["payload_metadata_orphans_sample"] : []),
51
+ ...(payload.brokenReferences ? ["broken_payload_references_sample"] : []),
52
+ ];
53
+ return {
54
+ resultType: "storage.status", readOnly: true, path, exists: true, health: warnings.length ? "degraded" : "healthy", sizes,
55
+ pages: { pageSize, pageCount, freelistCount, freelistRatio: pageCount ? freelistCount / pageCount : 0 },
56
+ wal: { ...walRow, pressure: walPressure }, rows,
57
+ payloads: { rows: payload.rows, rowsComplete: payload.rowsComplete, sampledRows: payload.sampledRows, referencedRows: payload.referencedRows, metadataOrphans: payload.metadataOrphans, brokenReferences: payload.brokenReferences, integrityComplete: payload.integrityComplete },
58
+ thresholds, last: readMaintenanceMetadata(path), warnings,
59
+ };
60
+ }
61
+ finally {
62
+ db.close();
63
+ }
64
+ }
65
+ export async function verifyStorage(input) {
66
+ const path = resolve(input.path), mode = input.mode ?? "quick", timeoutMs = input.timeoutMs ?? 60_000;
67
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3_600_000)
68
+ throw new Error("Verification timeout must be between 1 and 3600000 ms");
69
+ const started = Date.now(), progress = [];
70
+ const record = (stage, elapsedMs = Date.now() - started) => { const item = { stage, elapsedMs }; if (progress.length < 32)
71
+ progress.push(item); input.onProgress?.(item); };
72
+ const resultFor = (status, healthy, reason, messages = []) => ({ resultType: "storage.verification", path, mode, status, healthy, elapsedMs: Date.now() - started, progress, messages, ...(reason ? { reason } : {}) });
73
+ record("starting", 0);
74
+ if (input.signal?.aborted)
75
+ return resultFor("partial", false, "cancelled");
76
+ const result = await new Promise((resolveResult) => {
77
+ let child;
78
+ let settled = false;
79
+ let operationTimer;
80
+ let pendingPartialReason;
81
+ const startupTimer = setTimeout(() => requestStop("startup_timeout"), Math.min(5_000, Math.max(1_000, timeoutMs)));
82
+ const finish = (value) => {
83
+ if (settled)
84
+ return;
85
+ settled = true;
86
+ clearTimeout(startupTimer);
87
+ if (operationTimer)
88
+ clearTimeout(operationTimer);
89
+ input.signal?.removeEventListener("abort", cancel);
90
+ resolveResult(value);
91
+ };
92
+ const requestStop = (reason) => {
93
+ if (settled || pendingPartialReason)
94
+ return;
95
+ pendingPartialReason = reason;
96
+ record("terminating");
97
+ if (!child?.kill("SIGKILL"))
98
+ finish(resultFor("failed", false, "verification_process_not_terminated"));
99
+ };
100
+ const cancel = () => requestStop("cancelled");
101
+ input.signal?.addEventListener("abort", cancel, { once: true });
102
+ child = fork(fileURLToPath(new URL("./storage-verification-worker.js", import.meta.url)), [path, mode, ...(input.testNativeLongRunning ? ["native-long"] : [])], { stdio: ["ignore", "ignore", "ignore", "ipc"] });
103
+ child.on("message", (message) => {
104
+ if (!message || typeof message !== "object")
105
+ return;
106
+ const item = message;
107
+ if (item.type === "progress") {
108
+ const stage = String(item.stage);
109
+ record(stage, Number(item.elapsedMs));
110
+ if (stage.endsWith("_check") && !operationTimer) {
111
+ clearTimeout(startupTimer);
112
+ operationTimer = setTimeout(() => requestStop("timeout"), timeoutMs);
113
+ }
114
+ return;
115
+ }
116
+ if (item.type === "result") {
117
+ record("complete", Number(item.elapsedMs));
118
+ const messages = Array.isArray(item.messages) ? item.messages.map(String).slice(0, 100) : [];
119
+ finish(item.ok === true ? resultFor("complete", true, undefined, messages) : resultFor("failed", false, "integrity_errors", messages));
120
+ }
121
+ else if (item.type === "error")
122
+ finish(resultFor("failed", false, String(item.message ?? "verification_failed")));
123
+ });
124
+ child.on("error", (error) => finish(resultFor("failed", false, (error instanceof Error ? error.message : String(error)).slice(0, 500))));
125
+ child.on("exit", (code, signal) => {
126
+ if (pendingPartialReason) {
127
+ record("terminated");
128
+ finish(resultFor("partial", false, pendingPartialReason));
129
+ }
130
+ else if (!settled && code !== 0)
131
+ finish(resultFor("failed", false, `verification_process_exit_${code ?? signal ?? "unknown"}`));
132
+ });
133
+ });
134
+ if (result.status !== "partial") {
135
+ try {
136
+ recordMaintenance(path, { operation: "verification", at: new Date().toISOString(), mode, status: result.status, healthy: result.healthy, elapsedMs: result.elapsedMs });
137
+ }
138
+ catch { /* A read-only verification result remains valid when metadata storage is unavailable. */ }
139
+ }
140
+ return result;
141
+ }
142
+ export function checkpointStorage(input) {
143
+ const path = resolve(input.path), mode = input.mode ?? "passive";
144
+ if (!existsSync(path))
145
+ throw new Error(`Checkpoint database does not exist: ${path}`);
146
+ if (mode !== "passive" && mode !== "restart" && mode !== "truncate")
147
+ throw new Error("Checkpoint mode must be passive, restart, or truncate");
148
+ if (!input.apply)
149
+ return { resultType: "storage.checkpoint", mode: "dry-run", path, checkpointMode: mode, mutation: false };
150
+ const db = new DatabaseSync(path);
151
+ try {
152
+ db.exec("PRAGMA busy_timeout = 100");
153
+ const row = db.prepare(`PRAGMA wal_checkpoint(${mode.toUpperCase()})`).get();
154
+ const result = { resultType: "storage.checkpoint", mode: "apply", path, checkpointMode: mode, mutation: true, busy: Number(row.busy ?? 0), logPages: Number(row.log ?? 0), checkpointedPages: Number(row.checkpointed ?? 0), at: new Date().toISOString() };
155
+ recordMaintenance(path, { operation: "checkpoint", ...result });
156
+ return result;
157
+ }
158
+ finally {
159
+ db.close();
160
+ }
161
+ }
162
+ export async function maintainStorageRetention(input) {
163
+ const path = resolve(input.path), limit = input.limit ?? 1000;
164
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
165
+ throw new Error("Retention limit must be between 1 and 10000");
166
+ if (!Number.isFinite(Date.parse(input.before)))
167
+ throw new Error("Retention --before must be an ISO date");
168
+ const db = new DatabaseSync(path, { readOnly: !input.apply });
169
+ const target = tableExists(db, "event_log") ? { table: "event_log", time: "created_at" } : tableExists(db, "pibo_event_stream") ? { table: "pibo_event_stream", time: "created_at" } : undefined;
170
+ try {
171
+ if (!target)
172
+ return { resultType: "storage.retention", mode: input.apply ? "apply" : "dry-run", path, eligible: 0, deleted: 0, policy: "live_delta_only", preserved: ["chat_message", "audit_event", "idempotency_evidence", "referenced_payloads"] };
173
+ const eligibility = retentionEligibilitySql(target.table);
174
+ const eligible = Number(db.prepare(`SELECT COUNT(*) AS count FROM (SELECT 1 FROM ${target.table} WHERE ${eligibility} AND ${target.time} < ? ORDER BY ${target.time} LIMIT ?)`).get(input.before, limit).count);
175
+ const plan = retentionPlan(db, target.table, target.time, input.before, limit);
176
+ if (!input.apply)
177
+ return { resultType: "storage.retention", mode: "dry-run", path, eligible, deleteLimit: limit, policy: "live_delta_only", plan, preserved: ["chat_message", "audit_event", "idempotency_evidence", "referenced_payloads"] };
178
+ const auditId = `storage_retention_${randomUUID()}`;
179
+ const at = new Date().toISOString();
180
+ db.exec("PRAGMA busy_timeout = 100; BEGIN IMMEDIATE");
181
+ try {
182
+ db.exec(`CREATE TABLE IF NOT EXISTS storage_maintenance_audit (
183
+ id TEXT PRIMARY KEY,
184
+ operation TEXT NOT NULL,
185
+ status TEXT NOT NULL,
186
+ details_json TEXT NOT NULL,
187
+ created_at TEXT NOT NULL
188
+ )`);
189
+ const candidates = db.prepare(`SELECT rowid, ${target.table === "event_log" ? "payload_ref AS payloadRef" : "NULL AS payloadRef"} FROM ${target.table} WHERE ${eligibility} AND ${target.time} < ? ORDER BY ${target.time}, rowid LIMIT ?`).all(input.before, limit);
190
+ const deletion = db.prepare(`DELETE FROM ${target.table} WHERE rowid IN (SELECT rowid FROM ${target.table} WHERE ${eligibility} AND ${target.time} < ? ORDER BY ${target.time}, rowid LIMIT ?)`).run(input.before, limit);
191
+ const deleted = Number(deletion.changes);
192
+ if (deleted !== candidates.length)
193
+ throw new Error("Retention candidate/delete count changed while holding the write transaction");
194
+ const releasedPayloadIds = [...new Set(candidates.flatMap((row) => row.payloadRef ? [row.payloadRef] : []))];
195
+ const payloads = inspectReleasedPayloadsBounded(db, releasedPayloadIds);
196
+ const result = { resultType: "storage.retention", mode: "apply", path, eligible, deleted, deleteLimit: limit, policy: "live_delta_only", plan, payloads, auditId, preserved: ["chat_message", "audit_event", "idempotency_evidence", "referenced_payloads", "orphan_payload_files"], at };
197
+ db.prepare("INSERT INTO storage_maintenance_audit (id, operation, status, details_json, created_at) VALUES (?, 'retention', 'complete', ?, ?)").run(auditId, JSON.stringify(result), at);
198
+ db.exec("COMMIT");
199
+ try {
200
+ recordMaintenance(path, { operation: "retention", ...result });
201
+ }
202
+ catch { /* Transactional audit is authoritative. */ }
203
+ return result;
204
+ }
205
+ catch (error) {
206
+ if (db.isTransaction)
207
+ db.exec("ROLLBACK");
208
+ throw error;
209
+ }
210
+ }
211
+ finally {
212
+ db.close();
213
+ }
214
+ }
215
+ function retentionEligibilitySql(table) {
216
+ return table === "event_log"
217
+ ? "retention_class = 'live_delta' AND idempotency_key IS NULL"
218
+ : "retention_class = 'live_delta' AND idempotency_key IS NULL AND event_id IS NULL";
219
+ }
220
+ function retentionPlan(db, table, time, before, limit) {
221
+ const classes = ["live_delta", "trace_event", "chat_message", "audit_event"];
222
+ return classes.map((retentionClass) => {
223
+ const rows = Number(db.prepare(`SELECT COUNT(*) AS count FROM (SELECT 1 FROM ${table} WHERE retention_class=? AND ${time} < ? LIMIT ?)`).get(retentionClass, before, limit + 1).count);
224
+ return {
225
+ retentionClass,
226
+ rows: Math.min(rows, limit),
227
+ bounded: rows > limit,
228
+ disposition: retentionClass === "live_delta" ? "eligible" : retentionClass === "trace_event" ? "deferred_requires_policy" : "preserve",
229
+ };
230
+ });
231
+ }
232
+ function inspectPayloadReferencesBounded(db) {
233
+ if (!tableExists(db, "payloads"))
234
+ return { rows: 0, rowsComplete: true, sampledRows: 0, metadataBytes: 0, referencedRows: 0, metadataOrphans: 0, brokenReferences: 0, integrityComplete: true };
235
+ const rowCount = boundedCount(db, "payloads");
236
+ const payloadRows = db.prepare("SELECT id, COALESCE(compressed_byte_size, byte_size) AS bytes FROM payloads ORDER BY id LIMIT 1001").all();
237
+ const sample = payloadRows.slice(0, 1000);
238
+ const references = discoverPayloadReferenceColumns(db);
239
+ const sampledReferenceIds = new Set();
240
+ let brokenReferences = 0;
241
+ let referenceSamplesComplete = true;
242
+ for (const reference of references) {
243
+ const table = quoteIdentifier(reference.table), column = quoteIdentifier(reference.column);
244
+ // Do not filter here: LIMIT bounds rows examined even when references are sparse.
245
+ const rows = db.prepare(`SELECT ${column} AS id FROM ${table} LIMIT 1001`).all();
246
+ if (rows.length > 1000)
247
+ referenceSamplesComplete = false;
248
+ for (const row of rows.slice(0, 1000)) {
249
+ if (!row.id)
250
+ continue;
251
+ sampledReferenceIds.add(row.id);
252
+ if (!db.prepare("SELECT 1 FROM payloads WHERE id = ?").get(row.id))
253
+ brokenReferences += 1;
254
+ }
255
+ }
256
+ const referencedRows = sample.filter((payload) => sampledReferenceIds.has(payload.id)).length;
257
+ const integrityComplete = rowCount.complete && payloadRows.length <= 1000 && referenceSamplesComplete;
258
+ return {
259
+ rows: rowCount.count,
260
+ rowsComplete: rowCount.complete,
261
+ sampledRows: sample.length,
262
+ metadataBytes: sample.reduce((total, row) => total + Number(row.bytes ?? 0), 0),
263
+ referencedRows,
264
+ metadataOrphans: integrityComplete ? sample.length - referencedRows : 0,
265
+ brokenReferences,
266
+ integrityComplete,
267
+ };
268
+ }
269
+ function inspectReleasedPayloadsBounded(_db, candidateIds) {
270
+ const bounded = [...new Set(candidateIds)].slice(0, 1000);
271
+ return {
272
+ releasedReferenceCandidates: candidateIds.length,
273
+ candidatesReported: bounded.length,
274
+ candidatesTruncated: candidateIds.length > bounded.length,
275
+ referenceState: "not_scanned_online",
276
+ retainedMetadata: bounded.length,
277
+ retainedFiles: bounded.length,
278
+ action: "report_only",
279
+ };
280
+ }
281
+ function discoverPayloadReferenceColumns(db) {
282
+ const tables = db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name LIMIT 128").all();
283
+ const references = [];
284
+ for (const { name } of tables) {
285
+ const columns = db.prepare("SELECT name FROM pragma_table_info(?) LIMIT 128").all(name);
286
+ for (const column of columns) {
287
+ if (column.name === "payload_ref" || column.name === "content_payload_ref" || column.name === "payload_preview_ref")
288
+ references.push({ table: name, column: column.name });
289
+ }
290
+ }
291
+ return references.slice(0, 64);
292
+ }
293
+ function quoteIdentifier(value) { return `"${value.replaceAll('"', '""')}"`; }
294
+ function statEstimates(db) { const result = new Map(); if (!tableExists(db, "sqlite_stat1"))
295
+ return result; try {
296
+ for (const row of db.prepare("SELECT tbl, idx, stat FROM sqlite_stat1 LIMIT 256").all()) {
297
+ const estimate = Number.parseInt(row.stat.split(" ")[0] ?? "", 10);
298
+ if (Number.isFinite(estimate)) {
299
+ result.set(row.tbl, estimate);
300
+ if (row.idx)
301
+ result.set(row.idx, estimate);
302
+ }
303
+ }
304
+ }
305
+ catch { } return result; }
306
+ function boundedCount(db, name) { const quoted = `"${name.replaceAll('"', '""')}"`; try {
307
+ const count = Number(db.prepare(`SELECT COUNT(*) AS count FROM (SELECT 1 FROM ${quoted} LIMIT ?)`).get(ROW_SAMPLE_LIMIT + 1).count);
308
+ return { count: Math.min(count, ROW_SAMPLE_LIMIT), complete: count <= ROW_SAMPLE_LIMIT };
309
+ }
310
+ catch {
311
+ return { count: 0, complete: false };
312
+ } }
313
+ function pragmaNumber(db, name) { return Number(Object.values(db.prepare(`PRAGMA ${name}`).get() ?? { value: 0 })[0] ?? 0); }
314
+ function tableExists(db, table) { return Boolean(db.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND name=?").get(table)); }
315
+ function columnExists(db, table, column) { return Boolean(db.prepare(`SELECT 1 FROM pragma_table_info(?) WHERE name=?`).get(table, column)); }
316
+ function fileSize(path) { try {
317
+ return statSync(path).size;
318
+ }
319
+ catch {
320
+ return 0;
321
+ } }
322
+ function validThreshold(value, fallback) { return Number.isSafeInteger(value) && value > 0 ? value : fallback; }
323
+ export function recordStorageMaintenance(path, value) { appendFileSync(`${resolve(path)}${MAINTENANCE_LOG_SUFFIX}`, `${JSON.stringify(value)}\n`, { mode: 0o600 }); }
324
+ function recordMaintenance(path, value) { recordStorageMaintenance(path, value); }
325
+ function readMaintenanceMetadata(path) { const result = {}; try {
326
+ const metadataPath = `${path}${MAINTENANCE_LOG_SUFFIX}`;
327
+ const bytes = statSync(metadataPath).size;
328
+ if (bytes > 1024 * 1024)
329
+ return result;
330
+ const lines = readFileSync(metadataPath, "utf8").trim().split("\n").slice(-100);
331
+ for (const line of lines) {
332
+ const row = JSON.parse(line);
333
+ const operation = row.operation;
334
+ if (operation === "checkpoint")
335
+ result.checkpoint = row;
336
+ else if (operation === "backup")
337
+ result.backup = row;
338
+ else if (operation === "verification")
339
+ result.verification = row;
340
+ else if (operation === "retention")
341
+ result.retention = row;
342
+ }
343
+ }
344
+ catch { } return result; }
@@ -0,0 +1,25 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ const [path, mode, fixture] = process.argv.slice(2);
3
+ const started = Date.now();
4
+ const send = (message) => process.send?.(message);
5
+ let db;
6
+ try {
7
+ send({ type: "progress", stage: "opened", elapsedMs: 0 });
8
+ db = new DatabaseSync(path, { readOnly: true });
9
+ db.exec("PRAGMA busy_timeout = 50");
10
+ const pragma = mode === "full" ? "integrity_check" : "quick_check";
11
+ send({ type: "progress", stage: pragma, elapsedMs: Date.now() - started });
12
+ if (fixture === "native-long") {
13
+ db.prepare("WITH RECURSIVE counter(value) AS (VALUES(0) UNION ALL SELECT value + 1 FROM counter WHERE value < 1000000000) SELECT SUM(value) FROM counter").get();
14
+ }
15
+ const rows = db.prepare(`PRAGMA ${pragma}`).all();
16
+ const messages = rows.slice(0, 100).flatMap((row) => Object.values(row).map(String));
17
+ send({ type: "result", ok: messages.length === 1 && messages[0] === "ok", messages, elapsedMs: Date.now() - started });
18
+ }
19
+ catch (error) {
20
+ send({ type: "error", message: error instanceof Error ? error.message.slice(0, 500) : "Verification failed", elapsedMs: Date.now() - started });
21
+ }
22
+ finally {
23
+ db?.close();
24
+ process.disconnect?.();
25
+ }
@@ -0,0 +1,188 @@
1
+ import { DatabaseSync, backup } from "node:sqlite";
2
+ import { randomUUID, createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, statSync, openSync, closeSync, fsyncSync, createReadStream } from "node:fs";
4
+ import { join } from "node:path";
5
+ function validId(id) { if (!/^capture_[a-f0-9-]{36}$/.test(id))
6
+ throw Error("Invalid capture id"); return id; }
7
+ function readJson(path) { if (statSync(path).size > 16384)
8
+ throw Error("Capture manifest exceeds budget"); return JSON.parse(readFileSync(path, "utf8")); }
9
+ function publish(path, value) { const temp = path + ".tmp"; writeFileSync(temp, JSON.stringify(value) + "\n", { mode: 0o600 }); const fd = openSync(temp, "r"); try {
10
+ fsyncSync(fd);
11
+ }
12
+ finally {
13
+ closeSync(fd);
14
+ } renameSync(temp, path); }
15
+ export function inspectTelemetryCapture(root, id) { const m = readJson(join(root, validId(id), "manifest.json")); if (m.format !== "pibo-telemetry-capture-v1" || m.id !== id)
16
+ throw Error("Invalid capture manifest"); return m; }
17
+ export function startTelemetryCapture(root, input) {
18
+ if (!input.sessionId.startsWith("ps_") || input.sessionId.length > 128 || !input.owner.trim() || input.owner.length > 128 || !Number.isSafeInteger(input.durationMs) || input.durationMs < 1000 || input.durationMs > 3600000 || !Number.isSafeInteger(input.maxBytes) || input.maxBytes < 1024 || input.maxBytes > 64 * 1024 * 1024 || !Number.isSafeInteger(input.maxRows) || input.maxRows < 1 || input.maxRows > 100000)
19
+ throw Error("Capture requires explicit session, owner, duration, byte and row limits");
20
+ mkdirSync(root, { recursive: true, mode: 0o700 });
21
+ const active = join(root, "active.json");
22
+ const lock = openSync(active, "wx", 0o600);
23
+ const id = `capture_${randomUUID()}`, directory = join(root, id);
24
+ const m = { format: "pibo-telemetry-capture-v1", id, status: "active", sessionId: input.sessionId, owner: input.owner, detail: "provider_metadata", createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + input.durationMs).toISOString(), maxBytes: input.maxBytes, maxRows: input.maxRows, rows: 0, bytes: 0 };
25
+ try {
26
+ mkdirSync(directory, { mode: 0o700 });
27
+ const db = new DatabaseSync(join(directory, "active.sqlite"));
28
+ try {
29
+ db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA max_page_count=${Math.ceil((input.maxBytes + 1048576) / 4096)}; CREATE TABLE state(id INTEGER PRIMARY KEY CHECK(id=1),status TEXT NOT NULL,rows INTEGER NOT NULL,bytes INTEGER NOT NULL);INSERT INTO state VALUES(1,'active',0,0);CREATE TABLE events(sequence INTEGER PRIMARY KEY,received_at TEXT NOT NULL,type TEXT NOT NULL,metadata_json TEXT NOT NULL);`);
30
+ }
31
+ finally {
32
+ db.close();
33
+ }
34
+ publish(join(directory, "manifest.json"), m);
35
+ writeFileSync(lock, JSON.stringify({ id }) + "\n");
36
+ fsyncSync(lock);
37
+ return m;
38
+ }
39
+ catch (error) {
40
+ try {
41
+ unlinkSync(active);
42
+ }
43
+ catch { }
44
+ throw error;
45
+ }
46
+ finally {
47
+ closeSync(lock);
48
+ }
49
+ }
50
+ /** One run-owned connection; scopes and limits are checked transactionally before every append. */
51
+ export class TelemetryCaptureWriter {
52
+ root;
53
+ db;
54
+ manifest;
55
+ refreshAfter = 0;
56
+ failures = 0;
57
+ dropped = 0;
58
+ constructor(root) {
59
+ this.root = root;
60
+ }
61
+ status() { return { id: this.manifest?.id, rows: this.manifest?.rows, bytes: this.manifest?.bytes, expiresAt: this.manifest?.expiresAt, failures: this.failures, dropped: this.dropped }; }
62
+ close() { this.db?.close(); this.db = undefined; this.manifest = undefined; }
63
+ append(commands) {
64
+ try {
65
+ if (Date.now() >= this.refreshAfter) {
66
+ this.refreshAfter = Date.now() + 1000;
67
+ const path = join(this.root, "active.json");
68
+ if (!existsSync(path)) {
69
+ this.close();
70
+ return;
71
+ }
72
+ const id = validId(readJson(path).id);
73
+ if (this.manifest?.id !== id) {
74
+ this.close();
75
+ const m = inspectTelemetryCapture(this.root, id);
76
+ if (m.status !== "active")
77
+ return;
78
+ this.db = new DatabaseSync(join(this.root, id, "active.sqlite"));
79
+ this.db.exec("PRAGMA busy_timeout=10; PRAGMA synchronous=FULL");
80
+ this.manifest = m;
81
+ }
82
+ }
83
+ const db = this.db, m = this.manifest;
84
+ if (!db || !m)
85
+ return;
86
+ db.exec("BEGIN IMMEDIATE");
87
+ try {
88
+ const state = db.prepare("SELECT status,rows,bytes FROM state WHERE id=1").get();
89
+ if (state.status !== "active") {
90
+ db.exec("COMMIT");
91
+ this.close();
92
+ return;
93
+ }
94
+ const insert = db.prepare("INSERT INTO events(received_at,type,metadata_json) VALUES(?,?,?)");
95
+ let stopped = Date.now() >= Date.parse(m.expiresAt);
96
+ for (const input of commands) {
97
+ if (input.recorder !== "runtime" || input.command.kind !== "pi" || input.command.piboSessionId !== m.sessionId)
98
+ continue;
99
+ const summary = input.command.summary;
100
+ const metadata = { piboSessionId: m.sessionId, eventType: summary.eventType.slice(0, 256), byteSize: summary.byteSize, parseStatus: summary.parseStatus, normalizedType: summary.normalizedType?.slice(0, 256), assistantEventType: summary.assistantEventType?.slice(0, 128), messageEnded: summary.messageEnded };
101
+ const json = JSON.stringify(metadata), bytes = Buffer.byteLength(json) + 64;
102
+ if (stopped || state.rows >= m.maxRows || state.bytes + bytes > m.maxBytes) {
103
+ stopped = true;
104
+ this.dropped++;
105
+ continue;
106
+ }
107
+ insert.run(new Date().toISOString(), summary.eventType.slice(0, 256), json);
108
+ state.rows++;
109
+ state.bytes += bytes;
110
+ }
111
+ if (state.rows >= m.maxRows || state.bytes >= m.maxBytes)
112
+ stopped = true;
113
+ db.prepare("UPDATE state SET rows=?,bytes=?,status=? WHERE id=1").run(state.rows, state.bytes, stopped ? "stopped" : "active");
114
+ db.exec("COMMIT");
115
+ m.rows = state.rows;
116
+ m.bytes = state.bytes;
117
+ if (stopped) {
118
+ publish(join(this.root, m.id, "manifest.json"), { ...m, status: "stopped", rows: state.rows, bytes: state.bytes });
119
+ this.close();
120
+ }
121
+ }
122
+ catch (error) {
123
+ if (db.isTransaction)
124
+ db.exec("ROLLBACK");
125
+ throw error;
126
+ }
127
+ }
128
+ catch {
129
+ this.failures++;
130
+ this.close();
131
+ }
132
+ }
133
+ }
134
+ export async function finalizeTelemetryCapture(root, id) {
135
+ const m = inspectTelemetryCapture(root, id);
136
+ if (m.status === "archived")
137
+ return m;
138
+ const directory = join(root, id), db = new DatabaseSync(join(directory, "active.sqlite"));
139
+ try {
140
+ db.exec("PRAGMA busy_timeout=1000; PRAGMA synchronous=FULL; BEGIN IMMEDIATE");
141
+ db.exec("UPDATE state SET status='stopped' WHERE id=1");
142
+ const state = db.prepare("SELECT rows,bytes FROM state WHERE id=1").get();
143
+ db.exec("COMMIT");
144
+ publish(join(directory, "manifest.json"), { ...m, ...state, status: "stopped" });
145
+ // The archive is a separate closed snapshot; stale active handles cannot mutate it.
146
+ await backup(db, join(directory, "archive.sqlite"), { rate: 128 });
147
+ const archiveFd = openSync(join(directory, "archive.sqlite"), "r");
148
+ try {
149
+ fsyncSync(archiveFd);
150
+ }
151
+ finally {
152
+ closeSync(archiveFd);
153
+ }
154
+ const hash = createHash("sha256");
155
+ let bytes = 0;
156
+ for await (const chunk of createReadStream(join(directory, "archive.sqlite"), { highWaterMark: 65536 })) {
157
+ bytes += chunk.length;
158
+ if (bytes > m.maxBytes + 1048576)
159
+ throw Error("Capture archive exceeds physical quota");
160
+ hash.update(chunk);
161
+ }
162
+ const archived = { ...m, ...state, status: "archived", sha256: hash.digest("hex"), archiveBytes: bytes };
163
+ publish(join(directory, "manifest.json"), archived);
164
+ const pointer = join(root, "active.json");
165
+ if (existsSync(pointer) && readJson(pointer).id === id)
166
+ unlinkSync(pointer);
167
+ return archived;
168
+ }
169
+ finally {
170
+ if (db.isTransaction)
171
+ db.exec("ROLLBACK");
172
+ db.close();
173
+ }
174
+ }
175
+ export async function readTelemetryCapturePage(root, id, after = 0, limit = 50) { const m = inspectTelemetryCapture(root, id); if (m.status !== "archived")
176
+ throw Error("Capture must be finalized before inspection"); if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
177
+ throw Error("Invalid capture page budget"); const path = join(root, id, "archive.sqlite"), hash = createHash("sha256"); let bytes = 0; for await (const chunk of createReadStream(path, { highWaterMark: 65536 })) {
178
+ bytes += chunk.length;
179
+ if (bytes > m.maxBytes + 1048576)
180
+ throw Error("Capture archive exceeds physical quota");
181
+ hash.update(chunk);
182
+ } if (hash.digest("hex") !== m.sha256 || bytes !== m.archiveBytes)
183
+ throw Error("Capture archive hash mismatch"); const db = new DatabaseSync(path, { readOnly: true }); try {
184
+ return db.prepare("SELECT * FROM events WHERE sequence>? ORDER BY sequence LIMIT ?").all(after, limit);
185
+ }
186
+ finally {
187
+ db.close();
188
+ } }
@@ -0,0 +1,3 @@
1
+ export function isTelemetryProgress(input) {
2
+ return input.recorder === "runtime" && (input.command.kind === "pi" || (input.command.kind === "output" && ["assistant_delta", "thinking_delta", "tool_execution_updated"].includes(input.command.event.type)));
3
+ }
@@ -0,0 +1,40 @@
1
+ import { statSync } from "node:fs";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { parentPort, workerData, threadId } from "node:worker_threads";
4
+ import { pruneTelemetryRetention } from "./telemetry-retention.js";
5
+ import { TelemetryMaintenance } from "./telemetry-maintenance.js";
6
+ if (!parentPort)
7
+ throw Error("Maintenance requires a worker");
8
+ const db = new DatabaseSync(workerData.path);
9
+ db.exec("PRAGMA busy_timeout=10; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON");
10
+ const maintenance = new TelemetryMaintenance(db);
11
+ let timer, failures = 0, delay = 25;
12
+ let checkpointAfter = 0, checkpoint;
13
+ function observeCheckpoint() { if (Date.now() < checkpointAfter)
14
+ return; checkpointAfter = Date.now() + 5000; const started = performance.now(); const row = db.prepare("PRAGMA wal_checkpoint(PASSIVE)").get(); let walBytes = 0; try {
15
+ walBytes = statSync(workerData.path + "-wal").size;
16
+ }
17
+ catch { } checkpoint = { durationMs: performance.now() - started, busy: row.busy, logPages: row.log, checkpointedPages: row.checkpointed, walBytes, pendingSince: row.log > row.checkpointed ? (checkpoint?.pendingSince ?? Date.now()) : undefined }; }
18
+ function schedule() { if (timer || maintenance.status()?.status !== "running")
19
+ return; timer = setTimeout(() => { timer = undefined; try {
20
+ maintenance.step();
21
+ observeCheckpoint();
22
+ delay = 25;
23
+ }
24
+ catch {
25
+ failures++;
26
+ delay = Math.min(1000, delay * 2);
27
+ } schedule(); }, delay); timer.unref(); }
28
+ parentPort.on("message", (request) => {
29
+ try {
30
+ const { action, cutoff } = request.command;
31
+ const value = action === "preview" ? ["live", "diagnostic", "provider_event", "payload_preview", "incident"].map(retentionClass => pruneTelemetryRetention(db, { retentionClass: retentionClass, before: cutoff, apply: false })) : action === "start" ? maintenance.start(cutoff) : action === "status" ? maintenance.status() : maintenance.control(action);
32
+ if (action === "start" || action === "resume")
33
+ schedule();
34
+ parentPort.postMessage({ id: request.id, value: value ?? null, worker: { pid: process.pid, threadId, failures, delay, checkpoint, status: value } });
35
+ }
36
+ catch {
37
+ parentPort.postMessage({ id: request.id, error: { code: "storage_maintenance_failed", message: "Telemetry maintenance command failed" } });
38
+ }
39
+ });
40
+ parentPort.postMessage({ ready: true, worker: { pid: process.pid, threadId, failures, delay, checkpoint, status: maintenance.status() } });