@pasko70/pibo 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/agent-runtime/routed-session.js +80 -19
  2. package/dist/agent-runtimes/codex-native/turn.js +27 -3
  3. package/dist/apps/chat/data/chat-data-mappers.js +8 -1
  4. package/dist/apps/chat/data/read-state-service.js +24 -3
  5. package/dist/apps/chat/message-command-dispatcher.js +16 -1
  6. package/dist/apps/chat/web-app.js +40 -19
  7. package/dist/apps/chat-ui/assets/{dist-BG0n7zLd.js → dist-BQ-TQZet.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-D79vyxSX.js → dist-CYTRhePp.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-D6TjFhAm.js → dist-Crj0LNXs.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-DFZ8cwh0.js → dist-t24MVWWG.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-cOjokPrK.js → dist-tFj2S0WU.js} +1 -1
  12. package/dist/apps/chat-ui/assets/index-Bc9O0z52.css +1 -0
  13. package/dist/apps/chat-ui/assets/{index-RMHUTJ62.js → index-Cr2oFRhI.js} +77 -77
  14. package/dist/apps/chat-ui/index.html +2 -2
  15. package/dist/apps/chat-vscode-web/assets/{index-xacbCyTx.js → index-DgKYVP-6.js} +11 -11
  16. package/dist/apps/chat-vscode-web/index.html +1 -1
  17. package/dist/cli-session/localSessionSource.js +3 -2
  18. package/dist/core/output-render-sequence.js +63 -6
  19. package/dist/core/session-router.js +99 -11
  20. package/dist/data/async-chat-storage.js +7 -2
  21. package/dist/data/bounded-worker-client.js +1 -1
  22. package/dist/data/chat-read-projections.js +4 -4
  23. package/dist/data/chat-storage-worker.js +24 -3
  24. package/dist/data/ingest-service.js +73 -4
  25. package/dist/data/message-command-store.js +153 -11
  26. package/dist/data/schema.js +24 -3
  27. package/dist/data/storage-maintenance.js +344 -0
  28. package/dist/data/storage-verification-worker.js +25 -0
  29. package/dist/debug/cache.js +136 -0
  30. package/dist/debug/index.js +201 -1
  31. package/dist/debug/message-queue.js +108 -0
  32. package/dist/debug/output-collision-repair.js +140 -0
  33. package/dist/debug/output-integrity.js +38 -2
  34. package/dist/debug/output-repair.js +1 -0
  35. package/dist/debug/session.js +2 -0
  36. package/dist/debug/storage-backup.js +12 -4
  37. package/dist/debug/storage-maintenance.js +78 -0
  38. package/dist/debug/summary.js +1 -0
  39. package/dist/debug/trace.js +28 -0
  40. package/dist/gateway/cli.js +71 -7
  41. package/dist/gateway/server.js +1 -0
  42. package/dist/reliability/store.js +119 -23
  43. package/dist/session-ui/terminalRows.js +7 -8
  44. package/dist/sessions/pibo-data-store.js +18 -14
  45. package/dist/shared/cache-observability.js +73 -0
  46. package/dist/shared/model-inference-metrics.js +20 -7
  47. package/dist/shared/trace-event-projection.js +44 -6
  48. package/dist/web/channel.js +114 -12
  49. package/dist/web/http.js +105 -44
  50. package/npm-shrinkwrap.json +2 -2
  51. package/package.json +1 -1
  52. package/dist/apps/chat-ui/assets/index-hEkrlRk-.css +0 -1
