@astrosheep/keiyaku 1.0.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +5 -7
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +29 -24
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -0,0 +1,11 @@
1
+ import { openProjectionGenerationStoreReadOnly, } from "./projection-generation-store.js";
2
+ /** Read the immutable projection identity from the heart database. */
3
+ export function readProjectionIdentity(projectionDirectory) {
4
+ const heart = openProjectionGenerationStoreReadOnly(projectionDirectory);
5
+ try {
6
+ return heart.readIdentity();
7
+ }
8
+ finally {
9
+ heart.close();
10
+ }
11
+ }
@@ -0,0 +1,87 @@
1
+ import { openProjectionGenerationStoreReadOnly, } from "./projection-generation-store.js";
2
+ import { deriveProjectionLife, selectCurrentGeneration, } from "./projection-life-protocol.js";
3
+ import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
4
+ function errorDetail(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ /**
8
+ * Observe the current generation without creating, repairing, or mutating
9
+ * projection storage. Generation history is the ordering authority; the
10
+ * runner lock is consulted only for an adopted, non-terminal generation.
11
+ */
12
+ export function observeProjectionLifeSnapshot(projectionDirectory, options) {
13
+ const readRecords = () => {
14
+ const store = openProjectionGenerationStoreReadOnly(projectionDirectory);
15
+ try {
16
+ return store.readRecords();
17
+ }
18
+ finally {
19
+ store.close();
20
+ }
21
+ };
22
+ for (let attempt = 0; attempt < 4; attempt += 1) {
23
+ let records;
24
+ try {
25
+ records = readRecords();
26
+ }
27
+ catch (error) {
28
+ return {
29
+ life: {
30
+ phase: "unknown",
31
+ diagnostic: `generation history could not be observed: ${errorDetail(error)}`,
32
+ },
33
+ };
34
+ }
35
+ const current = selectCurrentGeneration(records);
36
+ if (!current || current.verdict || !current.adoption) {
37
+ return {
38
+ life: deriveProjectionLife({
39
+ records,
40
+ nowMs: options.nowMs,
41
+ startupTimeoutMs: options.startupTimeoutMs,
42
+ }),
43
+ records,
44
+ };
45
+ }
46
+ const lock = observeProjectionRunnerLock(projectionDirectory, options.platform);
47
+ let after;
48
+ try {
49
+ after = readRecords();
50
+ }
51
+ catch (error) {
52
+ return {
53
+ life: {
54
+ phase: "unknown",
55
+ executionId: current.launch.executionId,
56
+ diagnostic: `generation history could not be re-observed: ${errorDetail(error)}`,
57
+ },
58
+ };
59
+ }
60
+ const beforeTail = records.at(-1)?.seq;
61
+ const afterTail = after.at(-1)?.seq;
62
+ if (records.length !== after.length || beforeTail !== afterTail)
63
+ continue;
64
+ return {
65
+ life: deriveProjectionLife({
66
+ records,
67
+ runnerLock: lock.state === "held"
68
+ ? "held"
69
+ : lock.state === "released"
70
+ ? "released"
71
+ : "unknown",
72
+ nowMs: options.nowMs,
73
+ startupTimeoutMs: options.startupTimeoutMs,
74
+ }),
75
+ records,
76
+ };
77
+ }
78
+ return {
79
+ life: {
80
+ phase: "unknown",
81
+ diagnostic: "generation history changed during runner-lock observation",
82
+ },
83
+ };
84
+ }
85
+ export function observeProjectionLife(projectionDirectory, options) {
86
+ return observeProjectionLifeSnapshot(projectionDirectory, options).life;
87
+ }
@@ -0,0 +1,95 @@
1
+ export const GENERATION_VERDICT_STATES = [
2
+ "completed",
3
+ "failed",
4
+ "launch-failed",
5
+ "lost-runner",
6
+ "dismissed",
7
+ ];
8
+ function asLaunch(record) {
9
+ return record.kind === "launch"
10
+ ? record
11
+ : undefined;
12
+ }
13
+ function asAdoption(record) {
14
+ return record.kind === "adoption"
15
+ ? record
16
+ : undefined;
17
+ }
18
+ function asVerdict(record) {
19
+ return record.kind === "verdict"
20
+ ? record
21
+ : undefined;
22
+ }
23
+ /**
24
+ * Select the current generation using ledger sequence only. The store validates
25
+ * dense sequence and transition legality before records reach this fold.
26
+ */
27
+ export function selectCurrentGeneration(records) {
28
+ let launch;
29
+ for (const record of records) {
30
+ const candidate = asLaunch(record);
31
+ if (candidate)
32
+ launch = candidate;
33
+ }
34
+ if (!launch)
35
+ return null;
36
+ let adoption;
37
+ let verdict;
38
+ for (const record of records) {
39
+ if (record.seq <= launch.seq || record.executionId !== launch.executionId)
40
+ continue;
41
+ adoption ??= asAdoption(record);
42
+ verdict ??= asVerdict(record);
43
+ }
44
+ return {
45
+ launch,
46
+ ...(adoption ? { adoption } : {}),
47
+ ...(verdict ? { verdict } : {}),
48
+ };
49
+ }
50
+ function verdictState(record) {
51
+ const state = record?.facts.state;
52
+ return typeof state === "string"
53
+ && GENERATION_VERDICT_STATES.includes(state)
54
+ ? state
55
+ : undefined;
56
+ }
57
+ function phaseForVerdict(state) {
58
+ switch (state) {
59
+ case "completed": return "done";
60
+ case "dismissed": return "dismissed";
61
+ case "lost-runner": return "lost";
62
+ case "failed":
63
+ case "launch-failed":
64
+ return "failed";
65
+ }
66
+ }
67
+ export function deriveProjectionLife(input) {
68
+ const chain = selectCurrentGeneration(input.records);
69
+ if (!chain)
70
+ return { phase: "quiescent" };
71
+ const executionId = chain.launch.executionId;
72
+ if (chain.verdict) {
73
+ const state = verdictState(chain.verdict);
74
+ return state
75
+ ? { phase: phaseForVerdict(state), executionId, verdict: state }
76
+ : { phase: "unknown", executionId, diagnostic: "current generation has an invalid verdict" };
77
+ }
78
+ if (!chain.adoption) {
79
+ const createdAt = chain.launch.facts.createdAt;
80
+ const launchedAtMs = typeof createdAt === "string" ? Date.parse(createdAt) : Number.NaN;
81
+ if (!Number.isFinite(launchedAtMs)) {
82
+ return { phase: "unknown", executionId, diagnostic: "current launch has an invalid createdAt fact" };
83
+ }
84
+ return input.nowMs - launchedAtMs >= input.startupTimeoutMs
85
+ ? { phase: "startup-timeout", executionId }
86
+ : { phase: "minting", executionId };
87
+ }
88
+ switch (input.runnerLock) {
89
+ case "held": return { phase: "active", executionId };
90
+ case "released": return { phase: "lost", executionId };
91
+ case "unknown":
92
+ case undefined:
93
+ return { phase: "unknown", executionId, diagnostic: "runner lock could not be determined" };
94
+ }
95
+ }
@@ -1,8 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { isErrnoException } from "../errno.js";
4
- import { atomicPublishFile, ProjectionStateError, randomHex8 } from "./atomic-publish.js";
5
- import { encodeProjectionJson, isoFromNowMs, PROJECTION_LAYER_DIRS, PROJECTION_ROOT_SEGMENTS, projectionDir, projectionRoot, } from "./projection-core.js";
4
+ import { ProjectionStateError, randomHex8 } from "./atomic-publish.js";
5
+ import { isoFromNowMs, PROJECTION_ROOT_SEGMENTS, projectionDir, projectionRoot, } from "./projection-core.js";
6
+ import { parseExecutionAuthoritySelection, parseExecutionPact, parseExecutionPactFields, } from "./execution-pact.js";
7
+ import { openProjectionGenerationStore } from "./projection-generation-store.js";
8
+ import { initializeProjectionRunnerLockDatabase } from "./projection-runner-lock.js";
6
9
  export class ProjectionMintCollisionError extends Error {
7
10
  code = "PROJECTION_MINT_COLLISION";
8
11
  constructor(message, options) {
@@ -22,11 +25,10 @@ function assertMintSlug(slug) {
22
25
  return normalized;
23
26
  }
24
27
  /**
25
- * Mint a projection directory.
26
- * Order (closed): mkdirSync(id) pact.json + heartbeat + five layer dirs → ready last via atomicPublishFile.
27
- * Readers ignore directories without `ready`.
28
+ * Validate mint inputs that can fail, then reserve one private staging directory.
29
+ * Dot-prefixed staging is never a published projection address.
28
30
  */
29
- export function mintProjection(input) {
31
+ function reserveProjectionDirectory(input) {
30
32
  const slug = assertMintSlug(input.slug);
31
33
  if (!Number.isInteger(input.pid) || input.pid < 0) {
32
34
  throw new ProjectionStateError("projection pid must be a non-negative integer");
@@ -47,8 +49,11 @@ export function mintProjection(input) {
47
49
  }
48
50
  const id = `${slug}-${hex}`;
49
51
  const dir = projectionDir(input.cwd, id);
52
+ const stagingDir = path.join(root, `.mint-${id}`);
53
+ if (fs.existsSync(dir))
54
+ continue;
50
55
  try {
51
- fs.mkdirSync(dir);
56
+ fs.mkdirSync(stagingDir);
52
57
  }
53
58
  catch (error) {
54
59
  if (isErrnoException(error) && error.code === "EEXIST") {
@@ -56,24 +61,91 @@ export function mintProjection(input) {
56
61
  }
57
62
  throw error;
58
63
  }
59
- const pact = {
60
- version: 1,
61
- pid: input.pid,
62
- akuma: input.akuma,
63
- provider: input.provider,
64
- mintedAt: isoFromNowMs(input.nowMs),
65
- ...(input.effectivePolicy ? { effectivePolicy: input.effectivePolicy } : {}),
66
- };
67
- fs.writeFileSync(path.join(dir, "pact.json"), encodeProjectionJson(pact), "utf8");
68
- fs.writeFileSync(path.join(dir, "heartbeat"), Buffer.alloc(0));
69
- for (const layer of PROJECTION_LAYER_DIRS) {
70
- fs.mkdirSync(path.join(dir, layer));
71
- }
72
- const ready = atomicPublishFile(path.join(dir, "ready"), Buffer.alloc(0));
73
- if (ready === "taken") {
74
- throw new ProjectionStateError(`ready marker already present while minting ${id}`);
64
+ if (fs.existsSync(dir)) {
65
+ fs.rmdirSync(stagingDir);
66
+ continue;
75
67
  }
76
- return { id, dir, pact };
68
+ return { id, dir, stagingDir };
77
69
  }
78
70
  throw new ProjectionMintCollisionError(`exhausted ${maxAttempts} projection id attempts under ${PROJECTION_ROOT_SEGMENTS.join("/")} (PROJECTION_MINT_COLLISION)`);
79
71
  }
72
+ /** Build the pact fields shared by both authority variants. */
73
+ function buildMintPactFields(input) {
74
+ return {
75
+ version: 1,
76
+ pid: input.pid,
77
+ akuma: input.akuma,
78
+ provider: input.provider,
79
+ binding: input.binding,
80
+ mintedAt: isoFromNowMs(input.nowMs),
81
+ workspaceRoot: input.workspaceRoot ?? path.resolve(input.cwd),
82
+ ...(input.effectivePolicy ? { effectivePolicy: input.effectivePolicy } : {}),
83
+ };
84
+ }
85
+ /**
86
+ * Build both hard projection organs in private staging, close every database
87
+ * handle, then publish the complete projection with one directory rename.
88
+ */
89
+ function publishReservedProjection(pact, reserved) {
90
+ let published = false;
91
+ try {
92
+ const heart = openProjectionGenerationStore(reserved.stagingDir);
93
+ try {
94
+ heart.initializeIdentity(pact);
95
+ }
96
+ finally {
97
+ heart.close();
98
+ }
99
+ initializeProjectionRunnerLockDatabase(reserved.stagingDir);
100
+ fs.renameSync(reserved.stagingDir, reserved.dir);
101
+ published = true;
102
+ return { id: reserved.id, dir: reserved.dir, pact };
103
+ }
104
+ finally {
105
+ if (!published) {
106
+ fs.rmSync(reserved.stagingDir, { recursive: true, force: true });
107
+ }
108
+ }
109
+ }
110
+ /**
111
+ * Mint a user-authority projection directory.
112
+ * Order (closed): validate → reserve staging → heart + leash → atomic rename.
113
+ */
114
+ export function mintProjection(input) {
115
+ const authority = parseExecutionAuthoritySelection(input.authority, "projection identity", "user");
116
+ const fields = parseExecutionPactFields(buildMintPactFields(input));
117
+ const pact = parseExecutionPact({
118
+ version: fields.version,
119
+ pid: fields.pid,
120
+ akuma: fields.akuma,
121
+ provider: fields.provider,
122
+ authority,
123
+ binding: fields.binding,
124
+ mintedAt: fields.mintedAt,
125
+ workspaceRoot: fields.workspaceRoot,
126
+ ...(fields.effectivePolicy !== undefined ? { effectivePolicy: fields.effectivePolicy } : {}),
127
+ });
128
+ const reserved = reserveProjectionDirectory(input);
129
+ return publishReservedProjection(pact, reserved);
130
+ }
131
+ /**
132
+ * Mint a repository-authority projection.
133
+ * Order: validate complete identity → reserve staging → heart + leash → atomic rename.
134
+ */
135
+ export async function mintRepositoryProjection(input) {
136
+ parseExecutionAuthoritySelection(input.authority, "projection identity", "repository");
137
+ const fields = parseExecutionPactFields(buildMintPactFields(input));
138
+ const reserved = reserveProjectionDirectory(input);
139
+ const pact = parseExecutionPact({
140
+ version: fields.version,
141
+ pid: fields.pid,
142
+ akuma: fields.akuma,
143
+ provider: fields.provider,
144
+ authority: { kind: "repository" },
145
+ binding: fields.binding,
146
+ mintedAt: fields.mintedAt,
147
+ workspaceRoot: fields.workspaceRoot,
148
+ ...(fields.effectivePolicy !== undefined ? { effectivePolicy: fields.effectivePolicy } : {}),
149
+ });
150
+ return publishReservedProjection(pact, reserved);
151
+ }
@@ -0,0 +1,230 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ export const PROJECTION_RUNNER_LOCK_DATABASE = "leash";
5
+ const SQLITE_BUSY = 5;
6
+ const SUPPORTED_PLATFORMS = new Set(["darwin", "linux"]);
7
+ export class ProjectionRunnerLockUnavailableError extends Error {
8
+ code = "PROJECTION_RUNNER_LOCK_UNAVAILABLE";
9
+ constructor(message, options) {
10
+ super(message, options);
11
+ this.name = "ProjectionRunnerLockUnavailableError";
12
+ }
13
+ }
14
+ export class ProjectionRunnerLockUnsupportedError extends Error {
15
+ code = "PROJECTION_RUNNER_LOCK_UNSUPPORTED";
16
+ constructor(platform) {
17
+ super(`projection runner locks are unsupported on platform '${platform}'`);
18
+ this.name = "ProjectionRunnerLockUnsupportedError";
19
+ }
20
+ }
21
+ export function projectionRunnerLockPath(projectionDirectory) {
22
+ return path.join(projectionDirectory, PROJECTION_RUNNER_LOCK_DATABASE);
23
+ }
24
+ export function isProjectionRunnerLockPlatformSupported(platform = process.platform) {
25
+ return SUPPORTED_PLATFORMS.has(platform);
26
+ }
27
+ function errorDetail(error) {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+ function isSqliteBusy(error) {
31
+ return typeof error === "object"
32
+ && error !== null
33
+ && "errcode" in error
34
+ && error.errcode === SQLITE_BUSY;
35
+ }
36
+ function readJournalMode(database) {
37
+ const row = database.prepare("PRAGMA journal_mode").get();
38
+ return typeof row?.journal_mode === "string" ? row.journal_mode.toLowerCase() : undefined;
39
+ }
40
+ function assertCurrentLockSchema(database) {
41
+ database.prepare("SELECT id FROM lock_anchor LIMIT 1").get();
42
+ }
43
+ function closeAfterFailedAcquire(database, error) {
44
+ try {
45
+ database.close();
46
+ }
47
+ catch {
48
+ // The acquisition error remains the causal failure. Closing a connection
49
+ // that never became authoritative cannot turn it into a held lock.
50
+ }
51
+ if (isSqliteBusy(error)) {
52
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock is already held", { cause: error });
53
+ }
54
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock acquisition failed: ${errorDetail(error)}`, { cause: error });
55
+ }
56
+ /**
57
+ * Mint-time initialization for the fixed lock database. This creates storage,
58
+ * forces rollback-journal mode, verifies the anchor schema, and closes without
59
+ * retaining any process-life authority.
60
+ */
61
+ export function initializeProjectionRunnerLockDatabase(projectionDirectory, platform = process.platform) {
62
+ if (!isProjectionRunnerLockPlatformSupported(platform)) {
63
+ throw new ProjectionRunnerLockUnsupportedError(platform);
64
+ }
65
+ const lockPath = projectionRunnerLockPath(projectionDirectory);
66
+ let database;
67
+ try {
68
+ database = new DatabaseSync(lockPath);
69
+ }
70
+ catch (error) {
71
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock database could not be initialized: ${errorDetail(error)}`, { cause: error });
72
+ }
73
+ let initializationError;
74
+ try {
75
+ database.exec("PRAGMA busy_timeout=0");
76
+ database.exec("PRAGMA journal_mode=DELETE");
77
+ database.exec("CREATE TABLE IF NOT EXISTS lock_anchor (id INTEGER PRIMARY KEY CHECK (id = 1))");
78
+ if (readJournalMode(database) !== "delete") {
79
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
80
+ }
81
+ assertCurrentLockSchema(database);
82
+ }
83
+ catch (error) {
84
+ initializationError = error;
85
+ }
86
+ try {
87
+ database.close();
88
+ }
89
+ catch (error) {
90
+ initializationError ??= error;
91
+ }
92
+ if (initializationError !== undefined) {
93
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock database initialization failed: ${errorDetail(initializationError)}`, { cause: initializationError });
94
+ }
95
+ return lockPath;
96
+ }
97
+ /**
98
+ * Open the already initialized projection-local lock database and hold one
99
+ * rollback-mode EXCLUSIVE transaction until close() or process death. SQLite
100
+ * requires a writable connection for this write-grade lock; the preceding
101
+ * regular-file check prevents ordinary missing storage from being created.
102
+ * Database bytes and timestamps are deliberately absent from the returned authority.
103
+ */
104
+ export function acquireProjectionRunnerLock(projectionDirectory, platform = process.platform) {
105
+ if (!isProjectionRunnerLockPlatformSupported(platform)) {
106
+ throw new ProjectionRunnerLockUnsupportedError(platform);
107
+ }
108
+ const lockPath = projectionRunnerLockPath(projectionDirectory);
109
+ try {
110
+ if (!fs.lstatSync(lockPath).isFile()) {
111
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock database is not a regular file");
112
+ }
113
+ }
114
+ catch (error) {
115
+ if (error instanceof ProjectionRunnerLockUnavailableError)
116
+ throw error;
117
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock database is not initialized: ${errorDetail(error)}`, { cause: error });
118
+ }
119
+ let database;
120
+ try {
121
+ database = new DatabaseSync(lockPath);
122
+ }
123
+ catch (error) {
124
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock database could not be opened: ${errorDetail(error)}`, { cause: error });
125
+ }
126
+ try {
127
+ database.exec("PRAGMA busy_timeout=0");
128
+ database.exec("BEGIN EXCLUSIVE");
129
+ if (readJournalMode(database) !== "delete") {
130
+ throw new ProjectionRunnerLockUnavailableError("projection runner lock database does not use rollback-journal mode");
131
+ }
132
+ assertCurrentLockSchema(database);
133
+ }
134
+ catch (error) {
135
+ try {
136
+ database.exec("ROLLBACK");
137
+ }
138
+ catch {
139
+ // A failure before BEGIN has no transaction; a failure after BEGIN is
140
+ // still released by closeAfterFailedAcquire if rollback cannot run.
141
+ }
142
+ return closeAfterFailedAcquire(database, error);
143
+ }
144
+ let closed = false;
145
+ return {
146
+ path: lockPath,
147
+ close() {
148
+ if (closed)
149
+ return;
150
+ closed = true;
151
+ let rollbackError;
152
+ try {
153
+ database.exec("ROLLBACK");
154
+ }
155
+ catch (error) {
156
+ rollbackError = error;
157
+ }
158
+ try {
159
+ database.close();
160
+ }
161
+ catch (error) {
162
+ rollbackError ??= error;
163
+ }
164
+ if (rollbackError !== undefined) {
165
+ throw new ProjectionRunnerLockUnavailableError(`projection runner lock release failed: ${errorDetail(rollbackError)}`, { cause: rollbackError });
166
+ }
167
+ },
168
+ };
169
+ }
170
+ /**
171
+ * Probe without creating the lock database. BEGIN DEFERRED itself acquires no
172
+ * file lock, so the sqlite_schema SELECT is mandatory: SQLITE_BUSY there is
173
+ * positive evidence of the runner's EXCLUSIVE transaction. Successful reads
174
+ * hold a compatible SHARED transaction only long enough to verify DELETE
175
+ * journal mode, then roll back. Every other failure remains unknown.
176
+ */
177
+ export function observeProjectionRunnerLock(projectionDirectory, platform = process.platform) {
178
+ if (!isProjectionRunnerLockPlatformSupported(platform)) {
179
+ return {
180
+ state: "unknown",
181
+ reason: "unsupported-platform",
182
+ detail: `projection runner locks are unsupported on platform '${platform}'`,
183
+ };
184
+ }
185
+ const lockPath = projectionRunnerLockPath(projectionDirectory);
186
+ let database;
187
+ try {
188
+ database = new DatabaseSync(lockPath, { readOnly: true });
189
+ }
190
+ catch (error) {
191
+ return { state: "unknown", reason: "open-failed", detail: errorDetail(error) };
192
+ }
193
+ let transactionOpen = false;
194
+ try {
195
+ database.exec("PRAGMA busy_timeout=0");
196
+ database.exec("BEGIN DEFERRED");
197
+ transactionOpen = true;
198
+ assertCurrentLockSchema(database);
199
+ const journalMode = readJournalMode(database);
200
+ if (journalMode !== "delete") {
201
+ return {
202
+ state: "unknown",
203
+ reason: "unsupported-journal-mode",
204
+ detail: `projection runner lock database uses journal mode '${journalMode ?? "unknown"}'`,
205
+ };
206
+ }
207
+ return { state: "released" };
208
+ }
209
+ catch (error) {
210
+ if (isSqliteBusy(error))
211
+ return { state: "held" };
212
+ return { state: "unknown", reason: "probe-failed", detail: errorDetail(error) };
213
+ }
214
+ finally {
215
+ if (transactionOpen) {
216
+ try {
217
+ database.exec("ROLLBACK");
218
+ }
219
+ catch {
220
+ // Closing below still drops this observer's own ephemeral SHARED lock.
221
+ }
222
+ }
223
+ try {
224
+ database.close();
225
+ }
226
+ catch {
227
+ // Observer cleanup failures cannot be converted into death evidence.
228
+ }
229
+ }
230
+ }