@astrosheep/keiyaku 2.9.11 → 2.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +62 -72
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/providers/claude-agent-sdk/adapter.js +15 -2
  4. package/build/cli/commands/contract/bind/meta.js +4 -4
  5. package/build/cli/commands/metadata.js +3 -2
  6. package/build/cli/commands/projection/kill/handler.js +4 -5
  7. package/build/cli/commands/shared.js +1 -1
  8. package/build/cli/commands/task/catalog.js +2 -0
  9. package/build/cli/commands/task/compose/handler.js +18 -0
  10. package/build/cli/commands/task/compose/meta.js +10 -0
  11. package/build/cli/flags.js +5 -1
  12. package/build/cli/index.js +11 -6
  13. package/build/cli/parse-flags.js +21 -21
  14. package/build/cli/parse-metadata.js +5 -2
  15. package/build/cli/parse-selectors.js +4 -4
  16. package/build/cli/parse.js +23 -23
  17. package/build/cli/projection-address.js +7 -7
  18. package/build/cli/render/task.js +27 -0
  19. package/build/cli/types.js +1 -1
  20. package/build/core/addressing.js +3 -3
  21. package/build/core/call/context.js +3 -3
  22. package/build/core/call/prompt.js +1 -1
  23. package/build/core/contract-view.js +1 -1
  24. package/build/core/draft.js +51 -22
  25. package/build/core/projection/generation/model.js +1 -1
  26. package/build/core/projection/generation/projection-generation-runner.js +13 -3
  27. package/build/core/projection/generation/transitions.js +10 -0
  28. package/build/core/projection/projection-history.js +1 -1
  29. package/build/core/projection/projection-kill.js +5 -4
  30. package/build/core/projection/projection-runner-lock.js +58 -229
  31. package/build/core/projection/projection-wait.js +2 -2
  32. package/build/core/projection/projection-wake.js +2 -0
  33. package/build/core/repository-ledger/claim-evidence.js +1 -1
  34. package/build/core/repository-ledger/current-state-store.js +455 -337
  35. package/build/core/repository-ledger/read-model.js +3 -3
  36. package/build/core/repository-ledger/write-transaction.js +15 -13
  37. package/build/core/sqlite-process-lifetime-lock.js +194 -0
  38. package/build/core/task/compose.js +327 -0
  39. package/build/core/task/index.js +1 -0
  40. package/build/core/task/settlement-git.js +69 -26
  41. package/build/core/task/task-git-store.js +1 -0
  42. package/build/core/task/task-store-repository.js +1 -0
  43. package/build/flow-error.js +6 -1
  44. package/build/generated/version.js +2 -2
  45. package/package.json +2 -2
  46. package/skills/keiyaku-akuma/SKILL.md +6 -1
  47. package/skills/keiyaku-task/SKILL.md +23 -1
  48. package/skills/keiyaku-workflow/SKILL.md +33 -18
@@ -1,13 +1,16 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { runGitProcess } from "../../git/core.js";
6
+ import { acquireSqliteProcessLifetimeLock } from "../sqlite-process-lifetime-lock.js";
6
7
  import { applyFoldEntry } from "./fold.js";
7
8
  import { createLifecycleEntrySchema, evidenceObjectId, repositoryDeliveryRef } from "./codec.js";
8
9
  export const REPOSITORY_CURRENT_STATE_VERSION = 3;
