taskflow-mcp-core 0.3.0-beta.1.1 → 1.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.
Files changed (185) hide show
  1. package/CONTROL_GUIDE.md +113 -0
  2. package/README.md +34 -25
  3. package/dist/control/admission-service.d.ts +15 -0
  4. package/dist/control/admission-service.d.ts.map +1 -0
  5. package/dist/control/admission-service.js +64 -0
  6. package/dist/control/admission-service.js.map +1 -0
  7. package/dist/control/approval-edits.d.ts +12 -0
  8. package/dist/control/approval-edits.d.ts.map +1 -0
  9. package/dist/control/approval-edits.js +66 -0
  10. package/dist/control/approval-edits.js.map +1 -0
  11. package/dist/control/approval-service.d.ts +173 -0
  12. package/dist/control/approval-service.d.ts.map +1 -0
  13. package/dist/control/approval-service.js +342 -0
  14. package/dist/control/approval-service.js.map +1 -0
  15. package/dist/control/authorization.d.ts +94 -0
  16. package/dist/control/authorization.d.ts.map +1 -0
  17. package/dist/control/authorization.js +302 -0
  18. package/dist/control/authorization.js.map +1 -0
  19. package/dist/control/control-cli.d.ts +3 -0
  20. package/dist/control/control-cli.d.ts.map +1 -0
  21. package/dist/control/control-cli.js +279 -0
  22. package/dist/control/control-cli.js.map +1 -0
  23. package/dist/control/control-console.d.ts +17 -0
  24. package/dist/control/control-console.d.ts.map +1 -0
  25. package/dist/control/control-console.js +67 -0
  26. package/dist/control/control-console.js.map +1 -0
  27. package/dist/control/control-host.d.ts +98 -0
  28. package/dist/control/control-host.d.ts.map +1 -0
  29. package/dist/control/control-host.js +459 -0
  30. package/dist/control/control-host.js.map +1 -0
  31. package/dist/control/control-mcp.d.ts +11 -0
  32. package/dist/control/control-mcp.d.ts.map +1 -0
  33. package/dist/control/control-mcp.js +78 -0
  34. package/dist/control/control-mcp.js.map +1 -0
  35. package/dist/control/errors.d.ts +44 -0
  36. package/dist/control/errors.d.ts.map +1 -0
  37. package/dist/control/errors.js +93 -0
  38. package/dist/control/errors.js.map +1 -0
  39. package/dist/control/hello.d.ts +39 -0
  40. package/dist/control/hello.d.ts.map +1 -0
  41. package/dist/control/hello.js +80 -0
  42. package/dist/control/hello.js.map +1 -0
  43. package/dist/control/host-runtime.d.ts +32 -0
  44. package/dist/control/host-runtime.d.ts.map +1 -0
  45. package/dist/control/host-runtime.js +542 -0
  46. package/dist/control/host-runtime.js.map +1 -0
  47. package/dist/control/index.d.ts +32 -0
  48. package/dist/control/index.d.ts.map +1 -0
  49. package/dist/control/index.js +32 -0
  50. package/dist/control/index.js.map +1 -0
  51. package/dist/control/legacy-conflict.d.ts +19 -0
  52. package/dist/control/legacy-conflict.d.ts.map +1 -0
  53. package/dist/control/legacy-conflict.js +176 -0
  54. package/dist/control/legacy-conflict.js.map +1 -0
  55. package/dist/control/local-bootstrap.d.ts +21 -0
  56. package/dist/control/local-bootstrap.d.ts.map +1 -0
  57. package/dist/control/local-bootstrap.js +224 -0
  58. package/dist/control/local-bootstrap.js.map +1 -0
  59. package/dist/control/modes.d.ts +60 -0
  60. package/dist/control/modes.d.ts.map +1 -0
  61. package/dist/control/modes.js +86 -0
  62. package/dist/control/modes.js.map +1 -0
  63. package/dist/control/project-admin-cli.d.ts +3 -0
  64. package/dist/control/project-admin-cli.d.ts.map +1 -0
  65. package/dist/control/project-admin-cli.js +155 -0
  66. package/dist/control/project-admin-cli.js.map +1 -0
  67. package/dist/control/project-registry.d.ts +20 -0
  68. package/dist/control/project-registry.d.ts.map +1 -0
  69. package/dist/control/project-registry.js +181 -0
  70. package/dist/control/project-registry.js.map +1 -0
  71. package/dist/control/runtime-checkpoint.d.ts +11 -0
  72. package/dist/control/runtime-checkpoint.d.ts.map +1 -0
  73. package/dist/control/runtime-checkpoint.js +39 -0
  74. package/dist/control/runtime-checkpoint.js.map +1 -0
  75. package/dist/control/runtime-provider.d.ts +100 -0
  76. package/dist/control/runtime-provider.d.ts.map +1 -0
  77. package/dist/control/runtime-provider.js +258 -0
  78. package/dist/control/runtime-provider.js.map +1 -0
  79. package/dist/control/schema/approval.d.ts +108 -0
  80. package/dist/control/schema/approval.d.ts.map +1 -0
  81. package/dist/control/schema/approval.js +65 -0
  82. package/dist/control/schema/approval.js.map +1 -0
  83. package/dist/control/schema/commands.d.ts +663 -0
  84. package/dist/control/schema/commands.d.ts.map +1 -0
  85. package/dist/control/schema/commands.js +147 -0
  86. package/dist/control/schema/commands.js.map +1 -0
  87. package/dist/control/schema/common.d.ts +24 -0
  88. package/dist/control/schema/common.d.ts.map +1 -0
  89. package/dist/control/schema/common.js +38 -0
  90. package/dist/control/schema/common.js.map +1 -0
  91. package/dist/control/schema/coordinator.d.ts +98 -0
  92. package/dist/control/schema/coordinator.d.ts.map +1 -0
  93. package/dist/control/schema/coordinator.js +87 -0
  94. package/dist/control/schema/coordinator.js.map +1 -0
  95. package/dist/control/schema/evidence.d.ts +140 -0
  96. package/dist/control/schema/evidence.d.ts.map +1 -0
  97. package/dist/control/schema/evidence.js +64 -0
  98. package/dist/control/schema/evidence.js.map +1 -0
  99. package/dist/control/schema/header.d.ts +79 -0
  100. package/dist/control/schema/header.d.ts.map +1 -0
  101. package/dist/control/schema/header.js +66 -0
  102. package/dist/control/schema/header.js.map +1 -0
  103. package/dist/control/schema/index.d.ts +31 -0
  104. package/dist/control/schema/index.d.ts.map +1 -0
  105. package/dist/control/schema/index.js +31 -0
  106. package/dist/control/schema/index.js.map +1 -0
  107. package/dist/control/schema/plan.d.ts +291 -0
  108. package/dist/control/schema/plan.d.ts.map +1 -0
  109. package/dist/control/schema/plan.js +80 -0
  110. package/dist/control/schema/plan.js.map +1 -0
  111. package/dist/control/schema/policy.d.ts +96 -0
  112. package/dist/control/schema/policy.d.ts.map +1 -0
  113. package/dist/control/schema/policy.js +73 -0
  114. package/dist/control/schema/policy.js.map +1 -0
  115. package/dist/control/schema/run.d.ts +61 -0
  116. package/dist/control/schema/run.d.ts.map +1 -0
  117. package/dist/control/schema/run.js +56 -0
  118. package/dist/control/schema/run.js.map +1 -0
  119. package/dist/control/schema/te-mirrors.d.ts +145 -0
  120. package/dist/control/schema/te-mirrors.d.ts.map +1 -0
  121. package/dist/control/schema/te-mirrors.js +145 -0
  122. package/dist/control/schema/te-mirrors.js.map +1 -0
  123. package/dist/control/schema/transport.d.ts +479 -0
  124. package/dist/control/schema/transport.d.ts.map +1 -0
  125. package/dist/control/schema/transport.js +181 -0
  126. package/dist/control/schema/transport.js.map +1 -0
  127. package/dist/control/singleton.d.ts +135 -0
  128. package/dist/control/singleton.d.ts.map +1 -0
  129. package/dist/control/singleton.js +607 -0
  130. package/dist/control/singleton.js.map +1 -0
  131. package/dist/control/store/coordinator-store.d.ts +143 -0
  132. package/dist/control/store/coordinator-store.d.ts.map +1 -0
  133. package/dist/control/store/coordinator-store.js +620 -0
  134. package/dist/control/store/coordinator-store.js.map +1 -0
  135. package/dist/control/store/evidence-adapter.d.ts +10 -0
  136. package/dist/control/store/evidence-adapter.d.ts.map +1 -0
  137. package/dist/control/store/evidence-adapter.js +114 -0
  138. package/dist/control/store/evidence-adapter.js.map +1 -0
  139. package/dist/control/store/evidence-explanation.d.ts +55 -0
  140. package/dist/control/store/evidence-explanation.d.ts.map +1 -0
  141. package/dist/control/store/evidence-explanation.js +50 -0
  142. package/dist/control/store/evidence-explanation.js.map +1 -0
  143. package/dist/control/store/evidence-store.d.ts +143 -0
  144. package/dist/control/store/evidence-store.d.ts.map +1 -0
  145. package/dist/control/store/evidence-store.js +437 -0
  146. package/dist/control/store/evidence-store.js.map +1 -0
  147. package/dist/control/store/index.d.ts +3 -0
  148. package/dist/control/store/index.d.ts.map +1 -0
  149. package/dist/control/store/index.js +2 -0
  150. package/dist/control/store/index.js.map +1 -0
  151. package/dist/control/store/journal.d.ts +270 -0
  152. package/dist/control/store/journal.d.ts.map +1 -0
  153. package/dist/control/store/journal.js +50 -0
  154. package/dist/control/store/journal.js.map +1 -0
  155. package/dist/control/store/project-lifecycle.d.ts +49 -0
  156. package/dist/control/store/project-lifecycle.d.ts.map +1 -0
  157. package/dist/control/store/project-lifecycle.js +288 -0
  158. package/dist/control/store/project-lifecycle.js.map +1 -0
  159. package/dist/control/store/store.d.ts +93 -0
  160. package/dist/control/store/store.d.ts.map +1 -0
  161. package/dist/control/store/store.js +873 -0
  162. package/dist/control/store/store.js.map +1 -0
  163. package/dist/control/te-provider.d.ts +86 -0
  164. package/dist/control/te-provider.d.ts.map +1 -0
  165. package/dist/control/te-provider.js +131 -0
  166. package/dist/control/te-provider.js.map +1 -0
  167. package/dist/control/uds.d.ts +114 -0
  168. package/dist/control/uds.d.ts.map +1 -0
  169. package/dist/control/uds.js +456 -0
  170. package/dist/control/uds.js.map +1 -0
  171. package/dist/control/web-console/view.d.ts +5 -0
  172. package/dist/control/web-console/view.d.ts.map +1 -0
  173. package/dist/control/web-console/view.js +122 -0
  174. package/dist/control/web-console/view.js.map +1 -0
  175. package/dist/control/web-console.d.ts +50 -0
  176. package/dist/control/web-console.d.ts.map +1 -0
  177. package/dist/control/web-console.js +317 -0
  178. package/dist/control/web-console.js.map +1 -0
  179. package/dist/mcp/jsonrpc.d.ts.map +1 -1
  180. package/dist/mcp/jsonrpc.js +12 -3
  181. package/dist/mcp/jsonrpc.js.map +1 -1
  182. package/dist/mcp/server.d.ts.map +1 -1
  183. package/dist/mcp/server.js +26 -17
  184. package/dist/mcp/server.js.map +1 -1
  185. package/package.json +13 -5
