@pasko70/pibo 3.4.4 → 3.5.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.
- package/dist/agent-runtime/routed-session.js +37 -12
- package/dist/agent-runtimes/pi/adapter.js +1 -0
- package/dist/agent-runtimes/pi/routed-session.js +2 -2
- package/dist/apps/chat/bounded-event-stream.js +98 -0
- package/dist/apps/chat/chat-settings-routes.js +3 -3
- package/dist/apps/chat/data/chat-data-mappers.js +32 -13
- package/dist/apps/chat/data/event-command-service.js +30 -21
- package/dist/apps/chat/data/history-query-service.js +34 -25
- package/dist/apps/chat/data/read-state-service.js +13 -0
- package/dist/apps/chat/data/session-query-service.js +19 -11
- package/dist/apps/chat/data/timeline-query-service.js +19 -7
- package/dist/apps/chat/message-command-dispatcher.js +134 -0
- package/dist/apps/chat/output-compactor.js +9 -0
- package/dist/apps/chat/output-event-policy.js +9 -1
- package/dist/apps/chat/stream.js +21 -3
- package/dist/apps/chat/telemetry-retention-service.js +113 -8
- package/dist/apps/chat/trace-response-cache.js +46 -0
- package/dist/apps/chat/trace-v2.js +12 -6
- package/dist/apps/chat/trace.js +1 -1
- package/dist/apps/chat/web-app.js +608 -281
- package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-BG0n7zLd.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-D6TjFhAm.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-D79vyxSX.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-DFZ8cwh0.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-cOjokPrK.js} +1 -1
- package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
- package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/compute/pool/seeds.js +25 -3
- package/dist/core/events.js +4 -0
- package/dist/core/output-render-sequence.js +2 -0
- package/dist/core/provider-capacity.js +33 -0
- package/dist/core/provider-telemetry.js +21 -6
- package/dist/core/runtime-capacity.js +174 -0
- package/dist/core/runtime-telemetry.js +40 -12
- package/dist/core/session-router.js +79 -2
- package/dist/data/async-chat-reads.js +38 -0
- package/dist/data/async-chat-storage.js +91 -0
- package/dist/data/async-telemetry-maintenance.js +9 -0
- package/dist/data/bounded-worker-client.js +256 -0
- package/dist/data/chat-read-projections.js +159 -0
- package/dist/data/chat-read-worker.js +73 -0
- package/dist/data/chat-storage-worker.js +146 -0
- package/dist/data/ingest-service.js +79 -14
- package/dist/data/message-command-store.js +148 -0
- package/dist/data/payload-store.js +92 -11
- package/dist/data/pibo-store.js +10 -8
- package/dist/data/schema.js +16 -2
- package/dist/data/session-store.js +3 -1
- package/dist/data/storage-backup.js +278 -0
- package/dist/data/telemetry-capture.js +188 -0
- package/dist/data/telemetry-command.js +3 -0
- package/dist/data/telemetry-maintenance-worker.js +40 -0
- package/dist/data/telemetry-maintenance.js +110 -0
- package/dist/data/telemetry-retention.js +16 -7
- package/dist/data/telemetry-worker.js +111 -0
- package/dist/data/telemetry-writer.js +150 -83
- package/dist/data/telemetry.js +5 -0
- package/dist/debug/index.js +52 -0
- package/dist/debug/storage-backup.js +33 -0
- package/dist/debug/telemetry-capture.js +66 -0
- package/dist/gateway/server.js +2 -0
- package/dist/providers/openai-gpt56.js +11 -6
- package/dist/session-ui/terminalRows.js +48 -6
- package/dist/sessions/pibo-data-store.js +1 -0
- package/dist/shared/debug-features.js +4 -0
- package/dist/shared/model-inference-metrics.js +23 -0
- package/dist/shared/trace-event-projection.js +59 -2
- package/dist/shared/trace-history.js +9 -0
- package/dist/shared/trace-live-reducer.js +1 -0
- package/dist/shared/trace-patch-nodes.js +19 -0
- package/dist/web/channel.js +3 -0
- package/dist/web/http.js +36 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
package/dist/data/pibo-store.js
CHANGED
|
@@ -23,10 +23,12 @@ export class PiboDataStore {
|
|
|
23
23
|
telemetry;
|
|
24
24
|
constructor(path = piboHomePath("pibo.sqlite"), options = {}) {
|
|
25
25
|
this.path = path === ":memory:" ? path : resolve(path);
|
|
26
|
-
const insidePiboHome = ensurePrivatePiboHomeForPath(this.path);
|
|
27
|
-
if (this.path !== ":memory:")
|
|
26
|
+
const insidePiboHome = !options.readOnly && ensurePrivatePiboHomeForPath(this.path);
|
|
27
|
+
if (!options.readOnly && this.path !== ":memory:")
|
|
28
28
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
29
|
-
this.db = new DatabaseSync(this.path);
|
|
29
|
+
this.db = new DatabaseSync(this.path, { readOnly: options.readOnly === true });
|
|
30
|
+
// Connection-only configuration also protects the initial schema read.
|
|
31
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
30
32
|
try {
|
|
31
33
|
assertSupportedPiboDataSchemaVersion(this.db);
|
|
32
34
|
}
|
|
@@ -34,14 +36,14 @@ export class PiboDataStore {
|
|
|
34
36
|
this.db.close();
|
|
35
37
|
throw error;
|
|
36
38
|
}
|
|
37
|
-
if (insidePiboHome)
|
|
39
|
+
if (!options.readOnly && insidePiboHome)
|
|
38
40
|
protectPrivateFileSync(this.path);
|
|
39
|
-
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
40
41
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
41
|
-
if (this.path !== ":memory:")
|
|
42
|
+
if (!options.readOnly && this.path !== ":memory:")
|
|
42
43
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
if (!options.readOnly)
|
|
45
|
+
applyPiboDataSchema(this.db);
|
|
46
|
+
this.payloads = new PayloadStore(this.db, options.payloadRootDir ?? piboHomePath("payloads"), options.readOnly);
|
|
45
47
|
this.eventLog = new PiboEventLogStore(this.db);
|
|
46
48
|
this.messages = new MessageStore(this.db);
|
|
47
49
|
this.observations = new ObservationStore(this.db);
|
package/dist/data/schema.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { CHAT_READ_PROJECTION_SCHEMA, CHAT_NAVIGATION_REVISION_SCHEMA } from "./chat-read-projections.js";
|
|
2
|
+
import { TELEMETRY_MAINTENANCE_SCHEMA } from "./telemetry-maintenance.js";
|
|
3
|
+
import { MESSAGE_COMMAND_SCHEMA } from "./message-command-store.js";
|
|
4
|
+
export const PIBO_DATA_SCHEMA_VERSION = 13;
|
|
2
5
|
const NATIVE_HISTORY_FALLBACK_SCHEMA_VERSION = 5;
|
|
3
6
|
const retiredScopeColumn = ["owner", "scope"].join("_");
|
|
4
7
|
const payloadTableDefinition = `
|
|
@@ -710,6 +713,12 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
|
|
|
710
713
|
ON telemetry_turns(retention_class, updated_at);
|
|
711
714
|
CREATE INDEX IF NOT EXISTS idx_telemetry_phases_turn_started
|
|
712
715
|
ON telemetry_phases(turn_id, started_at ASC);
|
|
716
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_phases_open_name
|
|
717
|
+
ON telemetry_phases(turn_id, name, COALESCE(last_progress_at, started_at) DESC, created_at DESC) WHERE status='open';
|
|
718
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_phases_open_turn
|
|
719
|
+
ON telemetry_phases(turn_id, started_at ASC, created_at ASC) WHERE status='open';
|
|
720
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_phases_turn_name
|
|
721
|
+
ON telemetry_phases(turn_id, name);
|
|
713
722
|
CREATE INDEX IF NOT EXISTS idx_telemetry_phases_session_status
|
|
714
723
|
ON telemetry_phases(pibo_session_id, status, last_progress_at);
|
|
715
724
|
CREATE INDEX IF NOT EXISTS idx_telemetry_phases_provider_request
|
|
@@ -759,6 +768,9 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
|
|
|
759
768
|
CREATE INDEX IF NOT EXISTS idx_telemetry_tool_calls_retention_updated
|
|
760
769
|
ON telemetry_tool_calls(retention_class, updated_at);
|
|
761
770
|
`);
|
|
771
|
+
db.exec(CHAT_READ_PROJECTION_SCHEMA);
|
|
772
|
+
db.exec(CHAT_NAVIGATION_REVISION_SCHEMA);
|
|
773
|
+
db.exec(TELEMETRY_MAINTENANCE_SCHEMA);
|
|
762
774
|
hooks.afterStep?.("schema");
|
|
763
775
|
db.exec(`
|
|
764
776
|
INSERT OR IGNORE INTO session_runtime_bindings (
|
|
@@ -798,6 +810,7 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
|
|
|
798
810
|
END;
|
|
799
811
|
`);
|
|
800
812
|
hooks.afterStep?.("render-high-water");
|
|
813
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_event_log_sequence_repair_candidates ON event_log(session_id) WHERE session_id IS NOT NULL AND (session_sequence IS NULL OR session_sequence <= 0)");
|
|
801
814
|
// Always inspect for interrupted pre-atomic v7 repairs. A previous process
|
|
802
815
|
// may have written negative temporary values before setting user_version.
|
|
803
816
|
db.exec(`
|
|
@@ -807,7 +820,7 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
|
|
|
807
820
|
);
|
|
808
821
|
INSERT INTO pibo_v7_sequence_repair_sessions (session_id)
|
|
809
822
|
SELECT DISTINCT session_id
|
|
810
|
-
FROM event_log
|
|
823
|
+
FROM event_log INDEXED BY idx_event_log_sequence_repair_candidates
|
|
811
824
|
WHERE session_id IS NOT NULL
|
|
812
825
|
AND (session_sequence IS NULL OR session_sequence <= 0);
|
|
813
826
|
`);
|
|
@@ -858,6 +871,7 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
|
|
|
858
871
|
}
|
|
859
872
|
}
|
|
860
873
|
hooks.afterStep?.("runtime-binding-metadata");
|
|
874
|
+
db.exec(MESSAGE_COMMAND_SCHEMA);
|
|
861
875
|
db.exec(`PRAGMA user_version = ${PIBO_DATA_SCHEMA_VERSION}`);
|
|
862
876
|
hooks.afterStep?.("user-version");
|
|
863
877
|
}
|
|
@@ -31,7 +31,7 @@ export class SessionStore {
|
|
|
31
31
|
now,
|
|
32
32
|
];
|
|
33
33
|
const assignments = [
|
|
34
|
-
"pi_session_id = excluded.pi_session_id",
|
|
34
|
+
...(input.preserveRuntimeBinding ? [] : ["pi_session_id = excluded.pi_session_id"]),
|
|
35
35
|
"room_id = excluded.room_id",
|
|
36
36
|
"root_session_id = excluded.root_session_id",
|
|
37
37
|
"parent_id = excluded.parent_id",
|
|
@@ -53,6 +53,8 @@ export class SessionStore {
|
|
|
53
53
|
VALUES (${baseColumns.map(() => "?").join(", ")})
|
|
54
54
|
ON CONFLICT(id) DO UPDATE SET ${assignments.join(", ")}
|
|
55
55
|
`).run(...values);
|
|
56
|
+
if (input.preserveRuntimeBinding)
|
|
57
|
+
return;
|
|
56
58
|
const binding = input.session.runtimeBinding
|
|
57
59
|
?? createLegacyPiRuntimeSessionBinding(input.session.id, input.session.piSessionId, input.session.createdAt);
|
|
58
60
|
this.db.prepare(`
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { DatabaseSync, backup } from "node:sqlite";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createReadStream, createWriteStream, statSync } from "node:fs";
|
|
4
|
+
import { mkdir, open, readFile, rename, stat, copyFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, resolve, relative, join, isAbsolute } from "node:path";
|
|
6
|
+
import { createGunzip } from "node:zlib";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
const MANIFEST = "manifest.json", CATALOG = "payloads.jsonl", DATABASE = "snapshot.sqlite";
|
|
9
|
+
function ownedPath(root, path) { const target = resolve(root, path), rel = relative(resolve(root), target); if (isAbsolute(path) || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\"))
|
|
10
|
+
throw Error("Backup path escapes its directory"); return target; }
|
|
11
|
+
async function syncFile(path) { const file = await open(path, "r"); try {
|
|
12
|
+
await file.sync();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await file.close();
|
|
16
|
+
} }
|
|
17
|
+
async function publishManifest(root, manifest) { const temp = join(root, MANIFEST + ".tmp"), file = await open(temp, "w", 0o600); try {
|
|
18
|
+
await file.writeFile(JSON.stringify(manifest) + "\n");
|
|
19
|
+
await file.sync();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await file.close();
|
|
23
|
+
} await rename(temp, join(root, MANIFEST)); await syncFile(root); }
|
|
24
|
+
export async function readStorageBackupManifest(root) { const path = join(root, MANIFEST); if ((await stat(path)).size > 16384)
|
|
25
|
+
throw Error("Backup manifest exceeds budget"); const m = JSON.parse(await readFile(path, "utf8")); if (m.format !== "pibo-storage-backup-v1" || !["snapshot", "payloads", "complete"].includes(m.status) || !Number.isSafeInteger(m.maxBytes) || !Number.isSafeInteger(m.maxPayloads))
|
|
26
|
+
throw Error("Unsupported backup manifest"); return m; }
|
|
27
|
+
async function digest(path, maximum, encoding, signal) { const input = createReadStream(path, { highWaterMark: 65536, signal }); const stream = encoding === "gzip" ? input.pipe(createGunzip()) : input; let bytes = 0; const hash = createHash("sha256"); try {
|
|
28
|
+
for await (const chunk of stream) {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
bytes += chunk.length;
|
|
31
|
+
if (bytes > maximum)
|
|
32
|
+
throw Error("Backup byte quota exceeded");
|
|
33
|
+
hash.update(chunk);
|
|
34
|
+
}
|
|
35
|
+
return { sha256: hash.digest("hex"), bytes };
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
input.destroy();
|
|
39
|
+
stream.destroy();
|
|
40
|
+
} }
|
|
41
|
+
/** Explicit offline operator operation. The SQLite online-backup API includes committed WAL data. */
|
|
42
|
+
export async function createStorageBackup(input) {
|
|
43
|
+
const milliseconds = input.maxMilliseconds ?? 60000;
|
|
44
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < 1 || milliseconds > 3600000)
|
|
45
|
+
throw Error("Invalid backup time quota");
|
|
46
|
+
const timeout = AbortSignal.timeout(milliseconds);
|
|
47
|
+
input = { ...input, signal: input.signal ? AbortSignal.any([input.signal, timeout]) : timeout };
|
|
48
|
+
const root = resolve(input.destination), source = resolve(input.source), payloadRoot = resolve(input.payloadRoot), deadline = Date.now() + (input.maxMilliseconds ?? 60000);
|
|
49
|
+
const check = () => { input.signal?.throwIfAborted(); if (Date.now() > deadline)
|
|
50
|
+
throw Error("Backup time budget exhausted; resume explicitly"); };
|
|
51
|
+
let manifest;
|
|
52
|
+
let snapshotVerified = false;
|
|
53
|
+
if (input.resume) {
|
|
54
|
+
manifest = await readStorageBackupManifest(root);
|
|
55
|
+
if (manifest.source !== source || manifest.payloadRoot !== payloadRoot)
|
|
56
|
+
throw Error("Backup resume source does not match manifest");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const maxBytes = input.maxBytes ?? 1024 * 1024 * 1024, maxPayloads = input.maxPayloads ?? 100000;
|
|
60
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 4096 || !Number.isSafeInteger(maxPayloads) || maxPayloads < 1 || maxPayloads > 1000000)
|
|
61
|
+
throw Error("Invalid backup quota");
|
|
62
|
+
await mkdir(root, { mode: 0o700 });
|
|
63
|
+
manifest = { format: "pibo-storage-backup-v1", status: "snapshot", source, payloadRoot, createdAt: new Date().toISOString(), payloadCount: 0, payloadBytes: 0, cursor: "", maxBytes, maxPayloads };
|
|
64
|
+
await publishManifest(root, manifest);
|
|
65
|
+
}
|
|
66
|
+
if (manifest.status === "complete") {
|
|
67
|
+
await verifyStorageBackup(root, input.signal);
|
|
68
|
+
return manifest;
|
|
69
|
+
}
|
|
70
|
+
if (manifest.status === "snapshot") {
|
|
71
|
+
check();
|
|
72
|
+
const walBytes = () => { try {
|
|
73
|
+
return statSync(source + "-wal").size;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return 0;
|
|
77
|
+
} };
|
|
78
|
+
const initialWalBytes = walBytes(), maxWalGrowth = input.maxWalGrowthBytes ?? 64 * 1024 * 1024;
|
|
79
|
+
if (!Number.isSafeInteger(maxWalGrowth) || maxWalGrowth < 0)
|
|
80
|
+
throw Error("Invalid WAL growth quota");
|
|
81
|
+
const db = new DatabaseSync(source, { readOnly: true });
|
|
82
|
+
const temporary = join(root, DATABASE + ".partial");
|
|
83
|
+
try {
|
|
84
|
+
db.exec("PRAGMA busy_timeout=10; BEGIN");
|
|
85
|
+
const pages = db.prepare("PRAGMA page_count").get().page_count, pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
86
|
+
if (pages * pageSize > manifest.maxBytes)
|
|
87
|
+
throw Error("Database exceeds backup byte quota");
|
|
88
|
+
await backup(db, temporary, { rate: 128, progress: ({ totalPages, remainingPages }) => { check(); if (walBytes() - initialWalBytes > maxWalGrowth)
|
|
89
|
+
throw Error("Backup WAL growth quota exceeded; source snapshot released"); if (totalPages * pageSize > manifest.maxBytes)
|
|
90
|
+
throw Error("Database exceeds backup byte quota"); input.onProgress?.({ stage: "snapshot", copied: (totalPages - remainingPages) * pageSize }); } });
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (db.isTransaction)
|
|
94
|
+
db.exec("ROLLBACK");
|
|
95
|
+
db.close();
|
|
96
|
+
}
|
|
97
|
+
await syncFile(temporary);
|
|
98
|
+
await rename(temporary, join(root, DATABASE));
|
|
99
|
+
const hash = await digest(join(root, DATABASE), manifest.maxBytes, undefined, input.signal);
|
|
100
|
+
snapshotVerified = true;
|
|
101
|
+
manifest.databaseSha256 = hash.sha256;
|
|
102
|
+
manifest.databaseBytes = hash.bytes;
|
|
103
|
+
manifest.status = "payloads";
|
|
104
|
+
await publishManifest(root, manifest);
|
|
105
|
+
}
|
|
106
|
+
const database = join(root, DATABASE);
|
|
107
|
+
if (!snapshotVerified && (await digest(database, manifest.maxBytes, undefined, input.signal)).sha256 !== manifest.databaseSha256)
|
|
108
|
+
throw Error("Backup snapshot hash changed");
|
|
109
|
+
const db = new DatabaseSync(database, { readOnly: true });
|
|
110
|
+
try {
|
|
111
|
+
if (db.prepare("PRAGMA quick_check").get().quick_check !== "ok")
|
|
112
|
+
throw Error("Backup SQLite integrity check failed");
|
|
113
|
+
// Rebuild the small cursor from the fsynced catalog, including a file published before a crash.
|
|
114
|
+
let count = 0, bytes = 0, cursor = "";
|
|
115
|
+
const catalog = join(root, CATALOG);
|
|
116
|
+
const catalogFile = await open(catalog, "a+", 0o600);
|
|
117
|
+
try {
|
|
118
|
+
const size = (await catalogFile.stat()).size;
|
|
119
|
+
if (size) {
|
|
120
|
+
const tail = Buffer.alloc(Math.min(8192, size));
|
|
121
|
+
await catalogFile.read(tail, 0, tail.length, size - tail.length);
|
|
122
|
+
if (tail.at(-1) !== 10) {
|
|
123
|
+
const newline = tail.lastIndexOf(10);
|
|
124
|
+
if (newline < 0 && size > 8192)
|
|
125
|
+
throw Error("Invalid partial backup catalog");
|
|
126
|
+
await catalogFile.truncate(newline < 0 ? 0 : size - tail.length + newline + 1);
|
|
127
|
+
await catalogFile.sync();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await catalogFile.close();
|
|
133
|
+
}
|
|
134
|
+
for await (const row of catalogRows(catalog, manifest.maxPayloads)) {
|
|
135
|
+
check();
|
|
136
|
+
if (row.id <= cursor)
|
|
137
|
+
throw Error("Backup catalog order is invalid");
|
|
138
|
+
const hash = await digest(ownedPath(join(root, "payloads"), row.path), manifest.maxBytes, undefined, input.signal);
|
|
139
|
+
if (hash.sha256 !== row.storedSha256 || hash.bytes !== row.bytes)
|
|
140
|
+
throw Error("Backup payload changed");
|
|
141
|
+
count++;
|
|
142
|
+
bytes += row.bytes;
|
|
143
|
+
cursor = row.id;
|
|
144
|
+
}
|
|
145
|
+
manifest.payloadCount = count;
|
|
146
|
+
manifest.payloadBytes = bytes;
|
|
147
|
+
manifest.cursor = cursor;
|
|
148
|
+
const hasPayloads = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='payloads'").get());
|
|
149
|
+
while (hasPayloads) {
|
|
150
|
+
check();
|
|
151
|
+
const rows = db.prepare("SELECT id,storage_path,sha256,encoding,byte_size,status FROM payloads WHERE id>? ORDER BY id LIMIT 128").all(manifest.cursor);
|
|
152
|
+
if (!rows.length)
|
|
153
|
+
break;
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
check();
|
|
156
|
+
if (!row.storage_path || row.status !== "committed")
|
|
157
|
+
throw Error("Snapshot contains unavailable payload metadata");
|
|
158
|
+
if (manifest.payloadCount >= manifest.maxPayloads)
|
|
159
|
+
throw Error("Backup payload count quota exceeded");
|
|
160
|
+
const sourcePath = ownedPath(payloadRoot, row.storage_path), target = ownedPath(join(root, "payloads"), row.storage_path), size = (await stat(sourcePath)).size;
|
|
161
|
+
if ((manifest.databaseBytes ?? 0) + manifest.payloadBytes + size > manifest.maxBytes)
|
|
162
|
+
throw Error("Backup byte quota exceeded");
|
|
163
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
164
|
+
const temporary = target + ".partial";
|
|
165
|
+
await pipeline(createReadStream(sourcePath, { highWaterMark: 65536 }), createWriteStream(temporary, { mode: 0o600 }), { signal: input.signal });
|
|
166
|
+
const raw = await digest(temporary, manifest.maxBytes, undefined, input.signal), content = await digest(temporary, row.byte_size, row.encoding, input.signal);
|
|
167
|
+
if (content.sha256 !== row.sha256 || content.bytes !== row.byte_size)
|
|
168
|
+
throw Error("Source payload failed content verification");
|
|
169
|
+
await syncFile(temporary);
|
|
170
|
+
await rename(temporary, target);
|
|
171
|
+
await syncFile(dirname(target));
|
|
172
|
+
const entry = { id: row.id, path: row.storage_path, sha256: row.sha256, storedSha256: raw.sha256, bytes: raw.bytes, encoding: row.encoding, contentBytes: row.byte_size };
|
|
173
|
+
const file = await open(catalog, "a", 0o600);
|
|
174
|
+
try {
|
|
175
|
+
await file.writeFile(JSON.stringify(entry) + "\n");
|
|
176
|
+
await file.sync();
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await file.close();
|
|
180
|
+
}
|
|
181
|
+
manifest.payloadCount++;
|
|
182
|
+
manifest.payloadBytes += raw.bytes;
|
|
183
|
+
manifest.cursor = row.id;
|
|
184
|
+
input.onProgress?.({ stage: "payloads", copied: manifest.payloadCount });
|
|
185
|
+
}
|
|
186
|
+
await publishManifest(root, manifest);
|
|
187
|
+
}
|
|
188
|
+
manifest.catalogSha256 = (await digest(catalog, Math.max(1024, manifest.maxPayloads * 4096), undefined, input.signal)).sha256;
|
|
189
|
+
manifest.status = "complete";
|
|
190
|
+
await publishManifest(root, manifest);
|
|
191
|
+
return manifest;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
db.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function* catalogRows(path, maximum) {
|
|
198
|
+
const stream = createReadStream(path, { highWaterMark: 65536, encoding: "utf8" });
|
|
199
|
+
let count = 0, pending = "";
|
|
200
|
+
const parse = (line) => { if (line.length > 4096 || ++count > maximum)
|
|
201
|
+
throw Error("Backup catalog exceeds quota"); const row = JSON.parse(line); if (typeof row.id !== "string" || typeof row.path !== "string" || !Number.isSafeInteger(row.bytes) || row.bytes < 0)
|
|
202
|
+
throw Error("Invalid backup catalog row"); return row; };
|
|
203
|
+
try {
|
|
204
|
+
for await (const chunk of stream) {
|
|
205
|
+
pending += chunk;
|
|
206
|
+
let newline;
|
|
207
|
+
while ((newline = pending.indexOf("\n")) >= 0) {
|
|
208
|
+
const line = pending.slice(0, newline);
|
|
209
|
+
pending = pending.slice(newline + 1);
|
|
210
|
+
yield parse(line);
|
|
211
|
+
}
|
|
212
|
+
if (pending.length > 4096)
|
|
213
|
+
throw Error("Backup catalog line exceeds quota");
|
|
214
|
+
}
|
|
215
|
+
if (pending)
|
|
216
|
+
throw Error("Incomplete backup catalog line");
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
stream.destroy();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export async function verifyStorageBackup(root, signal) {
|
|
223
|
+
const m = await readStorageBackupManifest(root);
|
|
224
|
+
if (m.status !== "complete")
|
|
225
|
+
throw Error("Backup is incomplete");
|
|
226
|
+
if ((await digest(join(root, DATABASE), m.maxBytes, undefined, signal)).sha256 !== m.databaseSha256 || (await digest(join(root, CATALOG), Math.max(1024, m.maxPayloads * 4096), undefined, signal)).sha256 !== m.catalogSha256)
|
|
227
|
+
throw Error("Backup manifest hash mismatch");
|
|
228
|
+
const db = new DatabaseSync(join(root, DATABASE), { readOnly: true });
|
|
229
|
+
let payloads = 0, bytes = m.databaseBytes ?? 0, previousId = "";
|
|
230
|
+
try {
|
|
231
|
+
if (db.prepare("PRAGMA quick_check").get().quick_check !== "ok")
|
|
232
|
+
throw Error("Backup database failed verification");
|
|
233
|
+
const hasPayloads = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='payloads'").get());
|
|
234
|
+
for await (const row of catalogRows(join(root, CATALOG), m.maxPayloads)) {
|
|
235
|
+
signal?.throwIfAborted();
|
|
236
|
+
if (row.id <= previousId)
|
|
237
|
+
throw Error("Backup catalog order is invalid");
|
|
238
|
+
previousId = row.id;
|
|
239
|
+
const metadata = hasPayloads ? db.prepare("SELECT sha256,storage_path,byte_size FROM payloads WHERE id=?").get(row.id) : undefined;
|
|
240
|
+
if (!metadata || metadata.sha256 !== row.sha256 || metadata.storage_path !== row.path || metadata.byte_size !== row.contentBytes)
|
|
241
|
+
throw Error("Catalog does not match snapshot");
|
|
242
|
+
const path = ownedPath(join(root, "payloads"), row.path);
|
|
243
|
+
const raw = await digest(path, m.maxBytes, undefined, signal), content = await digest(path, row.contentBytes, row.encoding, signal);
|
|
244
|
+
if (raw.sha256 !== row.storedSha256 || raw.bytes !== row.bytes || content.sha256 !== row.sha256 || content.bytes !== row.contentBytes)
|
|
245
|
+
throw Error("Backup payload hash mismatch");
|
|
246
|
+
payloads++;
|
|
247
|
+
bytes += raw.bytes;
|
|
248
|
+
if (bytes > m.maxBytes)
|
|
249
|
+
throw Error("Backup exceeds byte quota");
|
|
250
|
+
}
|
|
251
|
+
const expected = hasPayloads ? Number(db.prepare("SELECT COUNT(*) AS count FROM payloads").get().count) : 0;
|
|
252
|
+
if (payloads !== expected || payloads !== m.payloadCount)
|
|
253
|
+
throw Error("Backup payload coverage mismatch");
|
|
254
|
+
return { payloads, bytes };
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
db.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export async function restoreStorageBackup(root, destination, signal) {
|
|
261
|
+
await verifyStorageBackup(root, signal);
|
|
262
|
+
const m = await readStorageBackupManifest(root), name = basename(m.source);
|
|
263
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(name))
|
|
264
|
+
throw Error("Unsafe restored database name");
|
|
265
|
+
await mkdir(destination, { mode: 0o700 });
|
|
266
|
+
const payloadRoot = join(destination, "payloads"), database = join(destination, name);
|
|
267
|
+
for await (const row of catalogRows(join(root, CATALOG), m.maxPayloads)) {
|
|
268
|
+
signal?.throwIfAborted();
|
|
269
|
+
const target = ownedPath(payloadRoot, row.path);
|
|
270
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
271
|
+
await copyFile(ownedPath(join(root, "payloads"), row.path), target);
|
|
272
|
+
await syncFile(target);
|
|
273
|
+
}
|
|
274
|
+
await copyFile(join(root, DATABASE), database);
|
|
275
|
+
await syncFile(database);
|
|
276
|
+
await syncFile(destination);
|
|
277
|
+
return { database, payloadRoot };
|
|
278
|
+
}
|
|
@@ -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,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() } });
|