immune-brain 3.6.8 → 4.0.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/README.md +11 -4
- package/README.zh-CN.md +10 -3
- package/package.json +3 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7581 -5042
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +27 -25
- package/plugins/immune-brain/dist/imm-planner.md +53 -32
- package/plugins/immune-brain/dist/imm-review-retro.md +123 -0
- package/plugins/immune-brain/dist/registry.yaml +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +3 -1
- package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
- package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
- package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
- package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +254 -29
- package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
- package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
- package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
- package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
- package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
- package/plugins/immune-brain/runtime/plan_core.ts +27 -65
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +3 -1
- package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
- package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
- package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
- package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
- package/plugins/immune-brain/skills/imm-review-retro/SKILL.md +23 -0
- package/plugins/immune-brain/skills/imm-review-retro/scripts/review_retro.ts +355 -0
- package/plugins/immune-brain/skills/registry.yaml +9 -0
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
- package/plugins/immune-brain/bin/imm-retired +0 -4
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
- package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
- package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
- package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel authority store: one worktree-local SQLite database is the single
|
|
3
|
+
* transaction boundary for runs, lifecycle, revisions, findings, attestations
|
|
4
|
+
* and operation outcomes.
|
|
5
|
+
*
|
|
6
|
+
* Layout: `.imm/state/kernel.sqlite` with WAL, `synchronous=FULL`, foreign
|
|
7
|
+
* keys and a bounded busy timeout. `.imm/state/` stays Git-ignored; the
|
|
8
|
+
* database is never committed, and audit evidence is exported separately as
|
|
9
|
+
* tracked, deterministic files.
|
|
10
|
+
*
|
|
11
|
+
* This module is mechanical: it opens/validates the database, runs bounded
|
|
12
|
+
* transactions with revision-checked CAS helpers, and exposes row access.
|
|
13
|
+
* Authority decisions (owner matrix, settlement rules, replay identity) stay
|
|
14
|
+
* in `storage.ts` and the reducer.
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import {
|
|
19
|
+
constants,
|
|
20
|
+
closeSync,
|
|
21
|
+
copyFileSync,
|
|
22
|
+
existsSync,
|
|
23
|
+
fsyncSync,
|
|
24
|
+
lstatSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
openSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
realpathSync,
|
|
29
|
+
statSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
writeFileSync,
|
|
33
|
+
} from "node:fs";
|
|
34
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
35
|
+
import { DatabaseSync } from "node:sqlite";
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
KERNEL_DB_RELATIVE,
|
|
39
|
+
KERNEL_STORE_SCHEMA_VERSION,
|
|
40
|
+
kernelStoreBindingDigest,
|
|
41
|
+
kernelStoreIdentityPath,
|
|
42
|
+
} from "./storage_paths";
|
|
43
|
+
|
|
44
|
+
export { KERNEL_STORE_SCHEMA_VERSION };
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
47
|
+
|
|
48
|
+
/** Schema/version rejection and unsafe-store diagnostics. */
|
|
49
|
+
export class KernelStoreSecurityError extends Error {
|
|
50
|
+
readonly code: string = "kernel_store_security_error";
|
|
51
|
+
|
|
52
|
+
constructor(message: string) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "KernelStoreSecurityError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Revision conflicts, busy stores and unrecoverable transaction failures. */
|
|
59
|
+
export class KernelStoreConflictError extends Error {
|
|
60
|
+
readonly code: string = "kernel_store_conflict";
|
|
61
|
+
|
|
62
|
+
constructor(message: string) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "KernelStoreConflictError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The database exists but its schema version is not the one this runtime owns. */
|
|
69
|
+
export class KernelSchemaError extends KernelStoreSecurityError {
|
|
70
|
+
readonly code = "kernel_schema_error";
|
|
71
|
+
|
|
72
|
+
constructor(message: string) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "KernelSchemaError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type RunLifecycleState = "active" | "done" | "stopped";
|
|
79
|
+
export type RunClaimStatus = "active" | "draining";
|
|
80
|
+
|
|
81
|
+
export interface KernelWorkspaceRow {
|
|
82
|
+
revision: number;
|
|
83
|
+
current_run_id: string | null;
|
|
84
|
+
updated_at: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface KernelRunRow {
|
|
88
|
+
run_id: string;
|
|
89
|
+
task_id: string;
|
|
90
|
+
state: RunLifecycleState;
|
|
91
|
+
revision: number;
|
|
92
|
+
record_json: string;
|
|
93
|
+
intent_revision: number;
|
|
94
|
+
intent_content_hash: string;
|
|
95
|
+
enrollment_event_id: string;
|
|
96
|
+
claim_status: RunClaimStatus | null;
|
|
97
|
+
created_at: string;
|
|
98
|
+
updated_at: string;
|
|
99
|
+
terminal_proof_json: string | null;
|
|
100
|
+
terminal_at: string | null;
|
|
101
|
+
audit_exported_at: string | null;
|
|
102
|
+
pending_relocations_json: string | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface KernelOperationRow {
|
|
106
|
+
operation_id: string;
|
|
107
|
+
kind: string;
|
|
108
|
+
run_id: string | null;
|
|
109
|
+
result_json: string;
|
|
110
|
+
committed_at: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface KernelJournalRow {
|
|
114
|
+
seq: number;
|
|
115
|
+
entry_json: string;
|
|
116
|
+
task_id: string | null;
|
|
117
|
+
observation_commit_id: string | null;
|
|
118
|
+
observation_id: string | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface KernelRunInsert {
|
|
122
|
+
run_id: string;
|
|
123
|
+
task_id: string;
|
|
124
|
+
record_json: string;
|
|
125
|
+
intent_revision: number;
|
|
126
|
+
intent_content_hash: string;
|
|
127
|
+
enrollment_event_id: string;
|
|
128
|
+
claim_status: RunClaimStatus;
|
|
129
|
+
created_at: string;
|
|
130
|
+
updated_at: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface OpenStore {
|
|
134
|
+
db: DatabaseSync;
|
|
135
|
+
depth: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const openStores = new Map<string, OpenStore>();
|
|
139
|
+
|
|
140
|
+
let storeFaultForTest: (() => void) | null = null;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Test-only seam: runs after every authority write of the current transaction
|
|
144
|
+
* and immediately before COMMIT, so a thrown fault must roll the transaction
|
|
145
|
+
* back completely.
|
|
146
|
+
*/
|
|
147
|
+
export function setStoreFaultForTest(hook: (() => void) | null): void {
|
|
148
|
+
storeFaultForTest = hook;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function runStoreFault(): void {
|
|
152
|
+
const hook = storeFaultForTest;
|
|
153
|
+
storeFaultForTest = null;
|
|
154
|
+
hook?.();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function canonicalRoot(root: string): string {
|
|
158
|
+
try {
|
|
159
|
+
return realpathSync(root);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new KernelStoreSecurityError("project root is unavailable");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function storeKey(root: string): string {
|
|
166
|
+
return canonicalRoot(root);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function kernelStorePath(root: string): string {
|
|
170
|
+
return resolve(canonicalRoot(root), KERNEL_DB_RELATIVE);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function assertSafeSegments(canonical: string, candidate: string): void {
|
|
174
|
+
let current = canonical;
|
|
175
|
+
for (const segment of relative(canonical, candidate).split(sep).filter(Boolean)) {
|
|
176
|
+
current = resolve(current, segment);
|
|
177
|
+
let stat;
|
|
178
|
+
try {
|
|
179
|
+
stat = lstatSync(current);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
182
|
+
if (code === "ENOENT" || code === "ENOTDIR") continue;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
if (stat.isSymbolicLink())
|
|
186
|
+
throw new KernelStoreSecurityError(
|
|
187
|
+
`symlink storage segment is forbidden: ${relative(canonical, current)}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Reject a store path that is not a regular file inside the worktree. */
|
|
193
|
+
function assertSafeStoreTarget(canonical: string, target: string): void {
|
|
194
|
+
assertSafeSegments(canonical, target);
|
|
195
|
+
try {
|
|
196
|
+
const stat = lstatSync(target);
|
|
197
|
+
if (stat.isSymbolicLink())
|
|
198
|
+
throw new KernelStoreSecurityError(
|
|
199
|
+
`symlink storage segment is forbidden: ${relative(canonical, target)}`,
|
|
200
|
+
);
|
|
201
|
+
if (!stat.isFile())
|
|
202
|
+
throw new KernelStoreSecurityError("kernel store is not a regular file");
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ensureStoreDirectory(canonical: string): void {
|
|
209
|
+
const target = resolve(canonical, KERNEL_DB_RELATIVE);
|
|
210
|
+
const directory = dirname(target);
|
|
211
|
+
assertSafeSegments(canonical, directory);
|
|
212
|
+
let current = canonical;
|
|
213
|
+
for (const segment of relative(canonical, directory).split(sep).filter(Boolean)) {
|
|
214
|
+
current = resolve(current, segment);
|
|
215
|
+
let stat;
|
|
216
|
+
try {
|
|
217
|
+
stat = lstatSync(current);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
220
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
221
|
+
stat = null;
|
|
222
|
+
}
|
|
223
|
+
if (!stat) {
|
|
224
|
+
mkdirSync(current);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (stat.isSymbolicLink())
|
|
228
|
+
throw new KernelStoreSecurityError(
|
|
229
|
+
`symlink storage segment is forbidden: ${relative(canonical, current)}`,
|
|
230
|
+
);
|
|
231
|
+
if (!stat.isDirectory())
|
|
232
|
+
throw new KernelStoreSecurityError(
|
|
233
|
+
`storage segment is not a directory: ${relative(canonical, current)}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
assertSafeStoreTarget(canonical, target);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function kernelStoreExists(root: string): boolean {
|
|
240
|
+
return existsSync(kernelStorePath(root));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const SCHEMA_SQL = `
|
|
244
|
+
CREATE TABLE IF NOT EXISTS store_meta (
|
|
245
|
+
key TEXT PRIMARY KEY,
|
|
246
|
+
value TEXT NOT NULL
|
|
247
|
+
);
|
|
248
|
+
CREATE TABLE IF NOT EXISTS workspace (
|
|
249
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
250
|
+
revision INTEGER NOT NULL,
|
|
251
|
+
current_run_id TEXT,
|
|
252
|
+
updated_at TEXT NOT NULL
|
|
253
|
+
);
|
|
254
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
255
|
+
run_id TEXT PRIMARY KEY,
|
|
256
|
+
task_id TEXT NOT NULL UNIQUE,
|
|
257
|
+
state TEXT NOT NULL CHECK (state IN ('active','done','stopped')),
|
|
258
|
+
revision INTEGER NOT NULL,
|
|
259
|
+
record_json TEXT NOT NULL,
|
|
260
|
+
intent_revision INTEGER NOT NULL,
|
|
261
|
+
intent_content_hash TEXT NOT NULL,
|
|
262
|
+
enrollment_event_id TEXT NOT NULL,
|
|
263
|
+
claim_status TEXT CHECK (claim_status IN ('active','draining')),
|
|
264
|
+
created_at TEXT NOT NULL,
|
|
265
|
+
updated_at TEXT NOT NULL,
|
|
266
|
+
terminal_proof_json TEXT,
|
|
267
|
+
terminal_at TEXT,
|
|
268
|
+
audit_exported_at TEXT,
|
|
269
|
+
pending_relocations_json TEXT
|
|
270
|
+
);
|
|
271
|
+
CREATE UNIQUE INDEX IF NOT EXISTS runs_single_active ON runs((1)) WHERE state = 'active';
|
|
272
|
+
CREATE INDEX IF NOT EXISTS runs_state ON runs(state);
|
|
273
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
274
|
+
operation_id TEXT PRIMARY KEY,
|
|
275
|
+
kind TEXT NOT NULL,
|
|
276
|
+
run_id TEXT,
|
|
277
|
+
result_json TEXT NOT NULL,
|
|
278
|
+
committed_at TEXT NOT NULL
|
|
279
|
+
);
|
|
280
|
+
CREATE TABLE IF NOT EXISTS journal (
|
|
281
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
282
|
+
entry_json TEXT NOT NULL,
|
|
283
|
+
task_id TEXT,
|
|
284
|
+
observation_commit_id TEXT,
|
|
285
|
+
observation_id TEXT
|
|
286
|
+
);
|
|
287
|
+
CREATE UNIQUE INDEX IF NOT EXISTS journal_observation_commit ON journal(observation_commit_id) WHERE observation_commit_id IS NOT NULL;
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
function workspaceBinding(root: string, workspaceId: string): string {
|
|
291
|
+
return kernelStoreBindingDigest(root, workspaceId);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function gitCommonDir(root: string): string | null {
|
|
295
|
+
const result = spawnSync("git", ["-C", root, "rev-parse", "--git-common-dir"], {
|
|
296
|
+
encoding: "utf8",
|
|
297
|
+
});
|
|
298
|
+
if (result.status !== 0) return null;
|
|
299
|
+
const value = result.stdout.trim();
|
|
300
|
+
if (!value) return null;
|
|
301
|
+
return isAbsolute(value) ? value : resolve(root, value);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readMeta(db: DatabaseSync, key: string): string | null {
|
|
305
|
+
const row = db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key) as
|
|
306
|
+
| { value?: unknown }
|
|
307
|
+
| undefined;
|
|
308
|
+
return row && typeof row.value === "string" ? row.value : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function writeMeta(db: DatabaseSync, key: string, value: string): void {
|
|
312
|
+
db.prepare(
|
|
313
|
+
"INSERT INTO store_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
314
|
+
).run(key, value);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function applyPragmas(db: DatabaseSync, busyTimeoutMs: number, writable: boolean): void {
|
|
318
|
+
// Read-only accessors must not switch journal mode or checkpoint: a read
|
|
319
|
+
// never changes the store bytes it observes.
|
|
320
|
+
if (writable) {
|
|
321
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
322
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
323
|
+
}
|
|
324
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
325
|
+
db.exec(`PRAGMA busy_timeout = ${Math.max(1, Math.trunc(busyTimeoutMs))}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function assertSchema(db: DatabaseSync, root: string): void {
|
|
329
|
+
const rows = db
|
|
330
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
331
|
+
.all() as Array<{ name?: unknown }>;
|
|
332
|
+
const tables = new Set(rows.map((row) => String(row.name)));
|
|
333
|
+
if (tables.size === 0) {
|
|
334
|
+
// A tableless file is either a brand-new store (created by the lock or by
|
|
335
|
+
// this open) or a store that lost its contents. The creation marker tells
|
|
336
|
+
// them apart: an existing identity means this worktree had a store, so
|
|
337
|
+
// re-initializing would silently replace real authority with an empty
|
|
338
|
+
// workspace. Fail closed and name the recovery.
|
|
339
|
+
if (existsSync(resolve(root, kernelStoreIdentityPath())))
|
|
340
|
+
throw new KernelStoreSecurityError(
|
|
341
|
+
"kernel store exists but carries no schema; restore .imm/state/kernel.sqlite from backup or remove the state directory deliberately to start a new worktree identity",
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!tables.has("store_meta") || !tables.has("runs") || !tables.has("workspace")) {
|
|
346
|
+
throw new KernelSchemaError(
|
|
347
|
+
"kernel store schema is not recognizable; the database was not created by this runtime",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const version = readMeta(db, "schema_version");
|
|
351
|
+
if (version !== String(KERNEL_STORE_SCHEMA_VERSION)) {
|
|
352
|
+
throw new KernelSchemaError(
|
|
353
|
+
`kernel store schema version ${version ?? "missing"} is incompatible with this runtime (${KERNEL_STORE_SCHEMA_VERSION}); run the supported migration before mutating`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const workspaceId = readMeta(db, "workspace_id");
|
|
357
|
+
const binding = readMeta(db, "workspace_binding");
|
|
358
|
+
if (!workspaceId || !binding)
|
|
359
|
+
throw new KernelSchemaError("kernel store identity metadata is missing");
|
|
360
|
+
if (workspaceBinding(root, workspaceId) !== binding)
|
|
361
|
+
throw new KernelStoreSecurityError(
|
|
362
|
+
"kernel store belongs to a different worktree; restore it into its binding worktree or run the supported rebinding",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Record that this worktree has a store. Written once, at first initialization:
|
|
368
|
+
* it is the only surviving evidence that a later tableless database is a
|
|
369
|
+
* damaged store rather than a new one.
|
|
370
|
+
*/
|
|
371
|
+
function writeStoreIdentity(root: string): void {
|
|
372
|
+
const path = resolve(root, kernelStoreIdentityPath());
|
|
373
|
+
if (existsSync(path)) return;
|
|
374
|
+
// Durable creation: the marker is the only evidence that a later tableless
|
|
375
|
+
// database is a damaged store rather than a new one.
|
|
376
|
+
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
|
|
377
|
+
try {
|
|
378
|
+
writeFileSync(fd, `${JSON.stringify({ contract: "assurance_kernel/store_identity/v1", created_at: new Date().toISOString() }, null, 2)}\n`);
|
|
379
|
+
fsyncSync(fd);
|
|
380
|
+
} finally {
|
|
381
|
+
closeSync(fd);
|
|
382
|
+
}
|
|
383
|
+
const directory = openSync(dirname(path), constants.O_RDONLY);
|
|
384
|
+
try {
|
|
385
|
+
fsyncSync(directory);
|
|
386
|
+
} finally {
|
|
387
|
+
closeSync(directory);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Ensure the store identity marker exists; idempotent and durable. */
|
|
392
|
+
export function ensureStoreIdentity(root: string): void {
|
|
393
|
+
assertSafeStoreTarget(canonicalRoot(root), resolve(root, kernelStoreIdentityPath()));
|
|
394
|
+
writeStoreIdentity(canonicalRoot(root));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function initializeSchema(db: DatabaseSync, root: string, now: string): void {
|
|
398
|
+
db.exec("BEGIN IMMEDIATE");
|
|
399
|
+
try {
|
|
400
|
+
db.exec(SCHEMA_SQL);
|
|
401
|
+
const workspaceId = readMeta(db, "workspace_id") ?? randomUUID();
|
|
402
|
+
writeMeta(db, "workspace_id", workspaceId);
|
|
403
|
+
writeMeta(db, "schema_version", String(KERNEL_STORE_SCHEMA_VERSION));
|
|
404
|
+
writeMeta(db, "workspace_binding", workspaceBinding(root, workspaceId));
|
|
405
|
+
writeMeta(db, "created_at", readMeta(db, "created_at") ?? now);
|
|
406
|
+
const common = gitCommonDir(root);
|
|
407
|
+
if (common) writeMeta(db, "git_common_dir", common);
|
|
408
|
+
db.prepare(
|
|
409
|
+
"INSERT INTO workspace (id, revision, current_run_id, updated_at) VALUES (1, 0, NULL, ?) ON CONFLICT(id) DO NOTHING",
|
|
410
|
+
).run(now);
|
|
411
|
+
db.exec("COMMIT");
|
|
412
|
+
} catch (error) {
|
|
413
|
+
try {
|
|
414
|
+
db.exec("ROLLBACK");
|
|
415
|
+
} catch {
|
|
416
|
+
// The transaction is already closed.
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function fsyncPath(path: string): void {
|
|
423
|
+
const fd = openSync(path, constants.O_RDONLY);
|
|
424
|
+
try {
|
|
425
|
+
fsyncSync(fd);
|
|
426
|
+
} finally {
|
|
427
|
+
closeSync(fd);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function fsyncDirectory(path: string): void {
|
|
432
|
+
const fd = openSync(path, constants.O_RDONLY);
|
|
433
|
+
try {
|
|
434
|
+
fsyncSync(fd);
|
|
435
|
+
} finally {
|
|
436
|
+
closeSync(fd);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Create one fully initialized store file at an explicit in-worktree path.
|
|
442
|
+
*
|
|
443
|
+
* The explicit legacy importer builds its candidate store here — under
|
|
444
|
+
* `.imm/state/`, never at the canonical path — verifies it, and only then
|
|
445
|
+
* publishes it. Initialization runs the same schema, meta and workspace binding
|
|
446
|
+
* as a first normal open, so a published migration store passes `assertSchema`.
|
|
447
|
+
*/
|
|
448
|
+
export function createMigrationStoreFile(root: string, targetPath: string, now: string): void {
|
|
449
|
+
const canonical = canonicalRoot(root);
|
|
450
|
+
const resolved = resolve(targetPath);
|
|
451
|
+
assertSafeStoreTarget(canonical, resolved);
|
|
452
|
+
assertSafeSegments(canonical, resolved);
|
|
453
|
+
ensureStoreDirectory(canonical);
|
|
454
|
+
if (existsSync(resolved)) throw new KernelStoreSecurityError(`migration store already exists at ${targetPath}`);
|
|
455
|
+
const db = new DatabaseSync(resolved);
|
|
456
|
+
try {
|
|
457
|
+
applyPragmas(db, DEFAULT_BUSY_TIMEOUT_MS, true);
|
|
458
|
+
initializeSchema(db, canonical, now);
|
|
459
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
460
|
+
// Leave the candidate in rollback-journal mode: a WAL sidecar would not
|
|
461
|
+
// travel with the publication rename, and the first runtime open converts
|
|
462
|
+
// the store back to WAL.
|
|
463
|
+
db.exec("PRAGMA journal_mode=DELETE");
|
|
464
|
+
} finally {
|
|
465
|
+
db.close();
|
|
466
|
+
}
|
|
467
|
+
fsyncPath(resolved);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Verify one store file against this worktree's schema and binding.
|
|
472
|
+
*
|
|
473
|
+
* The importer runs this on its candidate before publication, so a database
|
|
474
|
+
* built for another worktree, or by an incompatible runtime, can never replace
|
|
475
|
+
* the authority of this one.
|
|
476
|
+
*/
|
|
477
|
+
export function verifyStoreFile(root: string, path: string): void {
|
|
478
|
+
const canonical = canonicalRoot(root);
|
|
479
|
+
const resolved = resolve(path);
|
|
480
|
+
assertSafeStoreTarget(canonical, resolved);
|
|
481
|
+
const db = openStoreFile(canonical, resolved, { readOnly: true });
|
|
482
|
+
if (!db) throw new KernelStoreSecurityError(`store file is missing: ${path}`);
|
|
483
|
+
db.close();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Classify a store file's identity without asserting anything about its rows.
|
|
488
|
+
*
|
|
489
|
+
* The importer needs to tell three states apart: a file whose schema never
|
|
490
|
+
* committed (safe to rebuild), a file that belongs to another worktree (never
|
|
491
|
+
* ours), and a file that is bound to this worktree (its content is the only
|
|
492
|
+
* question left).
|
|
493
|
+
*/
|
|
494
|
+
export function classifyStoreFile(root: string, path: string): "uninitialized" | "foreign" | "binding_ok" {
|
|
495
|
+
const canonical = canonicalRoot(root);
|
|
496
|
+
const resolved = resolve(path);
|
|
497
|
+
assertSafeStoreTarget(canonical, resolved);
|
|
498
|
+
let db: DatabaseSync;
|
|
499
|
+
try {
|
|
500
|
+
db = new DatabaseSync(resolved, { readOnly: true });
|
|
501
|
+
} catch {
|
|
502
|
+
return "uninitialized";
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all() as Array<{ name?: unknown }>;
|
|
506
|
+
if (!new Set(rows.map((row) => String(row.name))).has("store_meta")) return "uninitialized";
|
|
507
|
+
const workspaceId = readMeta(db, "workspace_id");
|
|
508
|
+
const binding = readMeta(db, "workspace_binding");
|
|
509
|
+
if (!workspaceId || !binding) return "uninitialized";
|
|
510
|
+
return workspaceBinding(canonical, workspaceId) === binding ? "binding_ok" : "foreign";
|
|
511
|
+
} catch {
|
|
512
|
+
return "uninitialized";
|
|
513
|
+
} finally {
|
|
514
|
+
db.close();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Publish a verified migration store at the canonical path.
|
|
520
|
+
*
|
|
521
|
+
* The rename is the publication point: a crash before it leaves the canonical
|
|
522
|
+
* store absent, and a crash after it leaves a complete store, so no reader can
|
|
523
|
+
* ever observe a half-imported database. Nothing is written to the canonical
|
|
524
|
+
* path when it already exists.
|
|
525
|
+
*/
|
|
526
|
+
export function publishMigrationStoreFile(root: string, sourcePath: string): void {
|
|
527
|
+
const canonical = canonicalRoot(root);
|
|
528
|
+
const target = resolve(canonical, KERNEL_DB_RELATIVE);
|
|
529
|
+
const source = resolve(sourcePath);
|
|
530
|
+
assertSafeStoreTarget(canonical, source);
|
|
531
|
+
assertSafeSegments(canonical, source);
|
|
532
|
+
if (existsSync(target)) throw new KernelStoreConflictError("kernel store already exists; refusing to overwrite authority");
|
|
533
|
+
// The candidate must be self-contained: a WAL beside the renamed database
|
|
534
|
+
// would make the published store unreadable, so only empty sidecars may
|
|
535
|
+
// remain and they are removed before publication.
|
|
536
|
+
for (const sidecar of [`${source}-wal`, `${source}-shm`]) {
|
|
537
|
+
if (!existsSync(sidecar)) continue;
|
|
538
|
+
if (statSync(sidecar).size > 0)
|
|
539
|
+
throw new KernelStoreConflictError("migration store still carries a write-ahead log; checkpoint it before publishing");
|
|
540
|
+
rmSync(sidecar, { force: true });
|
|
541
|
+
}
|
|
542
|
+
fsyncPath(source);
|
|
543
|
+
renameSync(source, target);
|
|
544
|
+
fsyncDirectory(dirname(target));
|
|
545
|
+
writeStoreIdentity(canonical);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Open the worktree authority store. `create: false` returns null when the
|
|
550
|
+
* database does not exist yet, so read-only paths never create one.
|
|
551
|
+
*/
|
|
552
|
+
export function openKernelStore(
|
|
553
|
+
root: string,
|
|
554
|
+
options: { create?: boolean; busyTimeoutMs?: number; now?: string; readOnly?: boolean } = {},
|
|
555
|
+
): DatabaseSync | null {
|
|
556
|
+
const canonical = canonicalRoot(root);
|
|
557
|
+
const path = resolve(canonical, KERNEL_DB_RELATIVE);
|
|
558
|
+
const create = options.create ?? true;
|
|
559
|
+
if (!existsSync(path) && !create) return null;
|
|
560
|
+
// Safety validation applies to reads as well: following a symlinked store
|
|
561
|
+
// would read (or create) authority outside the worktree.
|
|
562
|
+
assertSafeStoreTarget(canonical, path);
|
|
563
|
+
if (!options.readOnly) ensureStoreDirectory(canonical);
|
|
564
|
+
return openStoreFile(canonical, path, options);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Open and validate one store file against a worktree binding. `assertSchema`
|
|
569
|
+
* runs before the caller can read or write anything, which is what lets a
|
|
570
|
+
* restore validate a backup copy in place without touching the live store.
|
|
571
|
+
*/
|
|
572
|
+
function openStoreFile(
|
|
573
|
+
canonical: string,
|
|
574
|
+
path: string,
|
|
575
|
+
options: { busyTimeoutMs?: number; now?: string; readOnly?: boolean } = {},
|
|
576
|
+
): DatabaseSync | null {
|
|
577
|
+
let db: DatabaseSync;
|
|
578
|
+
try {
|
|
579
|
+
db = new DatabaseSync(path, options.readOnly ? { readOnly: true } : {});
|
|
580
|
+
} catch (error) {
|
|
581
|
+
throw new KernelStoreSecurityError(
|
|
582
|
+
`kernel store could not be opened at ${KERNEL_DB_RELATIVE}: ${error instanceof Error ? error.message : String(error)}`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
applyPragmas(db, options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS, !options.readOnly);
|
|
587
|
+
assertSchema(db, canonical);
|
|
588
|
+
const initialized =
|
|
589
|
+
(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='store_meta'").get() as
|
|
590
|
+
| { name?: unknown }
|
|
591
|
+
| undefined) !== undefined;
|
|
592
|
+
if (!initialized) {
|
|
593
|
+
if (options.readOnly) {
|
|
594
|
+
// A read against a store that has no schema yet: there is nothing to
|
|
595
|
+
// read and nothing to create, so report "no store" instead of
|
|
596
|
+
// inventing one through a write from a read path.
|
|
597
|
+
db.close();
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
initializeSchema(db, canonical, options.now ?? new Date().toISOString());
|
|
601
|
+
writeStoreIdentity(canonical);
|
|
602
|
+
}
|
|
603
|
+
return db;
|
|
604
|
+
} catch (error) {
|
|
605
|
+
try {
|
|
606
|
+
db.close();
|
|
607
|
+
} catch {
|
|
608
|
+
// nothing to close
|
|
609
|
+
}
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function closeQuietly(db: DatabaseSync): void {
|
|
615
|
+
try {
|
|
616
|
+
db.close();
|
|
617
|
+
} catch {
|
|
618
|
+
// Already closed.
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Run `operation` inside one bounded, revision-checked write transaction.
|
|
624
|
+
* Nested calls in the same process reuse the open connection, so authority
|
|
625
|
+
* helpers that assume an already-held lock stay composable. Cross-process
|
|
626
|
+
* writers are serialized by SQLite's write lock and the bounded busy timeout.
|
|
627
|
+
*/
|
|
628
|
+
export function withKernelTransaction<T>(
|
|
629
|
+
root: string,
|
|
630
|
+
operation: (db: DatabaseSync) => T,
|
|
631
|
+
options: { busyTimeoutMs?: number; now?: string } = {},
|
|
632
|
+
): T {
|
|
633
|
+
const key = storeKey(root);
|
|
634
|
+
const existing = openStores.get(key);
|
|
635
|
+
if (existing) {
|
|
636
|
+
existing.depth += 1;
|
|
637
|
+
try {
|
|
638
|
+
return operation(existing.db);
|
|
639
|
+
} finally {
|
|
640
|
+
existing.depth -= 1;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
let db: DatabaseSync | null = null;
|
|
644
|
+
try {
|
|
645
|
+
db = openKernelStore(root, options);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (error instanceof Error && error.message.includes("database is locked"))
|
|
648
|
+
throw new KernelStoreConflictError(
|
|
649
|
+
"kernel store is busy: another writer holds the store lock",
|
|
650
|
+
);
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
if (!db) throw new KernelStoreSecurityError("kernel store could not be opened");
|
|
654
|
+
const handle: OpenStore = { db, depth: 1 };
|
|
655
|
+
openStores.set(key, handle);
|
|
656
|
+
try {
|
|
657
|
+
try {
|
|
658
|
+
db.exec("BEGIN IMMEDIATE");
|
|
659
|
+
} catch (error) {
|
|
660
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
661
|
+
if (message.includes("locked") || message.includes("busy"))
|
|
662
|
+
throw new KernelStoreConflictError(
|
|
663
|
+
"kernel store is busy: another writer holds the store lock",
|
|
664
|
+
);
|
|
665
|
+
throw new KernelStoreConflictError(`kernel store transaction could not start: ${message}`);
|
|
666
|
+
}
|
|
667
|
+
let result: T;
|
|
668
|
+
try {
|
|
669
|
+
result = operation(db);
|
|
670
|
+
// The fault seam runs after the authority writes and before COMMIT,
|
|
671
|
+
// so a thrown fault must roll the whole transaction back.
|
|
672
|
+
runStoreFault();
|
|
673
|
+
db.exec("COMMIT");
|
|
674
|
+
} catch (error) {
|
|
675
|
+
try {
|
|
676
|
+
db.exec("ROLLBACK");
|
|
677
|
+
} catch {
|
|
678
|
+
// A failed rollback leaves recovery to SQLite's own journal.
|
|
679
|
+
}
|
|
680
|
+
if (error instanceof Error && error.message.startsWith("kernel store transaction failed to commit"))
|
|
681
|
+
throw error;
|
|
682
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
683
|
+
if (message.includes("SQLITE_BUSY") || message.includes("database is locked"))
|
|
684
|
+
throw new KernelStoreConflictError(
|
|
685
|
+
`kernel store transaction failed to commit: ${message}`,
|
|
686
|
+
);
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
} finally {
|
|
691
|
+
openStores.delete(key);
|
|
692
|
+
closeQuietly(db);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Run a read-only operation against the store when it exists. */
|
|
697
|
+
export function withKernelRead<T>(
|
|
698
|
+
root: string,
|
|
699
|
+
operation: (db: DatabaseSync) => T,
|
|
700
|
+
options: { busyTimeoutMs?: number } = {},
|
|
701
|
+
): T | null {
|
|
702
|
+
const existing = openStores.get(storeKey(root));
|
|
703
|
+
if (existing) return operation(existing.db);
|
|
704
|
+
let db: DatabaseSync | null;
|
|
705
|
+
try {
|
|
706
|
+
db = openKernelStore(root, { ...options, create: false, readOnly: true });
|
|
707
|
+
} catch (error) {
|
|
708
|
+
// A cleanly closed WAL database keeps no `-shm` file, and a read-only
|
|
709
|
+
// connection is not allowed to rebuild it. Fall back to a plain
|
|
710
|
+
// connection: reading still never changes authority, and SQLite only
|
|
711
|
+
// materializes its own journal state.
|
|
712
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
713
|
+
if (!/unable to open database file|CANTOPEN|readonly/i.test(message)) throw error;
|
|
714
|
+
db = openKernelStore(root, { ...options, create: false });
|
|
715
|
+
}
|
|
716
|
+
if (!db) return null;
|
|
717
|
+
try {
|
|
718
|
+
return operation(db);
|
|
719
|
+
} finally {
|
|
720
|
+
closeQuietly(db);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export function activeRunId(db: DatabaseSync): string | null {
|
|
725
|
+
const row = db.prepare("SELECT current_run_id FROM workspace WHERE id = 1").get() as
|
|
726
|
+
| { current_run_id?: unknown }
|
|
727
|
+
| undefined;
|
|
728
|
+
const value = row?.current_run_id;
|
|
729
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function readWorkspaceRow(db: DatabaseSync): KernelWorkspaceRow {
|
|
733
|
+
const row = db
|
|
734
|
+
.prepare("SELECT revision, current_run_id, updated_at FROM workspace WHERE id = 1")
|
|
735
|
+
.get() as { revision?: unknown; current_run_id?: unknown; updated_at?: unknown } | undefined;
|
|
736
|
+
if (!row) throw new KernelStoreSecurityError("kernel store workspace row is missing");
|
|
737
|
+
return {
|
|
738
|
+
revision: Number(row.revision),
|
|
739
|
+
current_run_id:
|
|
740
|
+
typeof row.current_run_id === "string" && row.current_run_id.length > 0
|
|
741
|
+
? row.current_run_id
|
|
742
|
+
: null,
|
|
743
|
+
updated_at: String(row.updated_at ?? ""),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** CAS workspace: `expectedRevision` must equal the stored revision. */
|
|
748
|
+
export function writeWorkspaceRow(
|
|
749
|
+
db: DatabaseSync,
|
|
750
|
+
expectedRevision: number,
|
|
751
|
+
currentRunId: string | null,
|
|
752
|
+
updatedAt: string,
|
|
753
|
+
): number {
|
|
754
|
+
const result = db
|
|
755
|
+
.prepare(
|
|
756
|
+
"UPDATE workspace SET revision = revision + 1, current_run_id = ?, updated_at = ? WHERE id = 1 AND revision = ?",
|
|
757
|
+
)
|
|
758
|
+
.run(currentRunId, updatedAt, expectedRevision);
|
|
759
|
+
if (Number(result.changes) !== 1)
|
|
760
|
+
throw new KernelStoreConflictError(
|
|
761
|
+
`CAS mismatch for workspace: expected revision ${expectedRevision}`,
|
|
762
|
+
);
|
|
763
|
+
return expectedRevision + 1;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function mapRunRow(raw: Record<string, unknown>): KernelRunRow {
|
|
767
|
+
return {
|
|
768
|
+
run_id: String(raw.run_id),
|
|
769
|
+
task_id: String(raw.task_id),
|
|
770
|
+
state: raw.state as RunLifecycleState,
|
|
771
|
+
revision: Number(raw.revision),
|
|
772
|
+
record_json: String(raw.record_json),
|
|
773
|
+
intent_revision: Number(raw.intent_revision),
|
|
774
|
+
intent_content_hash: String(raw.intent_content_hash),
|
|
775
|
+
enrollment_event_id: String(raw.enrollment_event_id),
|
|
776
|
+
claim_status: (raw.claim_status ?? null) as RunClaimStatus | null,
|
|
777
|
+
created_at: String(raw.created_at),
|
|
778
|
+
updated_at: String(raw.updated_at),
|
|
779
|
+
terminal_proof_json:
|
|
780
|
+
typeof raw.terminal_proof_json === "string" ? raw.terminal_proof_json : null,
|
|
781
|
+
terminal_at: typeof raw.terminal_at === "string" ? raw.terminal_at : null,
|
|
782
|
+
audit_exported_at:
|
|
783
|
+
typeof raw.audit_exported_at === "string" ? raw.audit_exported_at : null,
|
|
784
|
+
pending_relocations_json:
|
|
785
|
+
typeof raw.pending_relocations_json === "string" ? raw.pending_relocations_json : null,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export function readRunRowByTask(db: DatabaseSync, taskId: string): KernelRunRow | null {
|
|
790
|
+
const row = db.prepare("SELECT * FROM runs WHERE task_id = ?").get(taskId) as
|
|
791
|
+
| Record<string, unknown>
|
|
792
|
+
| undefined;
|
|
793
|
+
return row ? mapRunRow(row) : null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function readRunRowById(db: DatabaseSync, runId: string): KernelRunRow | null {
|
|
797
|
+
const row = db.prepare("SELECT * FROM runs WHERE run_id = ?").get(runId) as
|
|
798
|
+
| Record<string, unknown>
|
|
799
|
+
| undefined;
|
|
800
|
+
return row ? mapRunRow(row) : null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function readActiveRun(db: DatabaseSync): KernelRunRow | null {
|
|
804
|
+
const row = db.prepare("SELECT * FROM runs WHERE state = 'active'").get() as
|
|
805
|
+
| Record<string, unknown>
|
|
806
|
+
| undefined;
|
|
807
|
+
return row ? mapRunRow(row) : null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function listTerminalRuns(db: DatabaseSync): KernelRunRow[] {
|
|
811
|
+
const rows = db
|
|
812
|
+
.prepare("SELECT * FROM runs WHERE state <> 'active' ORDER BY updated_at, task_id")
|
|
813
|
+
.all() as Array<Record<string, unknown>>;
|
|
814
|
+
return rows.map(mapRunRow);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function listPendingAuditExports(db: DatabaseSync): KernelRunRow[] {
|
|
818
|
+
const rows = db
|
|
819
|
+
.prepare(
|
|
820
|
+
"SELECT * FROM runs WHERE state <> 'active' AND terminal_proof_json IS NOT NULL AND audit_exported_at IS NULL ORDER BY updated_at, task_id",
|
|
821
|
+
)
|
|
822
|
+
.all() as Array<Record<string, unknown>>;
|
|
823
|
+
return rows.map(mapRunRow);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function insertRunRow(db: DatabaseSync, run: KernelRunInsert): KernelRunRow {
|
|
827
|
+
try {
|
|
828
|
+
db.prepare(
|
|
829
|
+
`INSERT INTO runs (run_id, task_id, state, revision, record_json, intent_revision, intent_content_hash, enrollment_event_id, claim_status, created_at, updated_at, terminal_proof_json, terminal_at, audit_exported_at, pending_relocations_json)
|
|
830
|
+
VALUES (?, ?, 'active', 1, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL)`,
|
|
831
|
+
).run(
|
|
832
|
+
run.run_id,
|
|
833
|
+
run.task_id,
|
|
834
|
+
run.record_json,
|
|
835
|
+
run.intent_revision,
|
|
836
|
+
run.intent_content_hash,
|
|
837
|
+
run.enrollment_event_id,
|
|
838
|
+
run.claim_status,
|
|
839
|
+
run.created_at,
|
|
840
|
+
run.updated_at,
|
|
841
|
+
);
|
|
842
|
+
} catch (error) {
|
|
843
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
844
|
+
if (message.includes("runs_single_active") || message.includes("runs.task_id"))
|
|
845
|
+
throw new KernelStoreConflictError(
|
|
846
|
+
`kernel store refused a second active run: ${message}`,
|
|
847
|
+
);
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
const inserted = readRunRowById(db, run.run_id);
|
|
851
|
+
if (!inserted) throw new KernelStoreSecurityError("kernel store run insert did not converge");
|
|
852
|
+
return inserted;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** CAS a record update on one exact run. */
|
|
856
|
+
export function updateRunRecord(
|
|
857
|
+
db: DatabaseSync,
|
|
858
|
+
runId: string,
|
|
859
|
+
expectedRevision: number,
|
|
860
|
+
recordJson: string,
|
|
861
|
+
updatedAt: string,
|
|
862
|
+
): number {
|
|
863
|
+
const result = db
|
|
864
|
+
.prepare(
|
|
865
|
+
"UPDATE runs SET revision = revision + 1, record_json = ?, updated_at = ? WHERE run_id = ? AND revision = ?",
|
|
866
|
+
)
|
|
867
|
+
.run(recordJson, updatedAt, runId, expectedRevision);
|
|
868
|
+
if (Number(result.changes) !== 1)
|
|
869
|
+
throw new KernelStoreConflictError(
|
|
870
|
+
`CAS mismatch for run ${runId}: expected revision ${expectedRevision}`,
|
|
871
|
+
);
|
|
872
|
+
return expectedRevision + 1;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function updateRunClaim(
|
|
876
|
+
db: DatabaseSync,
|
|
877
|
+
runId: string,
|
|
878
|
+
expectedClaimStatus: RunClaimStatus,
|
|
879
|
+
nextClaimStatus: RunClaimStatus,
|
|
880
|
+
updatedAt: string,
|
|
881
|
+
): void {
|
|
882
|
+
const result = db
|
|
883
|
+
.prepare(
|
|
884
|
+
"UPDATE runs SET claim_status = ?, updated_at = ? WHERE run_id = ? AND claim_status = ?",
|
|
885
|
+
)
|
|
886
|
+
.run(nextClaimStatus, updatedAt, runId, expectedClaimStatus);
|
|
887
|
+
if (Number(result.changes) !== 1)
|
|
888
|
+
throw new KernelStoreConflictError(
|
|
889
|
+
`CAS mismatch for run ${runId} claim ${expectedClaimStatus}`,
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export function updateRunTerminal(
|
|
894
|
+
db: DatabaseSync,
|
|
895
|
+
runId: string,
|
|
896
|
+
state: Exclude<RunLifecycleState, "active">,
|
|
897
|
+
recordJson: string,
|
|
898
|
+
proofJson: string,
|
|
899
|
+
updatedAt: string,
|
|
900
|
+
): void {
|
|
901
|
+
const result = db
|
|
902
|
+
.prepare(
|
|
903
|
+
`UPDATE runs SET state = ?, record_json = ?, terminal_proof_json = ?, terminal_at = ?, updated_at = ?, claim_status = NULL
|
|
904
|
+
WHERE run_id = ? AND state = 'active'`,
|
|
905
|
+
)
|
|
906
|
+
.run(state, recordJson, proofJson, updatedAt, updatedAt, runId);
|
|
907
|
+
if (Number(result.changes) !== 1)
|
|
908
|
+
throw new KernelStoreConflictError(
|
|
909
|
+
`terminal settlement refused for run ${runId}: the run is not active`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** Record the document relocations a committed record write still owes. */
|
|
914
|
+
export function setPendingRelocations(
|
|
915
|
+
db: DatabaseSync,
|
|
916
|
+
runId: string,
|
|
917
|
+
relocationsJson: string | null,
|
|
918
|
+
): void {
|
|
919
|
+
db.prepare("UPDATE runs SET pending_relocations_json = ? WHERE run_id = ?").run(
|
|
920
|
+
relocationsJson,
|
|
921
|
+
runId,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export function listPendingRelocations(db: DatabaseSync): KernelRunRow[] {
|
|
926
|
+
const rows = db
|
|
927
|
+
.prepare("SELECT * FROM runs WHERE pending_relocations_json IS NOT NULL ORDER BY updated_at, task_id")
|
|
928
|
+
.all() as Array<Record<string, unknown>>;
|
|
929
|
+
return rows.map(mapRunRow);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function markAuditExported(db: DatabaseSync, runId: string, at: string): void {
|
|
933
|
+
db.prepare("UPDATE runs SET audit_exported_at = ? WHERE run_id = ?").run(at, runId);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export function readOperationRow(
|
|
937
|
+
db: DatabaseSync,
|
|
938
|
+
operationId: string,
|
|
939
|
+
): KernelOperationRow | null {
|
|
940
|
+
const row = db.prepare("SELECT * FROM operations WHERE operation_id = ?").get(operationId) as
|
|
941
|
+
| Record<string, unknown>
|
|
942
|
+
| undefined;
|
|
943
|
+
if (!row) return null;
|
|
944
|
+
return {
|
|
945
|
+
operation_id: String(row.operation_id),
|
|
946
|
+
kind: String(row.kind),
|
|
947
|
+
run_id: typeof row.run_id === "string" ? row.run_id : null,
|
|
948
|
+
result_json: String(row.result_json),
|
|
949
|
+
committed_at: String(row.committed_at),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
export function insertOperationRow(db: DatabaseSync, row: KernelOperationRow): void {
|
|
954
|
+
try {
|
|
955
|
+
db.prepare(
|
|
956
|
+
"INSERT INTO operations (operation_id, kind, run_id, result_json, committed_at) VALUES (?, ?, ?, ?, ?)",
|
|
957
|
+
).run(row.operation_id, row.kind, row.run_id, row.result_json, row.committed_at);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
960
|
+
throw new KernelStoreConflictError(`operation ${row.operation_id} was already committed: ${message}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export function appendJournalRow(
|
|
965
|
+
db: DatabaseSync,
|
|
966
|
+
entryJson: string,
|
|
967
|
+
taskId: string | null,
|
|
968
|
+
observationCommitId: string | null,
|
|
969
|
+
observationId: string | null,
|
|
970
|
+
): number {
|
|
971
|
+
const result = db
|
|
972
|
+
.prepare(
|
|
973
|
+
"INSERT INTO journal (entry_json, task_id, observation_commit_id, observation_id) VALUES (?, ?, ?, ?)",
|
|
974
|
+
)
|
|
975
|
+
.run(entryJson, taskId, observationCommitId, observationId);
|
|
976
|
+
return Number(result.lastInsertRowid);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export function findJournalObservation(
|
|
980
|
+
db: DatabaseSync,
|
|
981
|
+
observationCommitId: string,
|
|
982
|
+
): { observation_id: string | null } | null {
|
|
983
|
+
const row = db
|
|
984
|
+
.prepare("SELECT observation_id FROM journal WHERE observation_commit_id = ? LIMIT 1")
|
|
985
|
+
.get(observationCommitId) as { observation_id?: unknown } | undefined;
|
|
986
|
+
if (!row) return null;
|
|
987
|
+
return {
|
|
988
|
+
observation_id: typeof row.observation_id === "string" ? row.observation_id : null,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export function readJournalRows(db: DatabaseSync, limit = 1000): KernelJournalRow[] {
|
|
993
|
+
const rows = db
|
|
994
|
+
.prepare("SELECT * FROM journal ORDER BY seq LIMIT ?")
|
|
995
|
+
.all(limit) as Array<Record<string, unknown>>;
|
|
996
|
+
return rows.map((raw) => ({
|
|
997
|
+
seq: Number(raw.seq),
|
|
998
|
+
entry_json: String(raw.entry_json),
|
|
999
|
+
task_id: typeof raw.task_id === "string" ? raw.task_id : null,
|
|
1000
|
+
observation_commit_id:
|
|
1001
|
+
typeof raw.observation_commit_id === "string" ? raw.observation_commit_id : null,
|
|
1002
|
+
observation_id: typeof raw.observation_id === "string" ? raw.observation_id : null,
|
|
1003
|
+
}));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** Stable path digests used for snapshot refs and audit identity metadata. */
|
|
1007
|
+
export function workspaceIdentity(db: DatabaseSync): string {
|
|
1008
|
+
const value = readMeta(db, "workspace_id");
|
|
1009
|
+
if (!value) throw new KernelSchemaError("kernel store identity metadata is missing");
|
|
1010
|
+
return value;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Consistent, closed-state backup. `VACUUM INTO` writes a checkpointed copy
|
|
1015
|
+
* that never contains a half-applied transaction.
|
|
1016
|
+
*/
|
|
1017
|
+
export function backupKernelStore(root: string, targetPath: string): void {
|
|
1018
|
+
const canonical = canonicalRoot(root);
|
|
1019
|
+
if (openStores.has(canonical))
|
|
1020
|
+
throw new KernelStoreConflictError("kernel store backup requires the store to be idle");
|
|
1021
|
+
const db = openKernelStore(root, { create: false });
|
|
1022
|
+
if (!db) throw new KernelStoreConflictError("kernel store does not exist");
|
|
1023
|
+
try {
|
|
1024
|
+
const target = resolve(targetPath);
|
|
1025
|
+
const live = resolve(canonical, KERNEL_DB_RELATIVE);
|
|
1026
|
+
for (const forbidden of [live, `${live}-wal`, `${live}-shm`])
|
|
1027
|
+
if (samePath(target, forbidden))
|
|
1028
|
+
throw new KernelStoreSecurityError(
|
|
1029
|
+
"a backup target must not be the live store or its sidecars",
|
|
1030
|
+
);
|
|
1031
|
+
// Write the backup beside its target, durably, and only then replace any
|
|
1032
|
+
// existing backup: a failed or interrupted backup must never cost the
|
|
1033
|
+
// last good copy.
|
|
1034
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
1035
|
+
const temp = `${target}.${randomUUID()}.backup`;
|
|
1036
|
+
rmSync(temp, { force: true });
|
|
1037
|
+
try {
|
|
1038
|
+
db.prepare(`VACUUM INTO '${temp.replace(/'/g, "''")}'`).run();
|
|
1039
|
+
const fd = openSync(temp, constants.O_RDONLY);
|
|
1040
|
+
try {
|
|
1041
|
+
fsyncSync(fd);
|
|
1042
|
+
} finally {
|
|
1043
|
+
closeSync(fd);
|
|
1044
|
+
}
|
|
1045
|
+
renameSync(temp, target);
|
|
1046
|
+
const directory = openSync(dirname(target), constants.O_RDONLY);
|
|
1047
|
+
try {
|
|
1048
|
+
fsyncSync(directory);
|
|
1049
|
+
} finally {
|
|
1050
|
+
closeSync(directory);
|
|
1051
|
+
}
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
rmSync(temp, { force: true });
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
} finally {
|
|
1057
|
+
closeQuietly(db);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Restore a consistent backup into this worktree. All accessors must be
|
|
1063
|
+
* stopped; the restored database is revalidated against the worktree binding
|
|
1064
|
+
* so a copy from another worktree is refused instead of granting authority.
|
|
1065
|
+
*/
|
|
1066
|
+
export function restoreKernelStore(root: string, sourcePath: string): void {
|
|
1067
|
+
const canonical = canonicalRoot(root);
|
|
1068
|
+
if (openStores.has(canonical))
|
|
1069
|
+
throw new KernelStoreConflictError("kernel store restore requires the store to be idle");
|
|
1070
|
+
const source = resolve(sourcePath);
|
|
1071
|
+
if (!existsSync(source)) throw new KernelStoreConflictError("kernel store backup is missing");
|
|
1072
|
+
ensureStoreDirectory(canonical);
|
|
1073
|
+
const target = resolve(canonical, KERNEL_DB_RELATIVE);
|
|
1074
|
+
const temp = `${target}.${randomUUID()}.restore`;
|
|
1075
|
+
try {
|
|
1076
|
+
copyFileSync(source, temp);
|
|
1077
|
+
const fd = openSync(temp, constants.O_RDONLY);
|
|
1078
|
+
try {
|
|
1079
|
+
fsyncSync(fd);
|
|
1080
|
+
} finally {
|
|
1081
|
+
closeSync(fd);
|
|
1082
|
+
}
|
|
1083
|
+
// Validate the backup copy *before* publishing it: an unreadable,
|
|
1084
|
+
// incompatible or foreign-workspace backup must leave the live store and
|
|
1085
|
+
// its sidecars exactly as they were.
|
|
1086
|
+
const probe = openStoreFile(canonical, temp, { readOnly: true });
|
|
1087
|
+
if (!probe) throw new KernelStoreConflictError("kernel store backup could not be opened");
|
|
1088
|
+
closeQuietly(probe);
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
rmSync(temp, { force: true });
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
1093
|
+
// Only a validated backup reaches the swap. Fold the live database's write
|
|
1094
|
+
// ahead log into its main file first: deleting the sidecars is otherwise a
|
|
1095
|
+
// window in which committed transactions only exist in the WAL. An
|
|
1096
|
+
// unreadable main file with no WAL is just a dead file: replace it. An
|
|
1097
|
+
// unreadable main file that still has a WAL must stay put so those
|
|
1098
|
+
// committed facts can be recovered.
|
|
1099
|
+
try {
|
|
1100
|
+
const walPresent = existsSync(`${target}-wal`);
|
|
1101
|
+
try {
|
|
1102
|
+
checkpointLiveStore(canonical, target);
|
|
1103
|
+
for (const suffix of ["-wal", "-shm"]) rmSync(`${target}${suffix}`, { force: true });
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
if (walPresent) throw error;
|
|
1106
|
+
}
|
|
1107
|
+
renameSync(temp, target);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
rmSync(temp, { force: true });
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
const directory = openSync(dirname(target), constants.O_RDONLY);
|
|
1113
|
+
try {
|
|
1114
|
+
fsyncSync(directory);
|
|
1115
|
+
} finally {
|
|
1116
|
+
closeSync(directory);
|
|
1117
|
+
}
|
|
1118
|
+
const db = openStoreFile(canonical, target, {});
|
|
1119
|
+
if (!db) throw new KernelStoreConflictError("restored kernel store could not be opened");
|
|
1120
|
+
closeQuietly(db);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Compare two paths as the filesystem sees them: a caller-supplied target may
|
|
1125
|
+
* name the same file through a symlinked prefix (macOS /var vs /private/var),
|
|
1126
|
+
* so both sides are resolved before the comparison.
|
|
1127
|
+
*/
|
|
1128
|
+
function samePath(a: string, b: string): boolean {
|
|
1129
|
+
const identity = (path: string): string => {
|
|
1130
|
+
try {
|
|
1131
|
+
return realpathSync(path);
|
|
1132
|
+
} catch {
|
|
1133
|
+
return resolve(path);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
return identity(a) === identity(b);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Fold committed write-ahead-log content into the main database file so a later
|
|
1141
|
+
* swap cannot lose transactions that had not reached it yet. A failure here
|
|
1142
|
+
* propagates: the caller must not remove sidecars it could not checkpoint.
|
|
1143
|
+
*/
|
|
1144
|
+
function checkpointLiveStore(canonical: string, target: string): void {
|
|
1145
|
+
if (!existsSync(target)) return;
|
|
1146
|
+
let db: DatabaseSync;
|
|
1147
|
+
try {
|
|
1148
|
+
db = new DatabaseSync(target);
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
// The live database cannot be read, so its write-ahead log cannot be
|
|
1151
|
+
// folded into it: removing the sidecars would drop committed facts.
|
|
1152
|
+
throw new KernelStoreConflictError(
|
|
1153
|
+
`live kernel store could not be checkpointed before restore: ${error instanceof Error ? error.message : String(error)}`,
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
try {
|
|
1157
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
1158
|
+
const result = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get() as
|
|
1159
|
+
| { busy?: number | bigint; log?: number | bigint; checkpointed?: number | bigint }
|
|
1160
|
+
| undefined;
|
|
1161
|
+
const busy = Number(result?.busy ?? 0);
|
|
1162
|
+
if (busy !== 0)
|
|
1163
|
+
throw new KernelStoreConflictError(
|
|
1164
|
+
"live kernel store still has an active writer; retry the restore when the store is idle",
|
|
1165
|
+
);
|
|
1166
|
+
const log = Number(result?.log ?? 0);
|
|
1167
|
+
const checkpointed = Number(result?.checkpointed ?? 0);
|
|
1168
|
+
if (log > checkpointed)
|
|
1169
|
+
throw new KernelStoreConflictError(
|
|
1170
|
+
"live kernel store write-ahead log could not be fully checkpointed; retry the restore",
|
|
1171
|
+
);
|
|
1172
|
+
} finally {
|
|
1173
|
+
try {
|
|
1174
|
+
db.close();
|
|
1175
|
+
} catch {
|
|
1176
|
+
// A close failure leaves the checkpointed file in place.
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const fd = openSync(target, constants.O_RDONLY);
|
|
1180
|
+
try {
|
|
1181
|
+
fsyncSync(fd);
|
|
1182
|
+
} finally {
|
|
1183
|
+
closeSync(fd);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export function readStoreBytes(path: string): Buffer {
|
|
1188
|
+
return readFileSync(path);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
export function writeStoreBytes(path: string, bytes: Buffer): void {
|
|
1192
|
+
writeFileSync(path, bytes);
|
|
1193
|
+
}
|