@@ -0,0 +1,620 @@
1
+ /** P16 user-private, files-only capacity authority. Project Runs/Receipts stay
2
+ * in project ledgers. Every operation re-reads one atomically published state
3
+ * under the shared persistent mutex; no process-local capacity cache exists. */
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { randomUUID } from "node:crypto";
7
+ import { Type } from "typebox";
8
+ import { Value } from "typebox/value";
9
+ import { PersistentFileMutex } from "taskflow-core/persistent-mutex";
10
+ import { ControlError } from "../errors.js";
11
+ import { commandRequestHash } from "../schema/commands.js";
12
+ import { UuidSchema, Sha256HexSchema } from "../schema/common.js";
13
+ import { CoordinatorLeaseSchema, CoordinatorCommandRecordSchema, ConcurrencyReservationSchema, assertReservationInvariants } from "../schema/coordinator.js";
14
+ import { writeJsonAtomicHardened } from "./store.js";
15
+ const integer = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
16
+ const text = Type.String({ minLength: 1 });
17
+ const rowSchema = Type.Object({ reservation: ConcurrencyReservationSchema, ownerId: text, principal: text, updatedAt: integer,
18
+ admitRunVersion: Type.Optional(integer), releaseKind: Type.Optional(Type.Union([Type.Literal("normal"), Type.Literal("force")])),
19
+ releaseProofId: Type.Optional(text), concurrencyGuarantee: Type.Union([Type.Literal("enforced"), Type.Literal("operator-overridden")]) }, { additionalProperties: false });
20
+ const operatorRequestSchema = Type.Union([
21
+ Type.Object({ kind: Type.Literal("setMaxActiveRuns"), maxActiveRuns: Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }) }, { additionalProperties: false }),
22
+ Type.Object({ kind: Type.Literal("forceRelease"), reservationId: UuidSchema, riskAcknowledgement: Type.Literal(true), reason: text }, { additionalProperties: false }),
23
+ ]);
24
+ const commandSchema = Type.Object({ request: Type.Optional(operatorRequestSchema), record: CoordinatorCommandRecordSchema, result: Type.Union([rowSchema, Type.Integer({ minimum: 1 })]) }, { additionalProperties: false });
25
+ const auditSchema = Type.Object({ commitSeq: integer, epoch: integer, recordedAt: integer, action: text, principal: text,
26
+ previousHash: Sha256HexSchema, stateDigest: Sha256HexSchema, hash: Sha256HexSchema }, { additionalProperties: false });
27
+ const stateSchema = Type.Object({ version: Type.Literal(1), fencingEpoch: integer, commitSeq: integer, lastWallTime: integer,
28
+ directoryIdentity: Type.Object({ canonicalPath: text, dev: integer, ino: integer }, { additionalProperties: false }),
29
+ instanceId: UuidSchema,
30
+ maxActiveRuns: Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }), reservations: Type.Record(Type.String(), rowSchema),
31
+ commands: Type.Record(Type.String(), commandSchema), audit: Type.Array(auditSchema), stateHash: Sha256HexSchema }, { additionalProperties: false });
32
+ const ZERO_HASH = "0".repeat(64);
33
+ export const COORDINATOR_CRASH_ENV = "TASKFLOW_COORDINATOR_CRASH_AT";
34
+ const active = (row) => ["reserved", "committed", "orphan-suspect"].includes(row.reservation.state);
35
+ const fail = (code, message) => { throw new ControlError(code, message, { recoveryAction: "operator", sideEffects: "none" }); };
36
+ const validId = (id) => { if (!Value.Check(UuidSchema, id))
37
+ fail("TF_COMMAND_FAILED", "coordinator identifier must be a UUID"); };
38
+ const positive = (value, name) => { if (!Number.isSafeInteger(value) || value < 1)
39
+ fail("TF_COMMAND_FAILED", `${name} must be a positive safe integer`); };
40
+ function capacity(state) {
41
+ const rows = Object.values(state.reservations);
42
+ const reserved = rows.filter((r) => r.reservation.state === "reserved").length;
43
+ const committed = rows.filter((r) => r.reservation.state === "committed").length;
44
+ const orphanSuspect = rows.filter((r) => r.reservation.state === "orphan-suspect").length;
45
+ return { maxActiveRuns: state.maxActiveRuns, active: reserved + committed + orphanSuspect, reserved, committed, orphanSuspect };
46
+ }
47
+ function businessDigest(state) {
48
+ return commandRequestHash({ fencingEpoch: state.fencingEpoch, maxActiveRuns: state.maxActiveRuns, reservations: state.reservations, commands: state.commands });
49
+ }
50
+ function hashState(state) { const { stateHash: _, ...body } = state; return commandRequestHash(body); }
51
+ function assertNoSymlink(file) {
52
+ try {
53
+ if (fs.lstatSync(file).isSymbolicLink())
54
+ fail("TF_DURABILITY_FAILED", `coordinator refuses symlink: ${file}`);
55
+ }
56
+ catch (error) {
57
+ if (error.code !== "ENOENT")
58
+ throw error;
59
+ }
60
+ }
61
+ function prepareDirectory(directory) {
62
+ const missing = [];
63
+ let ancestor = path.resolve(directory);
64
+ while (!fs.existsSync(ancestor)) {
65
+ missing.unshift(path.basename(ancestor));
66
+ ancestor = path.dirname(ancestor);
67
+ }
68
+ assertNoSymlink(ancestor);
69
+ let canonical = fs.realpathSync(ancestor);
70
+ for (const segment of missing) {
71
+ canonical = path.join(canonical, segment);
72
+ try {
73
+ fs.mkdirSync(canonical, { mode: 0o700 });
74
+ }
75
+ catch (error) {
76
+ if (error.code !== "EEXIST")
77
+ throw error;
78
+ }
79
+ assertNoSymlink(canonical);
80
+ if (!fs.lstatSync(canonical).isDirectory())
81
+ fail("TF_DURABILITY_FAILED", "coordinator path component is not a directory");
82
+ }
83
+ const stat = fs.lstatSync(canonical);
84
+ if (!stat.isDirectory() || (process.getuid && stat.uid !== process.getuid()))
85
+ fail("TF_DURABILITY_FAILED", "coordinator directory must be owned by the host user");
86
+ if (process.platform !== "win32")
87
+ fs.chmodSync(canonical, 0o700);
88
+ return canonical;
89
+ }
90
+ function validateState(raw) {
91
+ if (!Value.Check(stateSchema, raw))
92
+ fail("TF_DURABILITY_FAILED", "coordinator state has an invalid closed shape");
93
+ const state = raw;
94
+ if (hashState(state) !== state.stateHash || state.audit.length !== state.commitSeq)
95
+ fail("TF_DURABILITY_FAILED", "coordinator state/audit integrity mismatch");
96
+ let previous = ZERO_HASH;
97
+ let priorTime = 0;
98
+ for (const [index, entry] of state.audit.entries()) {
99
+ const { hash, ...body } = entry;
100
+ if (entry.commitSeq !== index + 1 || entry.previousHash !== previous || entry.recordedAt < priorTime || entry.epoch > state.fencingEpoch || commandRequestHash(body) !== hash)
101
+ fail("TF_DURABILITY_FAILED", "coordinator audit chain is inconsistent");
102
+ previous = hash;
103
+ priorTime = entry.recordedAt;
104
+ }
105
+ if (priorTime > state.lastWallTime || (state.audit.length && state.audit.at(-1).stateDigest !== businessDigest(state)))
106
+ fail("TF_DURABILITY_FAILED", "coordinator projection is not bound to its latest audit");
107
+ const bindings = new Set();
108
+ for (const [id, row] of Object.entries(state.reservations)) {
109
+ validId(id);
110
+ if (id !== row.reservation.reservationId || row.updatedAt > state.lastWallTime || row.reservation.coordinatorEpoch > state.fencingEpoch)
111
+ fail("TF_DURABILITY_FAILED", "coordinator reservation identity/time mismatch");
112
+ for (const number of [row.reservation.coordinatorEpoch, row.reservation.projectAdmitCommitSeq, row.reservation.reservedExpiresAt, row.reservation.renewedAt]) {
113
+ if (number !== undefined && (!Number.isSafeInteger(number) || number < 0))
114
+ fail("TF_DURABILITY_FAILED", "coordinator reservation contains an unsafe numeric authority");
115
+ }
116
+ try {
117
+ assertReservationInvariants(row.reservation);
118
+ }
119
+ catch {
120
+ fail("TF_ADMISSION_BINDING_CONFLICT", "committed/orphan reservation must have admission evidence and no TTL");
121
+ }
122
+ if (row.reservation.state === "reserved" && (row.reservation.reservedExpiresAt === undefined || row.reservation.projectAdmitCommitSeq !== undefined))
123
+ fail("TF_ADMISSION_BINDING_CONFLICT", "pre-admit reservation requires TTL and no admitted sequence");
124
+ if (["committed", "orphan-suspect"].includes(row.reservation.state)) {
125
+ const binding = bindingKey(row.reservation);
126
+ if (bindings.has(binding))
127
+ fail("TF_ADMISSION_BINDING_CONFLICT", "duplicate admitted project/domain/run binding");
128
+ bindings.add(binding);
129
+ }
130
+ }
131
+ for (const [id, command] of Object.entries(state.commands)) {
132
+ validId(id);
133
+ if (command.request && (command.request.kind !== command.record.kind || commandRequestHash(command.request) !== command.record.requestHash))
134
+ fail("TF_DURABILITY_FAILED", "coordinator audit request does not match its command");
135
+ if (id !== command.record.commandId || command.record.status !== "completed" || command.record.firstCommitSeq !== command.record.lastCommitSeq || command.record.lastCommitSeq > state.commitSeq)
136
+ fail("TF_DURABILITY_FAILED", "coordinator command authority mismatch");
137
+ }
138
+ if (capacity(state).active > state.maxActiveRuns)
139
+ fail("TF_CAPACITY_EXCEEDED", "durable coordinator exceeds its capacity ceiling");
140
+ return state;
141
+ }
142
+ /** Read-only upgrade fence for homes predating coordinator-epoch.json. Never
143
+ * trust a bare epoch field: verify the complete ledger and its genesis binding. */
144
+ export function readDurableCoordinatorEpoch(directory) {
145
+ const resolved = path.resolve(directory);
146
+ let stat;
147
+ try {
148
+ stat = fs.lstatSync(resolved);
149
+ }
150
+ catch (error) {
151
+ if (error.code === "ENOENT")
152
+ return 0;
153
+ throw error;
154
+ }
155
+ if (!stat.isDirectory() || stat.isSymbolicLink() || process.getuid && stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0)
156
+ fail("TF_DURABILITY_FAILED", "legacy coordinator directory owner, mode or type is invalid");
157
+ function read(name) {
158
+ const file = path.join(resolved, name);
159
+ let named;
160
+ try {
161
+ named = fs.lstatSync(file);
162
+ }
163
+ catch (error) {
164
+ if (error.code === "ENOENT")
165
+ return undefined;
166
+ throw error;
167
+ }
168
+ if (!named.isFile() || named.isSymbolicLink() || process.getuid && named.uid !== process.getuid() || (named.mode & 0o077) !== 0)
169
+ fail("TF_DURABILITY_FAILED", "legacy coordinator file owner, mode or type is invalid");
170
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
171
+ try {
172
+ const opened = fs.fstatSync(fd);
173
+ if (opened.dev !== named.dev || opened.ino !== named.ino)
174
+ fail("TF_DURABILITY_FAILED", "legacy coordinator file identity changed");
175
+ const value = JSON.parse(fs.readFileSync(fd, "utf8"));
176
+ const after = fs.lstatSync(file);
177
+ if (opened.dev !== after.dev || opened.ino !== after.ino)
178
+ fail("TF_DURABILITY_FAILED", "legacy coordinator file identity changed");
179
+ return value;
180
+ }
181
+ finally {
182
+ fs.closeSync(fd);
183
+ }
184
+ }
185
+ const raw = read("coordinator.json"), identity = read("coordinator.identity.json");
186
+ if (raw === undefined && identity === undefined)
187
+ return 0;
188
+ if (raw === undefined || identity === undefined)
189
+ fail("TF_DURABILITY_FAILED", "legacy coordinator ledger or genesis identity is missing");
190
+ const state = validateState(raw);
191
+ const identitySchema = Type.Object({ version: Type.Literal(1), canonicalPath: text, dev: integer, ino: integer, instanceId: UuidSchema }, { additionalProperties: false });
192
+ if (!Value.Check(identitySchema, identity) || identity.canonicalPath !== resolved || identity.dev !== stat.dev || identity.ino !== stat.ino
193
+ || identity.instanceId !== state.instanceId || state.directoryIdentity.canonicalPath !== resolved || state.directoryIdentity.dev !== stat.dev || state.directoryIdentity.ino !== stat.ino
194
+ || fs.realpathSync(resolved) !== resolved)
195
+ fail("TF_DURABILITY_FAILED", "legacy coordinator directory/genesis binding is invalid");
196
+ const after = fs.lstatSync(resolved);
197
+ if (after.dev !== stat.dev || after.ino !== stat.ino)
198
+ fail("TF_DURABILITY_FAILED", "legacy coordinator directory identity changed");
199
+ return state.fencingEpoch;
200
+ }
201
+ function bindingKey(row) { return `${row.projectId}/${row.projectControlDomainId}/${row.runId}`; }
202
+ function matchesProof(row, proof) {
203
+ return !!proof && proof.reservationId === row.reservation.reservationId && bindingKey(row.reservation) === bindingKey(proof) && proof.projectAdmitCommitSeq === row.reservation.projectAdmitCommitSeq
204
+ && Number.isSafeInteger(proof.runVersion) && proof.runVersion >= (row.admitRunVersion ?? 0);
205
+ }
206
+ function isAdmissionProof(proof) {
207
+ return !!proof && !("status" in proof) && Number.isSafeInteger(proof.projectAdmitCommitSeq) && proof.projectAdmitCommitSeq > 0 && Number.isSafeInteger(proof.runVersion) && proof.runVersion >= 0;
208
+ }
209
+ export class UserCoordinatorStore {
210
+ storePath;
211
+ #options;
212
+ #mutex;
213
+ #directoryIdentity;
214
+ #closed = false;
215
+ #clockHighWater = 0;
216
+ constructor(directory, options) {
217
+ positive(options.initialMaxActiveRuns, "initialMaxActiveRuns");
218
+ if (!Number.isSafeInteger(options.epoch) || options.epoch < 1 || !options.holderId?.trim())
219
+ fail("TF_AUTHORITY_REVOKED", "coordinator requires a verified holder and positive epoch");
220
+ if (!options.authority || ["readLease", "authorize", "readAdmission", "readRelease"].some((key) => typeof options.authority[key] !== "function"))
221
+ fail("TF_AUTHORITY_REVOKED", "coordinator requires trusted live authority callbacks");
222
+ this.#options = { ...options };
223
+ this.storePath = prepareDirectory(directory);
224
+ const { dev, ino } = fs.statSync(this.storePath);
225
+ this.#directoryIdentity = { dev, ino };
226
+ this.#mutex = new PersistentFileMutex(path.join(this.storePath, "writer.lock"));
227
+ }
228
+ close() { this.#closed = true; }
229
+ async initialize() { await this.#transaction("initialize", undefined, undefined, async () => undefined); }
230
+ async snapshot(context) {
231
+ return this.#transaction("snapshot", context, undefined, async (state) => ({ commitSeq: state.commitSeq, fencingEpoch: state.fencingEpoch, capacity: capacity(state),
232
+ concurrencyGuarantee: Object.values(state.reservations).some((r) => r.concurrencyGuarantee === "operator-overridden") ? "operator-overridden" : "enforced",
233
+ reservations: Object.values(state.reservations), commands: Object.values(state.commands) }));
234
+ }
235
+ async readReservation(id, context) {
236
+ validId(id);
237
+ return this.#transaction("readReservation", context, id, async (state, identity) => {
238
+ const row = state.reservations[id];
239
+ if (row)
240
+ this.#owner(row, identity);
241
+ return row;
242
+ });
243
+ }
244
+ /** Hold the durable capacity fence through asynchronous preparation and the
245
+ * synchronous provider launch. Preparation must not launch work or re-enter
246
+ * this coordinator. The returned activation must not await: its project
247
+ * binding/auth checks and actual runtime activation occur in one JS turn. */
248
+ async withDispatch(id, context, prepare) {
249
+ validId(id);
250
+ let activate;
251
+ let result;
252
+ await this.#transaction("dispatch", context, id, async (state, identity) => {
253
+ const row = this.#row(state, id);
254
+ this.#owner(row, identity);
255
+ if (row.reservation.state !== "committed")
256
+ fail("TF_ADMISSION_BINDING_CONFLICT", "dispatch requires a committed reservation");
257
+ activate = await prepare(structuredClone(row));
258
+ if (typeof activate !== "function")
259
+ fail("TF_COMMAND_FAILED", "dispatch preparation must return a synchronous activation");
260
+ }, undefined, () => { result = activate(); });
261
+ return result;
262
+ }
263
+ async readCommand(id, context) {
264
+ validId(id);
265
+ return this.#transaction("readCommand", context, undefined, async (state, identity) => {
266
+ const command = state.commands[id];
267
+ if (command && command.record.callerPrincipal !== identity.principal)
268
+ fail("TF_CROSS_PRINCIPAL_COMMAND", "coordinator result belongs to another verified principal");
269
+ return command;
270
+ });
271
+ }
272
+ async reserve(input, context) {
273
+ input = structuredClone(input);
274
+ const id = input.reservationId ?? randomUUID();
275
+ for (const value of [id, input.projectId, input.projectControlDomainId, input.runId])
276
+ validId(value);
277
+ this.#ttl(input.ttlMs);
278
+ return this.#transaction("reserve", context, id, async (state, identity, now) => {
279
+ const existing = state.reservations[id];
280
+ if (existing) {
281
+ this.#owner(existing, identity);
282
+ if (bindingKey(existing.reservation) !== bindingKey(input) || existing.reservation.state !== "reserved")
283
+ fail("TF_ADMISSION_BINDING_CONFLICT", "reservation ID cannot be rebound or reused after admission/expiry/release");
284
+ return existing;
285
+ }
286
+ if (capacity(state).active >= state.maxActiveRuns)
287
+ fail("TF_CAPACITY_EXCEEDED", "global admitted-run capacity exhausted");
288
+ return state.reservations[id] = { reservation: { reservationId: id, projectId: input.projectId, projectControlDomainId: input.projectControlDomainId, runId: input.runId,
289
+ state: "reserved", slots: 1, coordinatorEpoch: this.#options.epoch, reservedExpiresAt: now + input.ttlMs },
290
+ ownerId: identity.ownerId, principal: identity.principal, updatedAt: now, concurrencyGuarantee: "enforced" };
291
+ }, { reservationId: id, projectId: input.projectId, projectControlDomainId: input.projectControlDomainId, runId: input.runId, state: "reserved", slots: 1, coordinatorEpoch: this.#options.epoch });
292
+ }
293
+ async commit(id, input, context) {
294
+ input = structuredClone(input);
295
+ positive(input.projectAdmitCommitSeq, "projectAdmitCommitSeq");
296
+ if (input.attemptId)
297
+ validId(input.attemptId);
298
+ return this.#transaction("commit", context, id, async (state, identity, now) => {
299
+ const row = this.#row(state, id);
300
+ this.#owner(row, identity);
301
+ if (row.reservation.state === "committed") {
302
+ if (row.reservation.projectAdmitCommitSeq !== input.projectAdmitCommitSeq || row.reservation.attemptId !== input.attemptId || row.reservation.providerJobHandle !== input.providerJobHandle)
303
+ fail("TF_ADMISSION_BINDING_CONFLICT", "admission retry changed durable binding");
304
+ return row;
305
+ }
306
+ if (row.reservation.state !== "reserved")
307
+ fail("TF_ADMISSION_BINDING_CONFLICT", "only a live pre-admit reservation can commit");
308
+ if (Object.values(state.reservations).some((other) => other !== row && ["committed", "orphan-suspect"].includes(other.reservation.state) && bindingKey(other.reservation) === bindingKey(row.reservation)))
309
+ fail("TF_ADMISSION_BINDING_CONFLICT", "project/domain/run already has a committed reservation");
310
+ const proposed = { ...row.reservation, ...input };
311
+ const proof = await this.#options.authority.readAdmission(structuredClone(proposed));
312
+ this.#now(now);
313
+ if (row.reservation.reservedExpiresAt <= Date.now())
314
+ fail("TF_ADMISSION_BINDING_CONFLICT", "reservation expired before durable admission commit");
315
+ if (!isAdmissionProof(proof) || !matchesProof({ ...row, reservation: proposed }, proof))
316
+ fail("TF_ADMISSION_BINDING_CONFLICT", "project ledger does not prove this admission");
317
+ row.reservation = { ...proposed, state: "committed", coordinatorEpoch: this.#options.epoch };
318
+ delete row.reservation.reservedExpiresAt;
319
+ row.admitRunVersion = proof.runVersion;
320
+ row.updatedAt = now;
321
+ return row;
322
+ });
323
+ }
324
+ async renew(id, input, context) {
325
+ input = structuredClone(input);
326
+ if (input.ttlMs !== undefined)
327
+ this.#ttl(input.ttlMs);
328
+ return this.#transaction("renew", context, id, async (state, identity, now) => {
329
+ const row = this.#row(state, id);
330
+ this.#owner(row, identity);
331
+ if (!active(row))
332
+ fail("TF_ADMISSION_BINDING_CONFLICT", "inactive reservation cannot renew");
333
+ if (row.reservation.state === "reserved") {
334
+ if (input.ttlMs === undefined)
335
+ fail("TF_COMMAND_FAILED", "reserved renewal requires a TTL");
336
+ row.reservation.reservedExpiresAt = now + input.ttlMs;
337
+ }
338
+ else if (input.ttlMs !== undefined)
339
+ fail("TF_ADMISSION_BINDING_CONFLICT", "committed/orphan reservations must never receive a TTL");
340
+ row.reservation.renewedAt = now;
341
+ row.reservation.coordinatorEpoch = this.#options.epoch;
342
+ row.updatedAt = now;
343
+ return row;
344
+ });
345
+ }
346
+ async markOrphanSuspect(id, context) {
347
+ return this.#transaction("markOrphanSuspect", context, id, async (state, identity, now) => {
348
+ const row = this.#row(state, id);
349
+ this.#owner(row, identity);
350
+ if (!["committed", "orphan-suspect"].includes(row.reservation.state))
351
+ fail("TF_ADMISSION_BINDING_CONFLICT", "only admitted reservations can become orphan-suspect");
352
+ if (row.reservation.state === "committed") {
353
+ row.reservation.state = "orphan-suspect";
354
+ row.updatedAt = now;
355
+ row.reservation.coordinatorEpoch = this.#options.epoch;
356
+ }
357
+ return row;
358
+ });
359
+ }
360
+ async normalRelease(id, context) {
361
+ return this.#transaction("normalRelease", context, id, async (state, identity, now) => {
362
+ const row = this.#row(state, id);
363
+ this.#owner(row, identity);
364
+ if (row.reservation.state === "released" && row.releaseKind === "normal")
365
+ return row;
366
+ if (!["committed", "orphan-suspect"].includes(row.reservation.state))
367
+ fail("TF_ADMISSION_BINDING_CONFLICT", "normalRelease requires an admitted reservation");
368
+ const proof = await this.#options.authority.readRelease(structuredClone(row.reservation));
369
+ this.#now(now);
370
+ const terminal = ["completed", "failed", "blocked", "cancelled"].includes(proof?.status);
371
+ const parked = proof?.status === "paused" && proof.stage === "parked" && proof.requiresReadmission === true;
372
+ if (!matchesProof(row, proof) || !proof.proofId?.trim() || proof.providerNoLiveProcessTree !== true || proof.noAmbiguousJobs !== true || proof.reconcileTimeoutOnly !== false || (!terminal && !parked))
373
+ fail("TF_RECONCILE_REQUIRED", "D37 requires current ledger-bound terminal/parked and provider quiescence evidence");
374
+ row.reservation.state = "released";
375
+ row.reservation.coordinatorEpoch = this.#options.epoch;
376
+ delete row.reservation.reservedExpiresAt;
377
+ row.releaseKind = "normal";
378
+ row.releaseProofId = proof.proofId;
379
+ row.updatedAt = now;
380
+ return row;
381
+ });
382
+ }
383
+ async forceRelease(id, input, context) {
384
+ input = structuredClone(input);
385
+ validId(input.commandId);
386
+ if (input.riskAcknowledgement !== true || typeof input.reason !== "string" || !input.reason.trim())
387
+ fail("TF_POLICY_DENIED", "forceRelease requires explicit risk acknowledgement and reason");
388
+ return this.#transaction("forceRelease", context, id, async (state, identity, now) => {
389
+ this.#operator(identity);
390
+ return this.#command(state, identity, input.commandId, { kind: "forceRelease", reservationId: id, riskAcknowledgement: true, reason: input.reason }, () => {
391
+ const row = this.#row(state, id);
392
+ if (row.reservation.state === "expired")
393
+ fail("TF_ADMISSION_BINDING_CONFLICT", "expired reservation cannot be force released");
394
+ row.reservation.state = "released";
395
+ row.reservation.coordinatorEpoch = this.#options.epoch;
396
+ delete row.reservation.reservedExpiresAt;
397
+ row.releaseKind = "force";
398
+ row.concurrencyGuarantee = "operator-overridden";
399
+ row.updatedAt = now;
400
+ return row;
401
+ });
402
+ });
403
+ }
404
+ async setMaxActiveRuns(input, context) {
405
+ input = structuredClone(input);
406
+ validId(input.commandId);
407
+ positive(input.maxActiveRuns, "maxActiveRuns");
408
+ return this.#transaction("setMaxActiveRuns", context, undefined, async (state, identity) => {
409
+ this.#operator(identity);
410
+ return this.#command(state, identity, input.commandId, { kind: "setMaxActiveRuns", maxActiveRuns: input.maxActiveRuns }, () => {
411
+ if (input.maxActiveRuns < capacity(state).active)
412
+ fail("TF_CAPACITY_EXCEEDED", "cannot lower capacity below reserved/committed/orphan usage");
413
+ return state.maxActiveRuns = input.maxActiveRuns;
414
+ });
415
+ });
416
+ }
417
+ #command(state, identity, id, body, execute) {
418
+ const hash = commandRequestHash(body);
419
+ const prior = state.commands[id];
420
+ if (prior) {
421
+ if (prior.record.callerPrincipal !== identity.principal)
422
+ fail("TF_CROSS_PRINCIPAL_COMMAND", "coordinator result belongs to another verified principal");
423
+ if (prior.record.requestHash !== hash)
424
+ fail("TF_IDEMPOTENCY_CONFLICT", "coordinator command ID was reused with a different request");
425
+ return prior.result;
426
+ }
427
+ const result = execute();
428
+ state.commands[id] = { request: structuredClone(body), record: { commandId: id, kind: body.kind, requestHash: hash, callerPrincipal: identity.principal, firstCommitSeq: state.commitSeq + 1, lastCommitSeq: state.commitSeq + 1, status: "completed" }, result: structuredClone(result) };
429
+ return result;
430
+ }
431
+ #ttl(ttlMs) { positive(ttlMs, "ttlMs"); if (ttlMs > 86_400_000)
432
+ fail("TF_COMMAND_FAILED", "reservation TTL must not exceed one day"); }
433
+ #row(state, id) { validId(id); const row = state.reservations[id]; if (!row)
434
+ fail("TF_ADMISSION_BINDING_CONFLICT", "reservation not found"); return row; }
435
+ #owner(row, identity) { if (row.ownerId !== identity.ownerId || row.principal !== identity.principal)
436
+ fail("TF_AUTHORITY_REVOKED", "reservation owner/project authority does not match"); }
437
+ #operator(identity) { if (identity.operator !== true)
438
+ fail("TF_POLICY_DENIED", "an authorized coordinator operator is required"); }
439
+ #binding() {
440
+ assertNoSymlink(this.storePath);
441
+ const st = fs.statSync(this.storePath);
442
+ if (st.dev !== this.#directoryIdentity.dev || st.ino !== this.#directoryIdentity.ino)
443
+ fail("TF_DURABILITY_FAILED", "coordinator directory identity changed");
444
+ assertNoSymlink(path.join(this.storePath, "writer.lock.queue"));
445
+ assertNoSymlink(path.join(this.storePath, "coordinator.json"));
446
+ assertNoSymlink(path.join(this.storePath, "coordinator.identity.json"));
447
+ }
448
+ #now(floor = 0) {
449
+ const now = Date.now();
450
+ if (!Number.isSafeInteger(now) || now < Math.max(floor, this.#clockHighWater))
451
+ fail("TF_AUTHORITY_REVOKED", "wall clock moved backwards; no TTL reclaim or mutation is safe");
452
+ this.#clockHighWater = now;
453
+ return now;
454
+ }
455
+ async #fence(now) {
456
+ const lease = await this.#options.authority.readLease();
457
+ const observedNow = this.#now(now);
458
+ if (!Value.Check(CoordinatorLeaseSchema, lease) || !Number.isSafeInteger(lease.expiresAt) || !Number.isSafeInteger(lease.fencingEpoch) || lease.fencingEpoch !== this.#options.epoch || lease.holderId !== this.#options.holderId || lease.expiresAt <= observedNow)
459
+ fail("TF_AUTHORITY_REVOKED", "coordinator holder/epoch/lease is no longer authoritative");
460
+ return lease.expiresAt;
461
+ }
462
+ async #expireReservations(state, now) {
463
+ for (const row of Object.values(state.reservations)) {
464
+ if (row.reservation.state !== "reserved" || row.reservation.reservedExpiresAt > now)
465
+ continue;
466
+ let proof;
467
+ try {
468
+ proof = await this.#options.authority.readAdmission(structuredClone(row.reservation));
469
+ // An ordinary observer cannot close the late-admit race. Only the
470
+ // explicit durable ADMIT/ABANDON arbitration may free a slot.
471
+ if (!isAdmissionProof(proof) && this.#options.authority.abandonAdmissionIfAbsent) {
472
+ await this.#fence(this.#now(now));
473
+ proof = await this.#options.authority.abandonAdmissionIfAbsent(structuredClone(row.reservation));
474
+ }
475
+ }
476
+ catch {
477
+ this.#now(now);
478
+ continue;
479
+ } // An unavailable project ledger retains its slot.
480
+ const observedNow = this.#now(now);
481
+ await this.#fence(observedNow);
482
+ if (!proof || proof.reservationId !== row.reservation.reservationId || bindingKey(proof) !== bindingKey(row.reservation))
483
+ continue;
484
+ if (isAdmissionProof(proof)) {
485
+ // Run Admitted may have reached its project ledger before the host
486
+ // crashed between that fsync and coordinator commit. Never TTL-free it.
487
+ row.reservation.state = "committed";
488
+ row.reservation.projectAdmitCommitSeq = proof.projectAdmitCommitSeq;
489
+ row.reservation.coordinatorEpoch = this.#options.epoch;
490
+ row.admitRunVersion = proof.runVersion;
491
+ delete row.reservation.reservedExpiresAt;
492
+ row.updatedAt = observedNow;
493
+ }
494
+ else if ("status" in proof && proof.status === "abandoned"
495
+ && Number.isSafeInteger(proof.abandonmentCommitSeq) && proof.abandonmentCommitSeq > 0
496
+ && Number.isSafeInteger(proof.runVersion) && proof.runVersion >= 0) {
497
+ row.reservation.state = "expired";
498
+ delete row.reservation.reservedExpiresAt;
499
+ row.updatedAt = observedNow;
500
+ }
501
+ }
502
+ }
503
+ async #transaction(action, context, id, apply, authorizationReservation, afterCommit) {
504
+ if (this.#closed)
505
+ fail("TF_DURABILITY_FAILED", "coordinator store is closed");
506
+ this.#binding();
507
+ const release = await this.#mutex.acquire({ timeoutMs: 30_000, signal: AbortSignal.timeout(30_000) }).catch((error) => {
508
+ if (error instanceof ControlError)
509
+ throw error;
510
+ throw new ControlError("TF_DURABILITY_FAILED", `coordinator writer fence failed: ${error instanceof Error ? error.message : String(error)}`, { recoveryAction: "operator", sideEffects: "none" });
511
+ });
512
+ try {
513
+ if (this.#closed)
514
+ fail("TF_DURABILITY_FAILED", "coordinator store closed during lock admission");
515
+ this.#binding();
516
+ const file = path.join(this.storePath, "coordinator.json"), identityFile = path.join(this.storePath, "coordinator.identity.json");
517
+ let storeIdentity;
518
+ if (fs.existsSync(identityFile)) {
519
+ storeIdentity = JSON.parse(fs.readFileSync(identityFile, "utf8"));
520
+ if (!Value.Check(Type.Object({ version: Type.Literal(1), canonicalPath: text, dev: integer, ino: integer, instanceId: UuidSchema }, { additionalProperties: false }), storeIdentity) || storeIdentity.canonicalPath !== this.storePath || storeIdentity.dev !== this.#directoryIdentity.dev || storeIdentity.ino !== this.#directoryIdentity.ino)
521
+ fail("TF_DURABILITY_FAILED", "coordinator genesis directory identity is invalid or changed");
522
+ if (!fs.existsSync(file))
523
+ fail("TF_DURABILITY_FAILED", "coordinator authoritative ledger is missing; bootstrap would erase admitted capacity");
524
+ }
525
+ else {
526
+ if (fs.existsSync(file) || action !== "initialize")
527
+ fail("TF_DURABILITY_FAILED", "coordinator genesis authority is missing");
528
+ storeIdentity = { version: 1, canonicalPath: this.storePath, ...this.#directoryIdentity, instanceId: randomUUID() };
529
+ await this.#fence(this.#now());
530
+ this.#binding();
531
+ writeJsonAtomicHardened(identityFile, storeIdentity);
532
+ }
533
+ let state;
534
+ if (fs.existsSync(file)) {
535
+ state = validateState(JSON.parse(fs.readFileSync(file, "utf8")));
536
+ }
537
+ else {
538
+ state = { version: 1, fencingEpoch: this.#options.epoch, commitSeq: 0, lastWallTime: 0, maxActiveRuns: this.#options.initialMaxActiveRuns,
539
+ directoryIdentity: { canonicalPath: this.storePath, ...this.#directoryIdentity }, instanceId: storeIdentity.instanceId, reservations: {}, commands: {}, audit: [], stateHash: "" };
540
+ }
541
+ if (state.instanceId !== storeIdentity.instanceId || state.directoryIdentity.canonicalPath !== this.storePath || state.directoryIdentity.dev !== this.#directoryIdentity.dev || state.directoryIdentity.ino !== this.#directoryIdentity.ino)
542
+ fail("TF_DURABILITY_FAILED", "coordinator directory identity changed; explicit authority rebind is required");
543
+ const before = commandRequestHash(state);
544
+ const now = this.#now(state.lastWallTime);
545
+ await this.#fence(now);
546
+ if (state.fencingEpoch > this.#options.epoch)
547
+ fail("TF_AUTHORITY_REVOKED", "persisted coordinator epoch fences this host");
548
+ state.fencingEpoch = this.#options.epoch;
549
+ let identity;
550
+ if (action !== "initialize") {
551
+ identity = structuredClone(await this.#options.authority.authorize(context, action, structuredClone(id && state.reservations[id] ? state.reservations[id].reservation : authorizationReservation)));
552
+ this.#now(now);
553
+ if (!identity?.principal?.trim() || !identity.ownerId?.trim() || typeof identity.operator !== "boolean")
554
+ fail("TF_AUTHORITY_REVOKED", "trusted authority did not verify a principal and owner");
555
+ }
556
+ await this.#expireReservations(state, this.#now(now));
557
+ const value = await apply(state, identity, now);
558
+ this.#now(now);
559
+ if (action !== "initialize") {
560
+ const currentIdentity = await this.#options.authority.authorize(context, action, structuredClone(id && state.reservations[id] ? state.reservations[id].reservation : authorizationReservation));
561
+ this.#now(now);
562
+ if (!currentIdentity || currentIdentity.principal !== identity.principal || currentIdentity.ownerId !== identity.ownerId || currentIdentity.operator !== identity.operator)
563
+ fail("TF_AUTHORITY_REVOKED", "live authority changed during coordinator transaction");
564
+ }
565
+ if (this.#closed)
566
+ fail("TF_DURABILITY_FAILED", "coordinator store closed during transaction");
567
+ this.#binding();
568
+ const leaseExpiresAt = await this.#fence(this.#now(now));
569
+ const currentTime = this.#now(now);
570
+ if (commandRequestHash(state) !== before || !fs.existsSync(file)) {
571
+ state.lastWallTime = currentTime;
572
+ state.commitSeq++;
573
+ const body = { commitSeq: state.commitSeq, epoch: state.fencingEpoch, recordedAt: currentTime, action, principal: identity?.principal ?? "coordinator-bootstrap", previousHash: state.audit.at(-1)?.hash ?? ZERO_HASH, stateDigest: businessDigest(state) };
574
+ state.audit.push({ ...body, hash: commandRequestHash(body) });
575
+ state.stateHash = hashState(state);
576
+ validateState(state);
577
+ if (this.#now(currentTime) >= leaseExpiresAt)
578
+ fail("TF_AUTHORITY_REVOKED", "coordinator lease expired before atomic publication");
579
+ if (process.env[COORDINATOR_CRASH_ENV] === "before-state-write")
580
+ process.kill(process.pid, "SIGKILL");
581
+ writeJsonAtomicHardened(file, state);
582
+ if (process.env[COORDINATOR_CRASH_ENV] === "after-state-write")
583
+ process.kill(process.pid, "SIGKILL");
584
+ }
585
+ if (action === "snapshot")
586
+ Object.assign(value, { commitSeq: state.commitSeq, fencingEpoch: state.fencingEpoch });
587
+ // No await may be inserted between the last live fence/publication and
588
+ // provider activation. The mutex is released only in finally below.
589
+ if (afterCommit) {
590
+ if (this.#closed)
591
+ fail("TF_DURABILITY_FAILED", "coordinator store closed before dispatch activation");
592
+ this.#binding();
593
+ if (this.#now(currentTime) >= leaseExpiresAt)
594
+ fail("TF_AUTHORITY_REVOKED", "coordinator lease expired before dispatch activation");
595
+ afterCommit();
596
+ }
597
+ return structuredClone(value);
598
+ }
599
+ catch (error) {
600
+ if (error instanceof ControlError)
601
+ throw error;
602
+ throw new ControlError("TF_DURABILITY_FAILED", `coordinator transaction failed: ${error instanceof Error ? error.message : String(error)}`, { recoveryAction: "operator", sideEffects: "unknown" });
603
+ }
604
+ finally {
605
+ release();
606
+ }
607
+ }
608
+ }
609
+ export async function openCoordinatorStore(directory, options) {
610
+ const store = new UserCoordinatorStore(directory, options);
611
+ try {
612
+ await store.initialize();
613
+ return store;
614
+ }
615
+ catch (error) {
616
+ store.close();
617
+ throw error;
618
+ }
619
+ }
620
+ //# sourceMappingURL=coordinator-store.js.map