9
- export const REPOSITORY_CURRENT_STATE_FILE = "keiyaku-current-state.sqlite";
10
- const SNAPSHOT_WRITE_BUSY_MS = 5_000;
10
+ export const REPOSITORY_CURRENT_STATE_PREFIX = "keiyaku-current-state-";
11
+ export const REPOSITORY_CURRENT_STATE_SUFFIX = ".sqlite";
12
+ const SNAPSHOT_READER_BUSY_MS = 5_000;
13
+ const SNAPSHOT_WRITER_LOCK_FILE = "keiyaku-current-state-writer-coordination.sqlite";
11
14
  const SCHEMA = `
12
15
  CREATE TABLE snapshot_schema (
13
16
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
@@ -126,13 +129,16 @@ function validateRepositoryReceipts(database, contracts, summaryOnly, includeTop
126
129
  if (includeTopology)
127
130
  assertReceipt(database, "topology", topologyDigest(database));
128
131
  }
129
- async function snapshotPath(cwd, signal) {
132
+ function snapshotFileName(head) {
133
+ return `${REPOSITORY_CURRENT_STATE_PREFIX}${head ?? "empty"}${REPOSITORY_CURRENT_STATE_SUFFIX}`;
134
+ }
135
+ export async function repositoryCurrentStateSnapshotPath(cwd, head, signal) {
130
136
  // Addressing callers already pass the resolved primary worktree root. Reuse
131
137
  // that physical coordinate within this request instead of probing Git again.
132
138
  const localGitDirectory = path.join(cwd, ".git");
133
139
  try {
134
140
  if ((await fs.stat(localGitDirectory)).isDirectory()) {
135
- return path.join(await fs.realpath(localGitDirectory), REPOSITORY_CURRENT_STATE_FILE);
141
+ return path.join(await fs.realpath(localGitDirectory), snapshotFileName(head));
136
142
  }
137
143
  }
138
144
  catch {
@@ -145,38 +151,163 @@ async function snapshotPath(cwd, signal) {
145
151
  if (result.status !== 0 || result.error || !path.isAbsolute(commonDir)) {
146
152
  throw new Error(`cannot resolve repository common directory: ${result.stderr.trim() || result.error?.message || `exit ${String(result.status)}`}`);
147
153
  }
148
- return path.join(commonDir, REPOSITORY_CURRENT_STATE_FILE);
154
+ return path.join(commonDir, snapshotFileName(head));
155
+ }
156
+ function classifySnapshotFailure(error) {
157
+ const code = error.code;
158
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
159
+ if (code === "ENOENT" || message.includes("no such file or directory"))
160
+ return "missing";
161
+ if (code === "ERR_SQLITE_BUSY" || code === "ERR_SQLITE_LOCKED" || message.includes("database is locked") || message.includes("database is busy"))
162
+ return "transient";
163
+ if (code === "ERR_SQLITE_CORRUPT" || code === "ERR_SQLITE_NOTADB" || message.includes("database disk image is malformed") || message.includes("file is not a database"))
164
+ return "corrupt";
165
+ return "validation-failure";
166
+ }
167
+ function isCantOpen(error) {
168
+ const code = error.code;
169
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
170
+ return code === "ERR_SQLITE_CANTOPEN" || message.includes("unable to open database file");
171
+ }
172
+ async function snapshotExists(databasePath) {
173
+ try {
174
+ await fs.lstat(databasePath);
175
+ return true;
176
+ }
177
+ catch (error) {
178
+ if (error.code === "ENOENT")
179
+ return false;
180
+ return undefined;
181
+ }
149
182
  }
150
- function openValidated(databasePath) {
183
+ async function openValidated(databasePath) {
184
+ const existedBeforeOpen = await snapshotExists(databasePath);
151
185
  let database;
152
186
  try {
153
- database = new DatabaseSync(databasePath);
154
- database.exec("PRAGMA query_only = ON");
187
+ database = new DatabaseSync(databasePath, { readOnly: true });
188
+ database.exec(`PRAGMA busy_timeout = ${SNAPSHOT_READER_BUSY_MS}; PRAGMA query_only = ON`);
155
189
  const rows = database.prepare("SELECT version FROM snapshot_schema WHERE singleton = 1").all();
156
190
  if (rows.length !== 1 || rows[0]?.version !== REPOSITORY_CURRENT_STATE_VERSION) {
157
191
  database.close();
158
- return null;
192
+ return { kind: "validation-failure" };
159
193
  }
160
- return database;
194
+ return { kind: "opened", database };
161
195
  }
162
- catch {
196
+ catch (error) {
163
197
  try {
164
198
  database.close();
165
199
  }
166
- catch { /* discarded below */ }
167
- return null;
200
+ catch { /* open did not yield a usable connection */ }
201
+ if (isCantOpen(error)) {
202
+ const existsAfterFailure = await snapshotExists(databasePath);
203
+ if (existedBeforeOpen === false || existsAfterFailure === false)
204
+ return { kind: "missing", error };
205
+ }
206
+ return { kind: classifySnapshotFailure(error), error };
168
207
  }
169
208
  }
170
- async function discard(databasePath) {
171
- await Promise.all(["", "-wal", "-shm"].map(async (suffix) => {
209
+ async function readSnapshot(databasePath, read) {
210
+ const opened = await openValidated(databasePath);
211
+ if (opened.kind !== "opened")
212
+ return opened;
213
+ try {
214
+ return { kind: "read", value: read(opened.database) };
215
+ }
216
+ catch (error) {
217
+ return { kind: classifySnapshotFailure(error), error };
218
+ }
219
+ finally {
172
220
  try {
173
- await fs.unlink(`${databasePath}${suffix}`);
221
+ opened.database.close();
174
222
  }
175
- catch (error) {
176
- if (error.code !== "ENOENT")
177
- throw error;
223
+ catch { /* validation failure already has its outcome */ }
224
+ }
225
+ }
226
+ export function currentStateWriterCoordination(databasePath) {
227
+ const lockPath = path.join(path.dirname(databasePath), SNAPSHOT_WRITER_LOCK_FILE);
228
+ return {
229
+ path: lockPath,
230
+ async acquire(signal) {
231
+ const lease = await acquireSqliteProcessLifetimeLock({ path: lockPath, signal });
232
+ return { release: () => lease.close() };
233
+ },
234
+ };
235
+ }
236
+ async function withSnapshotWriter(databasePath, signal, operation) {
237
+ const lock = await currentStateWriterCoordination(databasePath).acquire(signal);
238
+ try {
239
+ return await operation();
240
+ }
241
+ finally {
242
+ lock.release();
243
+ }
244
+ }
245
+ function temporarySnapshotPath(databasePath) {
246
+ return `${databasePath}.${process.pid}.${randomUUID()}.tmp`;
247
+ }
248
+ async function discardTemporarySnapshot(databasePath) {
249
+ await fs.unlink(databasePath).catch((error) => {
250
+ if (error.code !== "ENOENT")
251
+ throw error;
252
+ });
253
+ }
254
+ /** Writers alone publish or retire cache bytes; readers have no filesystem mutation path. */
255
+ async function publishImmutableSnapshot(buildPath, databasePath) {
256
+ if (await snapshotExists(databasePath))
257
+ return false;
258
+ await fs.rename(buildPath, databasePath);
259
+ return true;
260
+ }
261
+ /**
262
+ * A filename becomes published only after its complete signed contents validate.
263
+ * This runs under writer coordination; readers merely report an unusable cache.
264
+ */
265
+ async function preparePublicationTarget(databasePath, expectedHead) {
266
+ const snapshot = await readSnapshot(databasePath, (database) => {
267
+ const pin = metadata(database);
268
+ if (!pin || pin.head !== expectedHead)
269
+ throw new Error("snapshot pin does not match its immutable filename");
270
+ validatePinReceipt(database, pin);
271
+ const rows = database.prepare(`
272
+ SELECT contract, status, current_json AS currentJson, fold_json AS foldJson, diagnostics_json AS diagnosticsJson
273
+ FROM snapshot_contracts ORDER BY contract
274
+ `).all();
275
+ const repository = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_repository WHERE singleton = 1").get();
276
+ if (!repository)
277
+ throw new Error("snapshot repository row is missing");
278
+ validateRepositoryReceipts(database, rows.map((row) => row.contract), false, true);
279
+ parseDiagnostics(repository.diagnosticsJson);
280
+ for (const row of rows) {
281
+ parseDiagnostics(row.diagnosticsJson, row.contract);
282
+ validateContractConsistency(database, row, pin);
178
283
  }
179
- }));
284
+ });
285
+ if (snapshot.kind === "read")
286
+ return "published";
287
+ if (snapshot.kind === "missing")
288
+ return "missing";
289
+ if (snapshot.kind === "transient")
290
+ return "transient";
291
+ await fs.unlink(databasePath).catch((error) => {
292
+ if (error.code !== "ENOENT")
293
+ throw error;
294
+ });
295
+ return "missing";
296
+ }
297
+ async function garbageCollectSnapshots(databasePath) {
298
+ const directory = path.dirname(databasePath);
299
+ const retained = path.basename(databasePath);
300
+ let entries;
301
+ try {
302
+ entries = await fs.readdir(directory);
303
+ }
304
+ catch {
305
+ return;
306
+ }
307
+ const cacheFile = /^keiyaku-current-state-(?:[0-9a-f]{40}|[0-9a-f]{64}|empty)\.sqlite(?:\..+\.tmp)?$/;
308
+ await Promise.all(entries
309
+ .filter((entry) => entry !== retained && cacheFile.test(entry))
310
+ .map((entry) => fs.unlink(path.join(directory, entry)).catch(() => undefined)));
180
311
  }
181
312
  function parseJson(text) {
182
313
  return JSON.parse(text);
@@ -345,13 +476,8 @@ function atPin(model, head) {
345
476
  return model.observedLedgerHead === head ? model : { ...model, observedLedgerHead: head };
346
477
  }
347
478
  export async function readPinnedCurrentContract(cwd, observedHead, contract, signal) {
348
- const databasePath = await snapshotPath(cwd, signal);
349
- const database = openValidated(databasePath);
350
- if (!database) {
351
- await discard(databasePath);
352
- return null;
353
- }
354
- try {
479
+ const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
480
+ const snapshot = await readSnapshot(databasePath, (database) => {
355
481
  const pin = metadata(database);
356
482
  if (!pin || pin.head !== observedHead)
357
483
  return null;
@@ -371,53 +497,36 @@ export async function readPinnedCurrentContract(cwd, observedHead, contract, sig
371
497
  return unattributed.length > 0
372
498
  ? { status: "indeterminate", contract, diagnostics: unattributed }
373
499
  : { status: "absent", contract, diagnostics: [] };
374
- }
375
- catch {
376
- database.close();
377
- await discard(databasePath);
378
- return null;
379
- }
380
- finally {
381
- try {
382
- database.close();
383
- }
384
- catch { /* corruption path already discarded */ }
385
- }
500
+ });
501
+ return snapshot.kind === "read" ? snapshot.value : null;
386
502
  }
387
- export async function readRepositoryCurrentStatePin(cwd, signal) {
388
- const databasePath = await snapshotPath(cwd, signal);
389
- const database = openValidated(databasePath);
390
- if (!database) {
391
- await discard(databasePath);
392
- return undefined;
393
- }
394
- try {
503
+ export async function readRepositoryCurrentStatePin(cwd, observedHead, signal) {
504
+ const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
505
+ const snapshot = await readSnapshot(databasePath, (database) => {
395
506
  const pin = metadata(database);
396
507
  if (!pin)
397
508
  return undefined;
398
509
  validatePinReceipt(database, pin);
399
510
  return pin.head;
400
- }
401
- catch {
402
- database.close();
403
- await discard(databasePath);
404
- return undefined;
405
- }
406
- finally {
407
- try {
408
- database.close();
409
- }
410
- catch { /* corruption path already discarded */ }
411
- }
511
+ });
512
+ return snapshot.kind === "read" ? snapshot.value : undefined;
513
+ }
514
+ /**
515
+ * The first commit parent is the only inexpensive candidate for a linear
516
+ * incremental predecessor. It is not a current pointer: the caller still
517
+ * validates the cache addressed by that exact OID before using it.
518
+ */
519
+ export async function readRepositoryCurrentStatePredecessor(cwd, observedHead, signal) {
520
+ const result = await runGitProcess(cwd, ["show", "-s", "--format=%P", observedHead], { signal });
521
+ if (result.error?.name === "AbortError")
522
+ throw result.error;
523
+ if (result.status !== 0 || result.error)
524
+ return null;
525
+ return result.stdout.trim().split(/\s+/)[0] || null;
412
526
  }
413
527
  export async function readRepositoryCurrentStateNodeIds(cwd, observedHead, signal) {
414
- const databasePath = await snapshotPath(cwd, signal);
415
- const database = openValidated(databasePath);
416
- if (!database) {
417
- await discard(databasePath);
418
- return null;
419
- }
420
- try {
528
+ const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
529
+ const snapshot = await readSnapshot(databasePath, (database) => {
421
530
  const pin = metadata(database);
422
531
  if (!pin || pin.head !== observedHead)
423
532
  return null;
@@ -442,28 +551,13 @@ export async function readRepositoryCurrentStateNodeIds(cwd, observedHead, signa
442
551
  }
443
552
  }
444
553
  return { ledgerParents, entryIntroductions };
445
- }
446
- catch {
447
- database.close();
448
- await discard(databasePath);
449
- return null;
450
- }
451
- finally {
452
- try {
453
- database.close();
454
- }
455
- catch { /* corruption path already discarded */ }
456
- }
554
+ });
555
+ return snapshot.kind === "read" ? snapshot.value : null;
457
556
  }
458
557
  /** Rehydrate the accepted typed topology for publication admission without Git history traversal. */
459
558
  export async function readPinnedAcceptedTraversal(cwd, observedHead, signal) {
460
- const databasePath = await snapshotPath(cwd, signal);
461
- const database = openValidated(databasePath);
462
- if (!database) {
463
- await discard(databasePath);
464
- return null;
465
- }
466
- try {
559
+ const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
560
+ const snapshot = await readSnapshot(databasePath, (database) => {
467
561
  const pin = metadata(database);
468
562
  if (!pin || pin.head !== observedHead || !pin.objectFormat || !pin.emptyTree)
469
563
  return null;
@@ -520,27 +614,12 @@ export async function readPinnedAcceptedTraversal(cwd, observedHead, signal) {
520
614
  };
521
615
  visit(observedHead);
522
616
  return { head: observedHead, coordinates: { objectFormat: pin.objectFormat, objectIdLength: pin.objectFormat === "sha1" ? 40 : 64, emptyTree: pin.emptyTree }, nodes, order, diseases: [] };
523
- }
524
- catch {
525
- database.close();
526
- await discard(databasePath);
527
- return null;
528
- }
529
- finally {
530
- try {
531
- database.close();
532
- }
533
- catch { /* corruption path already discarded */ }
534
- }
617
+ });
618
+ return snapshot.kind === "read" ? snapshot.value : null;
535
619
  }
536
620
  export async function readPinnedRepositoryLifecycle(cwd, observedHead, signal, options = {}) {
537
- const databasePath = await snapshotPath(cwd, signal);
538
- const database = openValidated(databasePath);
539
- if (!database) {
540
- await discard(databasePath);
541
- return null;
542
- }
543
- try {
621
+ const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
622
+ const snapshot = await readSnapshot(databasePath, (database) => {
544
623
  const pin = metadata(database);
545
624
  if (!pin || pin.head !== observedHead)
546
625
  return null;
@@ -575,279 +654,318 @@ export async function readPinnedRepositoryLifecycle(cwd, observedHead, signal, o
575
654
  : database.prepare("SELECT node_json AS nodeJson FROM snapshot_topology ORDER BY rowid").all().map((row) => parseJson(row.nodeJson)),
576
655
  },
577
656
  };
578
- }
579
- catch {
580
- database.close();
581
- await discard(databasePath);
582
- return null;
583
- }
584
- finally {
585
- try {
586
- database.close();
587
- }
588
- catch { /* corruption path already discarded */ }
589
- }
657
+ });
658
+ return snapshot.kind === "read" ? snapshot.value : null;
590
659
  }
591
660
  export async function publishRepositoryCurrentState(cwd, read, options) {
592
- const databasePath = await snapshotPath(cwd, options.signal);
661
+ const databasePath = await repositoryCurrentStateSnapshotPath(cwd, read.observedLedgerHead, options.signal);
593
662
  await fs.mkdir(path.dirname(databasePath), { recursive: true });
594
663
  if (await options.confirmHead() !== read.observedLedgerHead)
595
664
  return false;
596
- let database;
597
- try {
598
- database = new DatabaseSync(databasePath);
599
- database.exec(`PRAGMA busy_timeout = ${SNAPSHOT_WRITE_BUSY_MS}; PRAGMA synchronous = FULL; PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE`);
600
- database.exec("DROP TABLE IF EXISTS snapshot_integrity; DROP TABLE IF EXISTS snapshot_resources; DROP TABLE IF EXISTS snapshot_topology; DROP TABLE IF EXISTS snapshot_summaries; DROP TABLE IF EXISTS snapshot_history; DROP TABLE IF EXISTS snapshot_contracts; DROP TABLE IF EXISTS snapshot_repository; DROP TABLE IF EXISTS snapshot_pin; DROP TABLE IF EXISTS snapshot_schema");
601
- database.exec(SCHEMA);
602
- database.prepare("INSERT INTO snapshot_schema(singleton, version) VALUES (1, ?)").run(REPOSITORY_CURRENT_STATE_VERSION);
603
- database.prepare("INSERT INTO snapshot_pin(singleton, ledger_head, object_format, empty_tree) VALUES (1, ?, ?, ?)").run(read.observedLedgerHead, read.objectFormat, read.emptyTree);
604
- const insert = database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, ?, ?, ?, ?)");
605
- const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
606
- const insertSummary = database.prepare("INSERT INTO snapshot_summaries(contract, history_json) VALUES (?, ?)");
607
- const isolated = new Set(read.isolatedContracts);
608
- const models = new Map(read.contracts.map((model) => [model.contract, model]));
609
- for (const contract of new Set([...models.keys(), ...isolated])) {
610
- const model = models.get(contract) ?? null;
611
- const diagnostics = read.diagnostics.filter((disease) => disease.contract === contract);
612
- const status = isolated.has(contract) ? "isolated" : diagnostics.length > 0 ? "diseased" : "healthy";
613
- const folded = options.fold.contracts.get(contract) ?? null;
614
- const current = model === null ? null : Object.fromEntries(Object.entries(model).filter(([key]) => key !== "history"));
615
- insert.run(contract, status, current === null ? null : JSON.stringify(current), folded === null ? null : JSON.stringify(folded), JSON.stringify(diagnostics));
616
- model?.history.forEach((row, position) => insertHistory.run(contract, position, row.objectId, JSON.stringify(row.entry)));
617
- if (model)
618
- insertSummary.run(contract, JSON.stringify(summaryHistory(model.history)));
619
- }
620
- database.prepare("INSERT INTO snapshot_repository(singleton, diagnostics_json) VALUES (1, ?)").run(JSON.stringify(read.diagnostics.filter((disease) => disease.contract === undefined)));
621
- const insertTopology = database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
622
- for (const node of read.topology.nodes)
623
- insertTopology.run(node.objectId, JSON.stringify(node));
624
- const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
625
- for (const model of read.contracts) {
626
- if (model.resources.worktree?.state === "held")
627
- insertResource.run("worktree", model.resources.worktree.place, model.contract);
628
- for (const task of model.resources.tasks)
629
- if (task.state === "held")
630
- insertResource.run("task", task.taskId, model.contract);
631
- }
632
- writeIntegrityReceipts(database);
633
- options.beforeCommit?.();
634
- database.exec("COMMIT");
635
- database.close();
636
- return true;
637
- }
638
- catch (error) {
639
- try {
640
- database.exec("ROLLBACK");
641
- }
642
- catch { /* database may not have opened */ }
665
+ return withSnapshotWriter(databasePath, options.signal, async () => {
666
+ if (await options.confirmHead() !== read.observedLedgerHead)
667
+ return false;
668
+ const existing = await preparePublicationTarget(databasePath, read.observedLedgerHead);
669
+ if (existing === "published")
670
+ return true;
671
+ if (existing === "transient")
672
+ return false;
673
+ const buildPath = temporarySnapshotPath(databasePath);
674
+ let database;
643
675
  try {
676
+ await fs.unlink(buildPath).catch((error) => {
677
+ if (error.code !== "ENOENT")
678
+ throw error;
679
+ });
680
+ database = new DatabaseSync(buildPath);
681
+ database.exec("PRAGMA synchronous = FULL; PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE");
682
+ database.exec("DROP TABLE IF EXISTS snapshot_integrity; DROP TABLE IF EXISTS snapshot_resources; DROP TABLE IF EXISTS snapshot_topology; DROP TABLE IF EXISTS snapshot_summaries; DROP TABLE IF EXISTS snapshot_history; DROP TABLE IF EXISTS snapshot_contracts; DROP TABLE IF EXISTS snapshot_repository; DROP TABLE IF EXISTS snapshot_pin; DROP TABLE IF EXISTS snapshot_schema");
683
+ database.exec(SCHEMA);
684
+ database.prepare("INSERT INTO snapshot_schema(singleton, version) VALUES (1, ?)").run(REPOSITORY_CURRENT_STATE_VERSION);
685
+ database.prepare("INSERT INTO snapshot_pin(singleton, ledger_head, object_format, empty_tree) VALUES (1, ?, ?, ?)").run(read.observedLedgerHead, read.objectFormat, read.emptyTree);
686
+ const insert = database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, ?, ?, ?, ?)");
687
+ const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
688
+ const insertSummary = database.prepare("INSERT INTO snapshot_summaries(contract, history_json) VALUES (?, ?)");
689
+ const isolated = new Set(read.isolatedContracts);
690
+ const models = new Map(read.contracts.map((model) => [model.contract, model]));
691
+ for (const contract of new Set([...models.keys(), ...isolated])) {
692
+ const model = models.get(contract) ?? null;
693
+ const diagnostics = read.diagnostics.filter((disease) => disease.contract === contract);
694
+ const status = isolated.has(contract) ? "isolated" : diagnostics.length > 0 ? "diseased" : "healthy";
695
+ const folded = options.fold.contracts.get(contract) ?? null;
696
+ const current = model === null ? null : Object.fromEntries(Object.entries(model).filter(([key]) => key !== "history"));
697
+ insert.run(contract, status, current === null ? null : JSON.stringify(current), folded === null ? null : JSON.stringify(folded), JSON.stringify(diagnostics));
698
+ model?.history.forEach((row, position) => insertHistory.run(contract, position, row.objectId, JSON.stringify(row.entry)));
699
+ if (model)
700
+ insertSummary.run(contract, JSON.stringify(summaryHistory(model.history)));
701
+ }
702
+ database.prepare("INSERT INTO snapshot_repository(singleton, diagnostics_json) VALUES (1, ?)").run(JSON.stringify(read.diagnostics.filter((disease) => disease.contract === undefined)));
703
+ const insertTopology = database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
704
+ for (const node of read.topology.nodes)
705
+ insertTopology.run(node.objectId, JSON.stringify(node));
706
+ const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
707
+ for (const model of read.contracts) {
708
+ if (model.resources.worktree?.state === "held")
709
+ insertResource.run("worktree", model.resources.worktree.place, model.contract);
710
+ for (const task of model.resources.tasks)
711
+ if (task.state === "held")
712
+ insertResource.run("task", task.taskId, model.contract);
713
+ }
714
+ writeIntegrityReceipts(database);
715
+ await options.beforeCommit?.();
716
+ database.exec("COMMIT");
644
717
  database.close();
718
+ // No reader can be holding this new name. An existing same-head file is
719
+ // convergence, never a target for replacement.
720
+ await publishImmutableSnapshot(buildPath, databasePath);
721
+ await garbageCollectSnapshots(databasePath);
722
+ return true;
645
723
  }
646
- catch { /* database may not have opened */ }
647
- throw error;
648
- }
724
+ catch (error) {
725
+ try {
726
+ database.exec("ROLLBACK");
727
+ }
728
+ catch { /* database may not have opened */ }
729
+ try {
730
+ database.close();
731
+ }
732
+ catch { /* database may not have opened */ }
733
+ await discardTemporarySnapshot(buildPath);
734
+ throw error;
735
+ }
736
+ });
649
737
  }
650
738
  export async function advanceRepositoryCurrentState(cwd, pinnedHead, observedHead, segment, options) {
651
- const databasePath = await snapshotPath(cwd, options.signal);
739
+ const pinnedDatabasePath = await repositoryCurrentStateSnapshotPath(cwd, pinnedHead, options.signal);
740
+ const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, options.signal);
652
741
  if (await options.confirmHead() !== observedHead)
653
742
  return false;
654
- const database = openValidated(databasePath);
655
- if (!database)
656
- return false;
657
- try {
658
- database.exec(`PRAGMA query_only = OFF; PRAGMA busy_timeout = ${SNAPSHOT_WRITE_BUSY_MS}; PRAGMA synchronous = FULL; BEGIN IMMEDIATE`);
659
- const pin = metadata(database);
660
- if (!pin || pin.head !== pinnedHead) {
661
- database.exec("ROLLBACK");
743
+ return withSnapshotWriter(databasePath, options.signal, async () => {
744
+ if (await options.confirmHead() !== observedHead)
662
745
  return false;
663
- }
664
- validatePinReceipt(database, pin);
665
- assertReceipt(database, "topology", topologyDigest(database));
666
- assertReceipt(database, "resources", resourcesDigest(database));
667
- for (const contract of new Set(segment.transactions.map((transaction) => transaction.entries[0]?.entry.contract).filter((contract) => contract !== undefined))) {
668
- if (database.prepare("SELECT 1 FROM snapshot_contracts WHERE contract = ?").get(contract))
669
- validateContractReceipt(database, contract, false);
670
- }
671
- const acceptedTopology = new Set(database.prepare("SELECT object_id AS objectId FROM snapshot_topology").all().map((row) => row.objectId));
672
- const pendingJoins = new Map(segment.joins.map((join) => [join.objectId, join]));
673
- const promoteReadyJoins = () => {
674
- let promoted;
675
- do {
676
- promoted = false;
677
- for (const join of pendingJoins.values()) {
678
- if (!join.parents.every((parent) => acceptedTopology.has(parent)))
679
- continue;
680
- database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(join.objectId, JSON.stringify({
681
- objectId: join.objectId,
682
- type: "join",
683
- ledgerParents: join.parents,
684
- }));
685
- acceptedTopology.add(join.objectId);
686
- pendingJoins.delete(join.objectId);
687
- promoted = true;
688
- }
689
- } while (promoted);
690
- };
691
- for (const transaction of segment.transactions) {
692
- promoteReadyJoins();
693
- if ((transaction.predecessor !== null && !acceptedTopology.has(transaction.predecessor)) || transaction.entries.length === 0) {
746
+ const existing = await preparePublicationTarget(databasePath, observedHead);
747
+ if (existing === "published")
748
+ return true;
749
+ if (existing === "transient")
750
+ return false;
751
+ const buildPath = temporarySnapshotPath(databasePath);
752
+ let database;
753
+ try {
754
+ await fs.copyFile(pinnedDatabasePath, buildPath);
755
+ database = new DatabaseSync(buildPath);
756
+ database.exec("PRAGMA synchronous = FULL; BEGIN IMMEDIATE");
757
+ const versions = database.prepare("SELECT version FROM snapshot_schema WHERE singleton = 1").all();
758
+ if (versions.length !== 1 || versions[0]?.version !== REPOSITORY_CURRENT_STATE_VERSION) {
694
759
  database.exec("ROLLBACK");
760
+ database.close();
695
761
  return false;
696
762
  }
697
- if (transaction.entries.some((entry) => database.prepare("SELECT 1 FROM snapshot_history WHERE object_id = ?").get(entry.objectId) !== undefined)) {
763
+ }
764
+ catch {
765
+ try {
766
+ database.close();
767
+ }
768
+ catch { /* replacement recovery owns unusable bytes */ }
769
+ return false;
770
+ }
771
+ try {
772
+ const pin = metadata(database);
773
+ if (!pin || pin.head !== pinnedHead) {
698
774
  database.exec("ROLLBACK");
699
775
  return false;
700
776
  }
701
- const contract = transaction.entries[0].entry.contract;
702
- const row = database.prepare("SELECT status, current_json AS currentJson, fold_json AS foldJson, diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(contract);
703
- let state = row?.foldJson ? parseJson(row.foldJson) : undefined;
704
- const priorModel = row?.currentJson ? currentModel(database, contract, row.currentJson) : null;
705
- const first = transaction.entries[0];
706
- if (first.entry.kind === "bind" && (state || row)) {
707
- const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), {
708
- objectId: first.objectId,
709
- code: "same-contract-divergence",
710
- contract,
711
- detail: "incremental bind diverges from an already admitted contract",
712
- }];
713
- database.prepare("UPDATE snapshot_contracts SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
777
+ validatePinReceipt(database, pin);
778
+ assertReceipt(database, "topology", topologyDigest(database));
779
+ assertReceipt(database, "resources", resourcesDigest(database));
780
+ for (const contract of new Set(segment.transactions.map((transaction) => transaction.entries[0]?.entry.contract).filter((contract) => contract !== undefined))) {
781
+ if (database.prepare("SELECT 1 FROM snapshot_contracts WHERE contract = ?").get(contract))
782
+ validateContractReceipt(database, contract, false);
714
783
  }
715
- else {
716
- if (first.entry.kind !== "bind" && (!state || first.causalParent !== state.entryObjectIds.at(-1))) {
784
+ const acceptedTopology = new Set(database.prepare("SELECT object_id AS objectId FROM snapshot_topology").all().map((row) => row.objectId));
785
+ const pendingJoins = new Map(segment.joins.map((join) => [join.objectId, join]));
786
+ const promoteReadyJoins = () => {
787
+ let promoted;
788
+ do {
789
+ promoted = false;
790
+ for (const join of pendingJoins.values()) {
791
+ if (!join.parents.every((parent) => acceptedTopology.has(parent)))
792
+ continue;
793
+ database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(join.objectId, JSON.stringify({
794
+ objectId: join.objectId,
795
+ type: "join",
796
+ ledgerParents: join.parents,
797
+ }));
798
+ acceptedTopology.add(join.objectId);
799
+ pendingJoins.delete(join.objectId);
800
+ promoted = true;
801
+ }
802
+ } while (promoted);
803
+ };
804
+ for (const transaction of segment.transactions) {
805
+ promoteReadyJoins();
806
+ if ((transaction.predecessor !== null && !acceptedTopology.has(transaction.predecessor)) || transaction.entries.length === 0) {
717
807
  database.exec("ROLLBACK");
718
808
  return false;
719
809
  }
720
- let staged = state === undefined ? undefined : structuredClone(state);
721
- let disease;
722
- const basePosition = staged?.entryObjectIds.length ?? 0;
723
- for (let index = 0; index < transaction.entries.length; index += 1) {
724
- const entry = transaction.entries[index];
725
- const applied = applyFoldEntry(staged, { objectId: entry.objectId, entry: entry.entry, position: basePosition + index });
726
- staged = applied.state;
727
- if (applied.disease) {
728
- disease = { ...applied.disease, transactionObjectId: transaction.objectId };
729
- break;
730
- }
810
+ if (transaction.entries.some((entry) => database.prepare("SELECT 1 FROM snapshot_history WHERE object_id = ?").get(entry.objectId) !== undefined)) {
811
+ database.exec("ROLLBACK");
812
+ return false;
731
813
  }
732
- if (disease || !staged) {
733
- const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), ...(disease ? [disease] : [])];
734
- if (row)
735
- database.prepare("UPDATE snapshot_contracts SET status = 'diseased', diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
736
- else
737
- database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, 'isolated', NULL, NULL, ?)").run(contract, JSON.stringify(diagnostics));
814
+ const contract = transaction.entries[0].entry.contract;
815
+ const row = database.prepare("SELECT status, current_json AS currentJson, fold_json AS foldJson, diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(contract);
816
+ let state = row?.foldJson ? parseJson(row.foldJson) : undefined;
817
+ const priorModel = row?.currentJson ? currentModel(database, contract, row.currentJson) : null;
818
+ const first = transaction.entries[0];
819
+ if (first.entry.kind === "bind" && (state || row)) {
820
+ const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), {
821
+ objectId: first.objectId,
822
+ code: "same-contract-divergence",
823
+ contract,
824
+ detail: "incremental bind diverges from an already admitted contract",
825
+ }];
826
+ database.prepare("UPDATE snapshot_contracts SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
738
827
  }
739
828
  else {
740
- if (first.entry.kind === "bind" && (first.entry.data.after?.length ?? 0) > 0) {
741
- const unavailable = first.entry.data.after.filter((dependency) => {
742
- const prerequisite = database.prepare("SELECT status, fold_json AS foldJson FROM snapshot_contracts WHERE contract = ?").get(dependency);
743
- if (!prerequisite || prerequisite.status !== "healthy" || prerequisite.foldJson === null)
744
- return true;
745
- return parseJson(prerequisite.foldJson).terminalObjectId === null;
746
- });
747
- if (unavailable.length > 0) {
748
- const diagnostic = {
749
- objectId: first.objectId,
750
- code: "causal-prerequisite",
751
- contract,
752
- detail: `after prerequisites are not admitted terminal contracts: ${unavailable.join(", ")}`,
753
- };
754
- database.prepare(`
829
+ if (first.entry.kind !== "bind" && (!state || first.causalParent !== state.entryObjectIds.at(-1))) {
830
+ database.exec("ROLLBACK");
831
+ return false;
832
+ }
833
+ let staged = state === undefined ? undefined : structuredClone(state);
834
+ let disease;
835
+ const basePosition = staged?.entryObjectIds.length ?? 0;
836
+ for (let index = 0; index < transaction.entries.length; index += 1) {
837
+ const entry = transaction.entries[index];
838
+ const applied = applyFoldEntry(staged, { objectId: entry.objectId, entry: entry.entry, position: basePosition + index });
839
+ staged = applied.state;
840
+ if (applied.disease) {
841
+ disease = { ...applied.disease, transactionObjectId: transaction.objectId };
842
+ break;
843
+ }
844
+ }
845
+ if (disease || !staged) {
846
+ const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), ...(disease ? [disease] : [])];
847
+ if (row)
848
+ database.prepare("UPDATE snapshot_contracts SET status = 'diseased', diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
849
+ else
850
+ database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, 'isolated', NULL, NULL, ?)").run(contract, JSON.stringify(diagnostics));
851
+ }
852
+ else {
853
+ if (first.entry.kind === "bind" && (first.entry.data.after?.length ?? 0) > 0) {
854
+ const unavailable = first.entry.data.after.filter((dependency) => {
855
+ const prerequisite = database.prepare("SELECT status, fold_json AS foldJson FROM snapshot_contracts WHERE contract = ?").get(dependency);
856
+ if (!prerequisite || prerequisite.status !== "healthy" || prerequisite.foldJson === null)
857
+ return true;
858
+ return parseJson(prerequisite.foldJson).terminalObjectId === null;
859
+ });
860
+ if (unavailable.length > 0) {
861
+ const diagnostic = {
862
+ objectId: first.objectId,
863
+ code: "causal-prerequisite",
864
+ contract,
865
+ detail: `after prerequisites are not admitted terminal contracts: ${unavailable.join(", ")}`,
866
+ };
867
+ database.prepare(`
755
868
  INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json)
756
869
  VALUES (?, 'isolated', NULL, NULL, ?)
757
870
  ON CONFLICT(contract) DO UPDATE SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = excluded.diagnostics_json
758
871
  `).run(contract, JSON.stringify([diagnostic]));
759
- acceptedTopology.add(transaction.objectId);
760
- database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
761
- objectId: transaction.objectId,
762
- type: "transaction",
763
- ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
764
- verb: transaction.transaction.verb,
765
- contract,
766
- entryObjectIds: transaction.entries.map((entry) => entry.objectId),
767
- finalEntryObjectId: transaction.entries.at(-1).objectId,
768
- }));
769
- continue;
872
+ acceptedTopology.add(transaction.objectId);
873
+ database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
874
+ objectId: transaction.objectId,
875
+ type: "transaction",
876
+ ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
877
+ verb: transaction.transaction.verb,
878
+ contract,
879
+ entryObjectIds: transaction.entries.map((entry) => entry.objectId),
880
+ finalEntryObjectId: transaction.entries.at(-1).objectId,
881
+ }));
882
+ continue;
883
+ }
770
884
  }