@@ -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,136 @@
1
+ import { cacheUsageWarningText } from "../shared/cache-observability.js";
2
+ import { compareInferenceCompletion, modelInferenceCacheReadRatio, modelInferenceCachedInputTokens, modelInferenceInputTokens, modelInferenceUncachedInputTokens, } from "../shared/model-inference-metrics.js";
3
+ import { inspectDebugTrace } from "./trace.js";
4
+ import { formatNextCommands } from "./next-commands.js";
5
+ function finiteToken(value) {
6
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
7
+ }
8
+ export async function inspectDebugCache(piboSessionId, stores, options = {}) {
9
+ const trace = await inspectDebugTrace(piboSessionId, stores);
10
+ const byId = new Map();
11
+ for (const node of trace.nodes) {
12
+ for (const record of node.modelInferences ?? [])
13
+ byId.set(record.id, { nodeId: node.id, nodeTitle: node.title, record });
14
+ }
15
+ const located = [...byId.values()].sort((left, right) => compareInferenceCompletion(left.record, right.record));
16
+ let reportedInput = 0;
17
+ let reportedRead = 0;
18
+ let reportedCount = 0;
19
+ let unreportedCount = 0;
20
+ let writeTotal = 0;
21
+ let writeReported = false;
22
+ let possibleDropCount = 0;
23
+ for (const { record } of located) {
24
+ const input = modelInferenceInputTokens(record.metrics);
25
+ const read = modelInferenceCachedInputTokens(record.metrics);
26
+ if (input !== undefined && input > 0 && read !== undefined && read <= input) {
27
+ reportedInput += input;
28
+ reportedRead += read;
29
+ reportedCount++;
30
+ }
31
+ else {
32
+ unreportedCount++;
33
+ }
34
+ const write = finiteToken(record.metrics.cacheWriteTokens);
35
+ if (write !== undefined) {
36
+ writeTotal += write;
37
+ writeReported = true;
38
+ }
39
+ if (record.cacheObservation?.warning === "possible-cache-read-drop")
40
+ possibleDropCount++;
41
+ }
42
+ const limit = options.limit ?? 20;
43
+ const selected = located.slice(Math.max(0, located.length - limit));
44
+ return {
45
+ piboSessionId,
46
+ runtimeInstanceId: trace.runtimeInstanceId,
47
+ runtimeAdapterId: trace.runtimeAdapterId,
48
+ source: "provider-reported-usage",
49
+ summary: {
50
+ inferenceCount: located.length,
51
+ cacheReadReportedCount: reportedCount,
52
+ cacheReadUnreportedCount: unreportedCount,
53
+ possibleDropCount,
54
+ ...(reportedCount ? {
55
+ inputTokens: reportedInput,
56
+ cacheReadTokens: reportedRead,
57
+ uncachedInputTokens: reportedInput - reportedRead,
58
+ cacheReadRatio: reportedRead / reportedInput,
59
+ } : {}),
60
+ ...(writeReported ? { cacheWriteTokens: writeTotal } : {}),
61
+ },
62
+ inferences: selected.map(({ nodeId, nodeTitle, record }) => ({
63
+ id: record.id,
64
+ nodeId,
65
+ nodeTitle,
66
+ completedAt: record.completedAt,
67
+ inputTokens: modelInferenceInputTokens(record.metrics),
68
+ cacheReadTokens: modelInferenceCachedInputTokens(record.metrics),
69
+ cacheWriteTokens: finiteToken(record.metrics.cacheWriteTokens),
70
+ uncachedInputTokens: modelInferenceUncachedInputTokens(record.metrics),
71
+ cacheReadRatio: modelInferenceCacheReadRatio(record.metrics),
72
+ previousCacheReadRatio: record.cacheObservation?.previousCacheReadRatio,
73
+ elapsedMs: record.cacheObservation?.elapsedMs,
74
+ cacheState: record.cacheObservation?.cacheState ?? "unknown",
75
+ warning: record.cacheObservation?.warning ?? "none",
76
+ warningText: record.cacheObservation ? cacheUsageWarningText(record.cacheObservation) : undefined,
77
+ explanation: record.cacheObservation?.explanation ?? "insufficient-data",
78
+ })),
79
+ limitations: [
80
+ "Cache counters are reported by the runtime or provider; missing counters remain unknown.",
81
+ "A cache-read drop does not identify whether Pibo, the runtime, the provider, or eviction caused it.",
82
+ "This command does not inspect or alter runtime source code, prompts, or provider cache keys.",
83
+ ],
84
+ nextCommands: [
85
+ `pibo debug trace ${piboSessionId} --medium`,
86
+ `pibo debug events ${piboSessionId} --type assistant_usage --fields inputTokens,cacheReadTokens,cacheWriteTokens,totalTokens`,
87
+ ],
88
+ };
89
+ }
90
+ function metric(value) {
91
+ return value === undefined ? "unknown" : String(value);
92
+ }
93
+ function ratio(value) {
94
+ return value === undefined ? "unknown" : `${(value * 100).toFixed(1)}%`;
95
+ }
96
+ export function formatDebugCache(result) {
97
+ const lines = [
98
+ `piboSessionId: ${result.piboSessionId}`,
99
+ ...(result.runtimeInstanceId ? [`runtimeInstanceId: ${result.runtimeInstanceId}`] : []),
100
+ ...(result.runtimeAdapterId ? [`runtimeAdapterId: ${result.runtimeAdapterId}`] : []),
101
+ `source: ${result.source}`,
102
+ `inferences: ${result.summary.inferenceCount}`,
103
+ `cacheReadReported: ${result.summary.cacheReadReportedCount}`,
104
+ `cacheReadUnreported: ${result.summary.cacheReadUnreportedCount}`,
105
+ `possibleDrops: ${result.summary.possibleDropCount}`,
106
+ `inputTokens: ${metric(result.summary.inputTokens)}`,
107
+ `cacheReadTokens: ${metric(result.summary.cacheReadTokens)}`,
108
+ `uncachedInputTokens: ${metric(result.summary.uncachedInputTokens)}`,
109
+ `cacheWriteTokens: ${metric(result.summary.cacheWriteTokens)}`,
110
+ `cacheReadRatio: ${ratio(result.summary.cacheReadRatio)}`,
111
+ "",
112
+ ];
113
+ if (result.inferences.length) {
114
+ lines.push("completedAt\tstate\tinput\tcacheRead\tuncached\tcacheWrite\tratio\tid\tnode");
115
+ for (const inference of result.inferences) {
116
+ lines.push([
117
+ inference.completedAt ?? "unknown",
118
+ inference.cacheState,
119
+ metric(inference.inputTokens),
120
+ metric(inference.cacheReadTokens),
121
+ metric(inference.uncachedInputTokens),
122
+ metric(inference.cacheWriteTokens),
123
+ ratio(inference.cacheReadRatio),
124
+ inference.id,
125
+ inference.nodeTitle,
126
+ ].join("\t"));
127
+ }
128
+ }
129
+ for (const inference of result.inferences) {
130
+ if (inference.warningText)
131
+ lines.push(`cache-warning\t${inference.id}\t${inference.warningText}`);
132
+ }
133
+ lines.push("", "Limitations:", ...result.limitations.map((item) => `- ${item}`));
134
+ lines.push(...formatNextCommands(result.nextCommands));
135
+ return lines.join("\n");
136
+ }