771
- }
772
- const history = [...(priorModel?.history ?? []), ...transaction.entries.map((entry) => ({ objectId: entry.objectId, entry: entry.entry }))];
773
- const model = await options.materialize(staged, history);
774
- database.prepare(`
885
+ const history = [...(priorModel?.history ?? []), ...transaction.entries.map((entry) => ({ objectId: entry.objectId, entry: entry.entry }))];
886
+ const model = await options.materialize(staged, history);
887
+ database.prepare(`
775
888
  INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json)
776
889
  VALUES (?, 'healthy', ?, ?, '[]')
777
890
  ON CONFLICT(contract) DO UPDATE SET status = 'healthy', current_json = excluded.current_json, fold_json = excluded.fold_json, diagnostics_json = '[]'
778
891
  `).run(contract, JSON.stringify(Object.fromEntries(Object.entries(model).filter(([key]) => key !== "history"))), JSON.stringify(staged));
779
- database.prepare("DELETE FROM snapshot_history WHERE contract = ?").run(contract);
780
- const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
781
- model.history.forEach((historyRow, position) => insertHistory.run(contract, position, historyRow.objectId, JSON.stringify(historyRow.entry)));
782
- database.prepare("INSERT INTO snapshot_summaries(contract, history_json) VALUES (?, ?) ON CONFLICT(contract) DO UPDATE SET history_json = excluded.history_json").run(contract, JSON.stringify(summaryHistory(model.history)));
783
- database.prepare("DELETE FROM snapshot_resources WHERE contract = ?").run(contract);
784
- const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
785
- if (model.resources.worktree?.state === "held")
786
- insertResource.run("worktree", model.resources.worktree.place, contract);
787
- for (const task of model.resources.tasks)
788
- if (task.state === "held")
789
- insertResource.run("task", task.taskId, contract);
790
- const collisions = [
791
- ...(model.resources.worktree?.state === "held" ? [{ kind: "worktree", key: model.resources.worktree.place }] : []),
792
- ...model.resources.tasks.filter((task) => task.state === "held").map((task) => ({ kind: "task", key: task.taskId })),
793
- ];
794
- for (const resource of collisions) {
795
- const owners = database.prepare("SELECT contract FROM snapshot_resources WHERE kind = ? AND resource_key = ?").all(resource.kind, resource.key);
796
- if (owners.length < 2)
797
- continue;
798
- for (const owner of owners) {
799
- const ownerRow = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(owner.contract);
800
- const diagnostic = {
801
- objectId: first.objectId,
802
- code: resource.kind === "worktree" ? "worktree-place-collision" : "task-ownership-collision",
803
- contract: owner.contract,
804
- detail: `${resource.kind} ${resource.key} is held by ${owners.map((candidate) => candidate.contract).sort().join(", ")}`,
805
- };
806
- database.prepare("UPDATE snapshot_contracts SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = ? WHERE contract = ?").run(JSON.stringify([...parseJson(ownerRow.diagnosticsJson), diagnostic]), owner.contract);
892
+ database.prepare("DELETE FROM snapshot_history WHERE contract = ?").run(contract);
893
+ const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
894
+ model.history.forEach((historyRow, position) => insertHistory.run(contract, position, historyRow.objectId, JSON.stringify(historyRow.entry)));
895
+ database.prepare("INSERT INTO snapshot_summaries(contract, history_json) VALUES (?, ?) ON CONFLICT(contract) DO UPDATE SET history_json = excluded.history_json").run(contract, JSON.stringify(summaryHistory(model.history)));
896
+ database.prepare("DELETE FROM snapshot_resources WHERE contract = ?").run(contract);
897
+ const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
898
+ if (model.resources.worktree?.state === "held")
899
+ insertResource.run("worktree", model.resources.worktree.place, contract);
900
+ for (const task of model.resources.tasks)
901
+ if (task.state === "held")
902
+ insertResource.run("task", task.taskId, contract);
903
+ const collisions = [
904
+ ...(model.resources.worktree?.state === "held" ? [{ kind: "worktree", key: model.resources.worktree.place }] : []),
905
+ ...model.resources.tasks.filter((task) => task.state === "held").map((task) => ({ kind: "task", key: task.taskId })),
906
+ ];
907
+ for (const resource of collisions) {
908
+ const owners = database.prepare("SELECT contract FROM snapshot_resources WHERE kind = ? AND resource_key = ?").all(resource.kind, resource.key);
909
+ if (owners.length < 2)
910
+ continue;
911
+ for (const owner of owners) {
912
+ const ownerRow = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(owner.contract);
913
+ const diagnostic = {
914
+ objectId: first.objectId,
915
+ code: resource.kind === "worktree" ? "worktree-place-collision" : "task-ownership-collision",
916
+ contract: owner.contract,
917
+ detail: `${resource.kind} ${resource.key} is held by ${owners.map((candidate) => candidate.contract).sort().join(", ")}`,
918
+ };
919
+ database.prepare("UPDATE snapshot_contracts SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = ? WHERE contract = ?").run(JSON.stringify([...parseJson(ownerRow.diagnosticsJson), diagnostic]), owner.contract);
920
+ }
807
921
  }
808
922
  }
809
923
  }
810
- }
811
- const insertLifecycleTopology = database.prepare("INSERT OR IGNORE INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
812
- for (const entry of transaction.entries)
813
- insertLifecycleTopology.run(entry.objectId, JSON.stringify({
814
- objectId: entry.objectId,
815
- type: "lifecycle",
816
- ledgerParents: entry.causalParent === null ? [] : [entry.causalParent],
924
+ const insertLifecycleTopology = database.prepare("INSERT OR IGNORE INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
925
+ for (const entry of transaction.entries)
926
+ insertLifecycleTopology.run(entry.objectId, JSON.stringify({
927
+ objectId: entry.objectId,
928
+ type: "lifecycle",
929
+ ledgerParents: entry.causalParent === null ? [] : [entry.causalParent],
930
+ }));
931
+ database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
932
+ objectId: transaction.objectId,
933
+ type: "transaction",
934
+ ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
935
+ verb: transaction.transaction.verb,
936
+ contract,
937
+ entryObjectIds: transaction.entries.map((entry) => entry.objectId),
938
+ finalEntryObjectId: transaction.entries.at(-1).objectId,
817
939
  }));
818
- database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
819
- objectId: transaction.objectId,
820
- type: "transaction",
821
- ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
822
- verb: transaction.transaction.verb,
823
- contract,
824
- entryObjectIds: transaction.entries.map((entry) => entry.objectId),
825
- finalEntryObjectId: transaction.entries.at(-1).objectId,
826
- }));
827
- acceptedTopology.add(transaction.objectId);
940
+ acceptedTopology.add(transaction.objectId);
941
+ }
942
+ promoteReadyJoins();
943
+ if (pendingJoins.size > 0 || !acceptedTopology.has(observedHead)) {
944
+ database.exec("ROLLBACK");
945
+ return false;
946
+ }
947
+ database.prepare("UPDATE snapshot_contracts SET current_json = json_set(current_json, '$.observedLedgerHead', ?) WHERE current_json IS NOT NULL").run(observedHead);
948
+ database.prepare("UPDATE snapshot_pin SET ledger_head = ? WHERE singleton = 1").run(observedHead);
949
+ writeIntegrityReceipts(database);
950
+ database.exec("COMMIT");
951
+ database.close();
952
+ await publishImmutableSnapshot(buildPath, databasePath);
953
+ await garbageCollectSnapshots(databasePath);
954
+ return true;
828
955
  }
829
- promoteReadyJoins();
830
- if (pendingJoins.size > 0 || !acceptedTopology.has(observedHead)) {
831
- database.exec("ROLLBACK");
956
+ catch {
957
+ try {
958
+ database.exec("ROLLBACK");
959
+ }
960
+ catch { /* handled by recovery */ }
832
961
  return false;
833
962
  }
834
- database.prepare("UPDATE snapshot_contracts SET current_json = json_set(current_json, '$.observedLedgerHead', ?) WHERE current_json IS NOT NULL").run(observedHead);
835
- database.prepare("UPDATE snapshot_pin SET ledger_head = ? WHERE singleton = 1").run(observedHead);
836
- writeIntegrityReceipts(database);
837
- database.exec("COMMIT");
838
- return true;
839
- }
840
- catch {
841
- try {
842
- database.exec("ROLLBACK");
963
+ finally {
964
+ try {
965
+ database.close();
966
+ }
967
+ catch { /* database may not have opened */ }
968
+ await discardTemporarySnapshot(buildPath);
843
969
  }
844
- catch { /* handled by recovery */ }
845
- return false;
846
- }
847
- finally {
848
- database.close();
849
- }
850
- }
851
- export async function discardRepositoryCurrentState(cwd, signal) {
852
- await discard(await snapshotPath(cwd, signal));
970
+ });
853
971
  }