@cotal-ai/core 0.11.6 → 0.13.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 (170) hide show
  1. package/dist/acls.d.ts +31 -7
  2. package/dist/acls.d.ts.map +1 -1
  3. package/dist/acls.js +105 -17
  4. package/dist/acls.js.map +1 -1
  5. package/dist/agent-file.js +1 -1
  6. package/dist/agent-file.js.map +1 -1
  7. package/dist/auth-provider.d.ts +90 -10
  8. package/dist/auth-provider.d.ts.map +1 -1
  9. package/dist/auth-provider.js.map +1 -1
  10. package/dist/backup-config.d.ts +55 -0
  11. package/dist/backup-config.d.ts.map +1 -0
  12. package/dist/backup-config.js +243 -0
  13. package/dist/backup-config.js.map +1 -0
  14. package/dist/backup.d.ts +86 -0
  15. package/dist/backup.d.ts.map +1 -0
  16. package/dist/backup.js +497 -0
  17. package/dist/backup.js.map +1 -0
  18. package/dist/broker-floor.d.ts +34 -0
  19. package/dist/broker-floor.d.ts.map +1 -0
  20. package/dist/broker-floor.js +39 -0
  21. package/dist/broker-floor.js.map +1 -0
  22. package/dist/canonical.d.ts +24 -0
  23. package/dist/canonical.d.ts.map +1 -0
  24. package/dist/canonical.js +159 -0
  25. package/dist/canonical.js.map +1 -0
  26. package/dist/channels.d.ts.map +1 -1
  27. package/dist/channels.js +8 -1
  28. package/dist/channels.js.map +1 -1
  29. package/dist/connector.d.ts +4 -0
  30. package/dist/connector.d.ts.map +1 -1
  31. package/dist/endpoint-action.d.ts +424 -0
  32. package/dist/endpoint-action.d.ts.map +1 -0
  33. package/dist/endpoint-action.js +910 -0
  34. package/dist/endpoint-action.js.map +1 -0
  35. package/dist/endpoint-binding.d.ts +344 -0
  36. package/dist/endpoint-binding.d.ts.map +1 -0
  37. package/dist/endpoint-binding.js +753 -0
  38. package/dist/endpoint-binding.js.map +1 -0
  39. package/dist/endpoint-checkpoint.d.ts +292 -0
  40. package/dist/endpoint-checkpoint.d.ts.map +1 -0
  41. package/dist/endpoint-checkpoint.js +818 -0
  42. package/dist/endpoint-checkpoint.js.map +1 -0
  43. package/dist/endpoint-cluster.d.ts +95 -0
  44. package/dist/endpoint-cluster.d.ts.map +1 -0
  45. package/dist/endpoint-cluster.js +194 -0
  46. package/dist/endpoint-cluster.js.map +1 -0
  47. package/dist/endpoint-contract-store.d.ts +90 -0
  48. package/dist/endpoint-contract-store.d.ts.map +1 -0
  49. package/dist/endpoint-contract-store.js +348 -0
  50. package/dist/endpoint-contract-store.js.map +1 -0
  51. package/dist/endpoint-envelope.d.ts +165 -0
  52. package/dist/endpoint-envelope.d.ts.map +1 -0
  53. package/dist/endpoint-envelope.js +346 -0
  54. package/dist/endpoint-envelope.js.map +1 -0
  55. package/dist/endpoint-grants.d.ts +123 -0
  56. package/dist/endpoint-grants.d.ts.map +1 -0
  57. package/dist/endpoint-grants.js +215 -0
  58. package/dist/endpoint-grants.js.map +1 -0
  59. package/dist/endpoint-guard.d.ts +222 -0
  60. package/dist/endpoint-guard.d.ts.map +1 -0
  61. package/dist/endpoint-guard.js +468 -0
  62. package/dist/endpoint-guard.js.map +1 -0
  63. package/dist/endpoint-handle.d.ts +176 -0
  64. package/dist/endpoint-handle.d.ts.map +1 -0
  65. package/dist/endpoint-handle.js +608 -0
  66. package/dist/endpoint-handle.js.map +1 -0
  67. package/dist/endpoint-journal.d.ts +225 -0
  68. package/dist/endpoint-journal.d.ts.map +1 -0
  69. package/dist/endpoint-journal.js +431 -0
  70. package/dist/endpoint-journal.js.map +1 -0
  71. package/dist/endpoint-receipt.d.ts +225 -0
  72. package/dist/endpoint-receipt.d.ts.map +1 -0
  73. package/dist/endpoint-receipt.js +417 -0
  74. package/dist/endpoint-receipt.js.map +1 -0
  75. package/dist/endpoint-records.d.ts +243 -0
  76. package/dist/endpoint-records.d.ts.map +1 -0
  77. package/dist/endpoint-records.js +591 -0
  78. package/dist/endpoint-records.js.map +1 -0
  79. package/dist/endpoint-serve.d.ts +176 -0
  80. package/dist/endpoint-serve.d.ts.map +1 -0
  81. package/dist/endpoint-serve.js +446 -0
  82. package/dist/endpoint-serve.js.map +1 -0
  83. package/dist/endpoint-service.d.ts +518 -0
  84. package/dist/endpoint-service.d.ts.map +1 -0
  85. package/dist/endpoint-service.js +973 -0
  86. package/dist/endpoint-service.js.map +1 -0
  87. package/dist/endpoint-session.d.ts +398 -0
  88. package/dist/endpoint-session.d.ts.map +1 -0
  89. package/dist/endpoint-session.js +834 -0
  90. package/dist/endpoint-session.js.map +1 -0
  91. package/dist/endpoint-signing.d.ts +105 -0
  92. package/dist/endpoint-signing.d.ts.map +1 -0
  93. package/dist/endpoint-signing.js +190 -0
  94. package/dist/endpoint-signing.js.map +1 -0
  95. package/dist/endpoint-subjects.d.ts +222 -0
  96. package/dist/endpoint-subjects.d.ts.map +1 -0
  97. package/dist/endpoint-subjects.js +381 -0
  98. package/dist/endpoint-subjects.js.map +1 -0
  99. package/dist/endpoint-supervisor.d.ts +19 -0
  100. package/dist/endpoint-supervisor.d.ts.map +1 -0
  101. package/dist/endpoint-supervisor.js +12 -0
  102. package/dist/endpoint-supervisor.js.map +1 -0
  103. package/dist/endpoint-traits.d.ts +225 -0
  104. package/dist/endpoint-traits.d.ts.map +1 -0
  105. package/dist/endpoint-traits.js +469 -0
  106. package/dist/endpoint-traits.js.map +1 -0
  107. package/dist/endpoint-verbs.d.ts +277 -0
  108. package/dist/endpoint-verbs.d.ts.map +1 -0
  109. package/dist/endpoint-verbs.js +576 -0
  110. package/dist/endpoint-verbs.js.map +1 -0
  111. package/dist/endpoint-virtual.d.ts +308 -0
  112. package/dist/endpoint-virtual.d.ts.map +1 -0
  113. package/dist/endpoint-virtual.js +440 -0
  114. package/dist/endpoint-virtual.js.map +1 -0
  115. package/dist/endpoint-work.d.ts +304 -0
  116. package/dist/endpoint-work.d.ts.map +1 -0
  117. package/dist/endpoint-work.js +728 -0
  118. package/dist/endpoint-work.js.map +1 -0
  119. package/dist/endpoint.d.ts +66 -22
  120. package/dist/endpoint.d.ts.map +1 -1
  121. package/dist/endpoint.js +289 -89
  122. package/dist/endpoint.js.map +1 -1
  123. package/dist/evict.d.ts +95 -32
  124. package/dist/evict.d.ts.map +1 -1
  125. package/dist/evict.js +193 -3
  126. package/dist/evict.js.map +1 -1
  127. package/dist/index.d.ts +29 -0
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +29 -0
  130. package/dist/index.js.map +1 -1
  131. package/dist/lease.d.ts +4 -0
  132. package/dist/lease.d.ts.map +1 -1
  133. package/dist/lease.js.map +1 -1
  134. package/dist/members.d.ts +4 -4
  135. package/dist/members.d.ts.map +1 -1
  136. package/dist/members.js +12 -12
  137. package/dist/members.js.map +1 -1
  138. package/dist/provision.d.ts +78 -20
  139. package/dist/provision.d.ts.map +1 -1
  140. package/dist/provision.js +214 -37
  141. package/dist/provision.js.map +1 -1
  142. package/dist/runtime.d.ts +3 -0
  143. package/dist/runtime.d.ts.map +1 -1
  144. package/dist/safe-pattern.d.ts +33 -0
  145. package/dist/safe-pattern.d.ts.map +1 -0
  146. package/dist/safe-pattern.js +486 -0
  147. package/dist/safe-pattern.js.map +1 -0
  148. package/dist/schema-profile.d.ts +93 -0
  149. package/dist/schema-profile.d.ts.map +1 -0
  150. package/dist/schema-profile.js +284 -0
  151. package/dist/schema-profile.js.map +1 -0
  152. package/dist/secret-store.d.ts +46 -0
  153. package/dist/secret-store.d.ts.map +1 -0
  154. package/dist/secret-store.js +2 -0
  155. package/dist/secret-store.js.map +1 -0
  156. package/dist/space-auth.d.ts +9 -0
  157. package/dist/space-auth.d.ts.map +1 -0
  158. package/dist/space-auth.js +126 -0
  159. package/dist/space-auth.js.map +1 -0
  160. package/dist/streams.d.ts +14 -10
  161. package/dist/streams.d.ts.map +1 -1
  162. package/dist/streams.js +47 -76
  163. package/dist/streams.js.map +1 -1
  164. package/dist/subjects.d.ts +100 -40
  165. package/dist/subjects.d.ts.map +1 -1
  166. package/dist/subjects.js +136 -60
  167. package/dist/subjects.js.map +1 -1
  168. package/dist/types.d.ts +9 -0
  169. package/dist/types.d.ts.map +1 -1
  170. package/package.json +6 -2
@@ -0,0 +1,728 @@
1
+ import { jetstream, jetstreamManager } from "@nats-io/jetstream";
2
+ import { headers as natsHeaders } from "@nats-io/transport-node";
3
+ import { canonicalJson, rawDigest } from "./canonical.js";
4
+ import { EpEnvelopeError } from "./endpoint-envelope.js";
5
+ import { epwSubject, epfSubject, assertBoundedOwner, assertLifecycleToken } from "./endpoint-subjects.js";
6
+ import { RECORD_KINDS, recordSpecKey, createRecordEntry, updateRecordEntry, openRecordsBucket } from "./endpoint-records.js";
7
+ import { epfStreamName, readLastFact } from "./endpoint-journal.js";
8
+ // The §13.12 resource names and consumer configs live in endpoint-binding.ts (the single source
9
+ // of the stream/durable table: epwStreamName, poolDurable, poolConsumerConfig); this module is
10
+ // the lease/commit/reconcile SEMANTICS over them.
11
+ import { epwStreamName } from "./endpoint-binding.js";
12
+ /** Bond the resources to one space by CONSTRUCTION: the JetStream client, the manager, and the
13
+ * records KV (the space's own bucket) all derive from the ONE connection passed in — four
14
+ * already-separated resources are not accepted, so the advertised bond is real, not asserted.
15
+ * The returned context is FROZEN (no later swap) and BRANDED: every seam accepts only a
16
+ * context this constructor built, so a hand-assembled structural look-alike (the cross-space
17
+ * mixup the bond exists to prevent) is rejected at the consuming boundary. */
18
+ export async function workPoolContext(nc, space) {
19
+ if (nc === null || typeof nc !== "object" || typeof nc.close !== "function")
20
+ throw new EpEnvelopeError("failed-precondition", "a work-pool context is constructed from ONE binding-layer connection; separate resources are never accepted (SPEC 13.4)");
21
+ if (typeof space !== "string" || space.length === 0)
22
+ throw new EpEnvelopeError("failed-precondition", "a work-pool context needs a space");
23
+ const js = jetstream(nc);
24
+ const jsm = await jetstreamManager(nc);
25
+ const kv = await openRecordsBucket(nc, space);
26
+ const ctx = Object.freeze({ kv, js, jsm, space });
27
+ BRANDED_CONTEXTS.add(ctx);
28
+ return ctx;
29
+ }
30
+ const BRANDED_CONTEXTS = new WeakSet();
31
+ function assertCtx(ctx) {
32
+ if (!BRANDED_CONTEXTS.has(ctx))
33
+ throw new EpEnvelopeError("failed-precondition", `the work-pool context was not constructed by workPoolContext(); a hand-assembled resource bundle never authorizes - the space bond is constructed, not asserted (SPEC 13.4)`);
34
+ }
35
+ /** The brand assertion, exported for sibling modules composing over this context (the §13.6
36
+ * virtual admission/occupancy seams): every seam that accepts a WorkPoolContext enforces the
37
+ * constructed bond, none trusts a structural look-alike. */
38
+ export function assertWorkPoolContext(ctx) {
39
+ assertCtx(ctx);
40
+ }
41
+ /** Snapshot the item ref to a validated, DETACHED copy at seam entry, BEFORE the first await:
42
+ * a caller-shared mutable ref can otherwise split one operation's identity across its lease
43
+ * CAS and its terminal publish (settle item A, publish item B). Every seam works only on
44
+ * this copy. */
45
+ function snapshotRef(ref) {
46
+ // Every property is READ EXACTLY ONCE: a getter answering differently between a validation
47
+ // read and a copy read must not split what was checked from what is used.
48
+ if (ref === null || typeof ref !== "object")
49
+ throw new EpEnvelopeError("failed-precondition", `a work-item ref must carry string endpoint/pool and a full acceptance identity (SPEC 13.2)`);
50
+ const endpoint = ref.endpoint;
51
+ const pool = ref.pool;
52
+ const a = ref.acceptance;
53
+ if (typeof endpoint !== "string" || typeof pool !== "string" || a === null || typeof a !== "object")
54
+ throw new EpEnvelopeError("failed-precondition", `a work-item ref must carry string endpoint/pool and a full acceptance identity (SPEC 13.2)`);
55
+ const owner = a.owner;
56
+ const actor = a.actor;
57
+ const uid = a.uid;
58
+ const id = a.id;
59
+ if (typeof owner !== "string" || typeof actor !== "string" || typeof uid !== "string" || typeof id !== "string")
60
+ throw new EpEnvelopeError("failed-precondition", `a work-item ref must carry string endpoint/pool and a full acceptance identity (SPEC 13.2)`);
61
+ return { endpoint, pool, acceptance: { owner, actor, uid, id } };
62
+ }
63
+ /** The item's stored subject (`epw.<e>.<pool>.<cOwner>.<cActor>.<cUid>.<id>`). */
64
+ export function workItemSubject(space, ref) {
65
+ return epwSubject(space, ref.endpoint, ref.pool, ref.acceptance);
66
+ }
67
+ /** The item's terminal-fact subject (`epf.<e>.wrk.<pool>.<acceptance identity>`, §13.2). */
68
+ export function workTerminalSubject(space, ref) {
69
+ return epfSubject(space, ref.endpoint, ["wrk", ref.pool, ref.acceptance.owner, ref.acceptance.actor, ref.acceptance.uid, ref.acceptance.id]);
70
+ }
71
+ function leaseKeyOf(ref) {
72
+ return recordSpecKey(RECORD_KINDS.lease, [ref.endpoint, ref.pool, ref.acceptance.owner, ref.acceptance.actor, ref.acceptance.uid, ref.acceptance.id]);
73
+ }
74
+ function assertSafeInt(v, what) {
75
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0)
76
+ throw new EpEnvelopeError("failed-precondition", `${what} must be a non-negative safe integer; got ${JSON.stringify(v)}`);
77
+ return v;
78
+ }
79
+ /** Execution coordinates (sourceSeq/attempt/fencingToken) are POSITIVE at issue, parse, and
80
+ * commit — symmetrically. Zero is reserved for the worker-less never-leased expiry sentinel;
81
+ * admitting it anywhere else would durably poison the terminal rail (parseWorkTerminalFact refuses
82
+ * zero coordinates, so a zero-coordinate settle could never be projected). */
83
+ function assertPositiveInt(v, what) {
84
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 1)
85
+ throw new EpEnvelopeError("failed-precondition", `${what} must be a positive safe integer; got ${JSON.stringify(v)} (SPEC 13.5)`);
86
+ return v;
87
+ }
88
+ /** The no-message result of a subject-confined last-by-subject read (JetStream err_code 10037):
89
+ * the ONLY error that means "genuinely absent". Every other failure (permission, transport,
90
+ * server) is `unavailable`, never fabricated as absence. */
91
+ function isNoMessage(e) {
92
+ return e?.code === 10037;
93
+ }
94
+ /** The CANONICAL acceptance→item-bytes derivation (§13.6): the ONE deterministic projection of a
95
+ * pool-routed {@link AcceptanceFact} into the EPW stored bytes — work identity + input ONLY
96
+ * (`v`/`id`/`fingerprint`/`sourceSeq`/`workExpiry`/`caller`/`request`; never a lease, token, or
97
+ * decision metadata), RFC-8785 canonical JSON so two independent derivations are BYTE-IDENTICAL.
98
+ * Every first enqueue AND every reconciliation re-enqueue MUST derive through this function:
99
+ * {@link enqueueWorkItem}'s idempotency is same-subject AND same-bytes, so a canonicalizer and a
100
+ * drain repairing its crash-before-enqueue must agree byte-for-byte or the repair fails loud as
101
+ * a mixup. Refuses a non-pool route or a missing work horizon (those never enqueue). */
102
+ export function workItemBytesOf(acceptance) {
103
+ if (!acceptance.route.startsWith("pool."))
104
+ throw new EpEnvelopeError("failed-precondition", `work item bytes derive only from a pool-routed acceptance; route is "${acceptance.route}" (SPEC 13.6)`);
105
+ if (typeof acceptance.workExpiry !== "number" || !Number.isSafeInteger(acceptance.workExpiry))
106
+ throw new EpEnvelopeError("failed-precondition", `a pool-routed acceptance must pin an integer workExpiry; the stored item carries its absolute horizon (SPEC 13.8)`);
107
+ return new TextEncoder().encode(canonicalJson({
108
+ v: 1,
109
+ id: acceptance.id,
110
+ fingerprint: acceptance.fingerprint,
111
+ sourceSeq: acceptance.sourceSeq,
112
+ workExpiry: acceptance.workExpiry,
113
+ caller: acceptance.caller,
114
+ request: acceptance.request,
115
+ }));
116
+ }
117
+ /** Enqueue a pool item (the canonicalizer's seam, §13.6): CREATE-ONLY per acceptance-identity
118
+ * subject, so acceptance→enqueue spanning two streams stays idempotent — a duplicate or
119
+ * reconciliation re-enqueue of the same item loses its CAS harmlessly. The bytes are the
120
+ * acceptance-derived work identity + input ONLY (never a lease/token; {@link workItemBytesOf}
121
+ * is the canonical derivation). A CAS loss is only a
122
+ * benign duplicate if the stored bytes are BYTE-IDENTICAL to the ones offered (same
123
+ * acceptance-derived work): a differing prior body under the same identity is a canonicalizer
124
+ * mixup and fails loud, never silently executes the wrong input. */
125
+ export async function enqueueWorkItem(ctx, itemRef, itemBytes) {
126
+ assertCtx(ctx);
127
+ const ref = snapshotRef(itemRef);
128
+ if (!(itemBytes instanceof Uint8Array))
129
+ throw new EpEnvelopeError("failed-precondition", `itemBytes must be a Uint8Array (the acceptance-derived stored bytes, SPEC 13.6)`);
130
+ const bytes = new Uint8Array(itemBytes); // real copy detached at entry: the published body and the CAS-loss identity check read the SAME bytes. NOT .slice() — a Node Buffer (Buffer extends Uint8Array, so it passes the guard above) .slice()s to an ALIASING view, which a caller mutating itemBytes during the publish await would still corrupt; new Uint8Array(...) always copies into a fresh ArrayBuffer.
131
+ const h = natsHeaders();
132
+ h.set("Nats-Expected-Last-Subject-Sequence", "0");
133
+ const subject = workItemSubject(ctx.space, ref);
134
+ try {
135
+ const pa = await ctx.js.publish(subject, bytes, { headers: h });
136
+ return { enqueued: true, seq: pa.seq };
137
+ }
138
+ catch (e) {
139
+ const code = e?.code;
140
+ if (code !== 10071 && code !== 10164)
141
+ throw e;
142
+ // A prior enqueue exists (or existed and was consumed). If a live entry remains, verify its
143
+ // bytes match; a consumed-and-gone entry (no message) is a settled/in-flight item the caller
144
+ // reconciles, not a mismatch.
145
+ let stored;
146
+ // LEADER-SERVED STREAM.MSG.GET, never a follower Direct Get (SPEC 13.6:1797-1799): this read
147
+ // gates the enqueue decision (a stale follower miss would re-arm settled work), so it is a
148
+ // fencing read and must go to the stream leader that just rejected the CAS — read-your-writes.
149
+ try {
150
+ stored = await ctx.jsm.streams.getMessage(epwStreamName(ctx.space), { last_by_subj: subject });
151
+ }
152
+ catch (ge) {
153
+ if (isNoMessage(ge))
154
+ return { enqueued: false };
155
+ throw new EpEnvelopeError("unavailable", `the enqueue CAS lost and the stored item is not readable to verify identity (SPEC 13.6): ${ge?.message ?? String(ge)}`);
156
+ }
157
+ if (stored !== null && rawDigest(stored.data) !== rawDigest(bytes)) // the DETACHED bytes, the ones actually published — not the caller-owned itemBytes a mutation could have changed during the publish await
158
+ throw new EpEnvelopeError("conflict", `an item with acceptance identity "${ref.acceptance.id}" is already enqueued with DIFFERENT bytes; idempotency is same-subject AND same-bytes; a differing body is a canonicalizer mixup, never silently accepted (SPEC 13.6)`);
159
+ return { enqueued: false };
160
+ }
161
+ }
162
+ /** Detach + validate a caller-supplied worker at seam entry, reading each property EXACTLY
163
+ * ONCE (single-read: a getter answering differently between a validation read and a
164
+ * construction read must not split the principal that was checked from the one persisted). */
165
+ function assertWorker(w, what) {
166
+ const kind = w.kind;
167
+ const owner = w.owner;
168
+ const actor = w.actor;
169
+ const lifecycleUid = w.lifecycleUid;
170
+ assertBoundedOwner(owner, `${what} owner`);
171
+ assertBoundedOwner(actor, `${what} actor`);
172
+ assertLifecycleToken(lifecycleUid, `${what} lifecycleUid`);
173
+ if (kind === "endpoint") {
174
+ const epoch = w.epoch;
175
+ if (!Number.isSafeInteger(epoch) || epoch < 0)
176
+ throw new EpEnvelopeError("failed-precondition", `${what} is an endpoint worker but carries no valid epoch; an endpoint worker's process epoch is its fence (SPEC 13.8)`);
177
+ return Object.freeze({ kind: "endpoint", owner, actor, lifecycleUid, epoch });
178
+ }
179
+ if (kind !== "agent")
180
+ throw new EpEnvelopeError("failed-precondition", `${what} has an unknown worker kind; a worker is "agent" or "endpoint" (SPEC 13.5)`);
181
+ return Object.freeze({ kind: "agent", owner, actor, lifecycleUid });
182
+ }
183
+ function assertClosedKeys(o, allowed, what) {
184
+ for (const k of Object.keys(o))
185
+ if (!allowed.includes(k))
186
+ throw new EpEnvelopeError("internal", `${what} carries unknown field ${JSON.stringify(k)}; a closed schema admits no extras - garbled state never authorizes (SPEC 13.4)`);
187
+ }
188
+ function parseWorker(raw, key) {
189
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
190
+ throw new EpEnvelopeError("internal", `lease record ${key} carries a non-object worker; garbled mediated lease state never authorizes (SPEC 13.5)`);
191
+ const w = raw;
192
+ if (typeof w.owner !== "string" || typeof w.actor !== "string" || typeof w.lifecycleUid !== "string" || (w.kind !== "agent" && w.kind !== "endpoint"))
193
+ throw new EpEnvelopeError("internal", `lease record ${key} worker is missing its kind/principal/lifecycle binding; garbled state never authorizes (SPEC 13.5)`);
194
+ if (w.kind === "endpoint" && (typeof w.epoch !== "number" || !Number.isSafeInteger(w.epoch) || w.epoch < 0))
195
+ throw new EpEnvelopeError("internal", `lease record ${key} endpoint worker has no valid epoch; garbled state never authorizes (SPEC 13.5)`);
196
+ assertClosedKeys(w, w.kind === "endpoint" ? ["kind", "owner", "actor", "lifecycleUid", "epoch"] : ["kind", "owner", "actor", "lifecycleUid"], `worker on ${key}`);
197
+ return w.kind === "endpoint"
198
+ ? { kind: "endpoint", owner: w.owner, actor: w.actor, lifecycleUid: w.lifecycleUid, epoch: w.epoch }
199
+ : { kind: "agent", owner: w.owner, actor: w.actor, lifecycleUid: w.lifecycleUid };
200
+ }
201
+ function sameWorker(a, b) {
202
+ if (a === undefined || b === undefined)
203
+ return false;
204
+ return a.kind === b.kind && a.owner === b.owner && a.actor === b.actor && a.lifecycleUid === b.lifecycleUid
205
+ && (a.kind !== "endpoint" || (b.kind === "endpoint" && a.epoch === b.epoch));
206
+ }
207
+ /** Closed per-state/per-disposition validation (§13.4/§13.5): a `leased` lease MUST carry a
208
+ * worker + positive attempt/sourceSeq and NO settlement fields; a `settled:committed` MUST
209
+ * carry a worker + a present `outcome` + committedTs + positive execution coordinates (its
210
+ * terminal fact derives from them); `settled:expired|retired` carry committedTs, KEEP their
211
+ * worker when one was ever leased (workerless = never-leased), and never carry an outcome.
212
+ * A retired settlement additionally carries its op/target binding. Unknown fields and garbled
213
+ * cross-variant state never authorize. */
214
+ function parseLease(raw, key) {
215
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
216
+ throw new EpEnvelopeError("internal", `lease record ${key} is not an object; garbled mediated lease state never authorizes (SPEC 13.5)`);
217
+ const o = raw;
218
+ if (o.v !== 1 || (o.state !== "leased" && o.state !== "settled"))
219
+ throw new EpEnvelopeError("internal", `lease record ${key} has an unknown version/state; garbled state never authorizes (SPEC 13.5)`);
220
+ assertClosedKeys(o, ["v", "state", "sourceSeq", "attempt", "worker", "fencingToken", "leaseDeadline", "workExpiry", "disposition", "outcome", "opId", "targetUid", "committedTs"], `lease record ${key}`);
221
+ for (const [name, v] of [["sourceSeq", o.sourceSeq], ["attempt", o.attempt], ["fencingToken", o.fencingToken], ["leaseDeadline", o.leaseDeadline], ["workExpiry", o.workExpiry]])
222
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0)
223
+ throw new EpEnvelopeError("internal", `lease record ${key} field ${name} is not a safe integer; garbled state never authorizes (SPEC 13.5)`);
224
+ const committed = o.state === "settled" && o.disposition === "committed";
225
+ const expired = o.state === "settled" && o.disposition === "expired";
226
+ const retired = o.state === "settled" && o.disposition === "retired";
227
+ if (o.state === "settled" && !committed && !expired && !retired)
228
+ throw new EpEnvelopeError("internal", `settled lease record ${key} has no valid disposition; garbled state never authorizes (SPEC 13.5)`);
229
+ if (o.state === "leased" && (o.disposition !== undefined || o.outcome !== undefined || o.opId !== undefined || o.targetUid !== undefined || o.committedTs !== undefined))
230
+ throw new EpEnvelopeError("internal", `leased lease record ${key} carries settlement fields; garbled cross-variant state never authorizes (SPEC 13.5)`);
231
+ if ((expired || retired) && o.outcome !== undefined)
232
+ throw new EpEnvelopeError("internal", `${o.disposition} lease record ${key} carries an outcome; garbled cross-variant state never authorizes (SPEC 13.5)`);
233
+ if (retired) {
234
+ if (typeof o.opId !== "string" || typeof o.targetUid !== "string")
235
+ throw new EpEnvelopeError("internal", `retired lease record ${key} carries no op/target binding; garbled state never authorizes (SPEC 13.1)`);
236
+ assertLifecycleToken(o.opId, "retired lease opId");
237
+ assertLifecycleToken(o.targetUid, "retired lease targetUid");
238
+ }
239
+ else if (o.opId !== undefined || o.targetUid !== undefined) {
240
+ throw new EpEnvelopeError("internal", `${String(o.disposition ?? o.state)} lease record ${key} carries retirement fields; garbled cross-variant state never authorizes (SPEC 13.5)`);
241
+ }
242
+ const needsWorker = o.state === "leased" || committed;
243
+ // An EXECUTED coordinate set (any record that carries a worker) is positive across ALL THREE
244
+ // coordinates; only a workerless never-leased expiry/retirement sentinel may carry zeros.
245
+ const executed = needsWorker || o.worker !== undefined;
246
+ if (executed && (o.attempt < 1 || o.sourceSeq < 1 || o.fencingToken < 1))
247
+ throw new EpEnvelopeError("internal", `lease record ${key} is ${o.state}/${String(o.disposition ?? "")} with zero execution coordinates (attempt/sourceSeq/fencingToken); garbled state never authorizes (SPEC 13.5)`);
248
+ if (committed && !("outcome" in o))
249
+ throw new EpEnvelopeError("internal", `committed lease record ${key} carries no outcome; garbled state never authorizes (SPEC 13.5)`);
250
+ if (o.state === "settled" && (typeof o.committedTs !== "number" || !Number.isSafeInteger(o.committedTs) || o.committedTs < 0))
251
+ throw new EpEnvelopeError("internal", `settled lease record ${key} has no valid committedTs; garbled state never authorizes (SPEC 13.5)`);
252
+ const worker = needsWorker || o.worker !== undefined ? parseWorker(o.worker, key) : undefined;
253
+ return {
254
+ v: 1, state: o.state, sourceSeq: o.sourceSeq, attempt: o.attempt,
255
+ ...(worker !== undefined ? { worker } : {}),
256
+ fencingToken: o.fencingToken,
257
+ leaseDeadline: o.leaseDeadline, workExpiry: o.workExpiry,
258
+ ...(o.state === "settled" ? { disposition: o.disposition, outcome: o.outcome, opId: o.opId, targetUid: o.targetUid, committedTs: o.committedTs } : {}),
259
+ };
260
+ }
261
+ function decodeJson(value, key) {
262
+ try {
263
+ return JSON.parse(new TextDecoder().decode(value));
264
+ }
265
+ catch (e) {
266
+ throw new EpEnvelopeError("internal", `record ${key} does not decode as JSON: ${e.message}`);
267
+ }
268
+ }
269
+ /** Issue (or idempotently re-issue) the item's lease for the owner's CURRENT delivery — the
270
+ * reserved `lease` command's handler seam, driven ONLY by the pool-owning endpoint after it
271
+ * fetched the item off its own durable (§13.5).
272
+ *
273
+ * First-wins idempotent CAS per (item, attempt):
274
+ * - no record → create `leased {attempt, worker, fencingToken: 1, leaseDeadline, workExpiry}`;
275
+ * - the recorded attempt EQUALS this delivery → the SAME lease returns unchanged (no
276
+ * reassignment within an attempt; the commit gate binds to the RECORDED worker);
277
+ * - the recorded attempt is OLDER → redelivery advanced: revision-pinned update to the new
278
+ * attempt with `fencingToken + 1`;
279
+ * - the recorded attempt is NEWER → the caller's delivery is stale (`expired`).
280
+ * Refusals before touching state: EXPIRED work (`now >= workExpiry`, settled by reconciliation,
281
+ * never leased) and a SETTLED lease (`state === "settled"` — a committed item can never be
282
+ * leased again, fenced on the SAME key, no cross-store read). `leaseDeadline` is CLAMPED to
283
+ * `workExpiry` so no valid lease outlives the horizon. A DEL marker on the lease refuses. */
284
+ export async function leaseWorkItem(ctx, args) {
285
+ assertCtx(ctx);
286
+ // Snapshot the FULL operation input to detached, validated locals at entry, BEFORE the first
287
+ // await; nothing below reads args again (a live args object mutated across an await must not
288
+ // move any coordinate mid-seam).
289
+ const ref = snapshotRef(args.ref);
290
+ const worker = assertWorker(args.worker, "lease worker");
291
+ const sourceSeq = assertPositiveInt(args.sourceSeq, "sourceSeq"); // positive at ISSUE: a zero here would create a lease parseLease refuses forever
292
+ const now = assertSafeInt(args.now, "now");
293
+ const workExpiry = assertSafeInt(args.workExpiry, "workExpiry");
294
+ const attempt = args.attempt;
295
+ const leaseTtlMs = args.leaseTtlMs;
296
+ if (!Number.isSafeInteger(attempt) || attempt < 1)
297
+ throw new EpEnvelopeError("failed-precondition", `attempt must be a positive delivery count; got ${JSON.stringify(attempt)} (SPEC 13.5)`);
298
+ if (!Number.isSafeInteger(leaseTtlMs) || leaseTtlMs <= 0)
299
+ throw new EpEnvelopeError("failed-precondition", `leaseTtlMs must be a positive integer; got ${JSON.stringify(leaseTtlMs)}`);
300
+ if (now >= workExpiry)
301
+ throw new EpEnvelopeError("expired", `the item's workExpiry (${workExpiry}) has passed at the owner clock (${now}); expired work is settled by reconciliation, never leased (SPEC 13.6/13.8)`);
302
+ const leaseDeadline = Math.min(now + leaseTtlMs, workExpiry); // no valid lease outlives the horizon
303
+ const key = leaseKeyOf(ref);
304
+ for (let pass = 0; pass < 2; pass++) {
305
+ const entry = await ctx.kv.get(key);
306
+ if (!entry || entry.operation !== "PUT") {
307
+ if (entry && entry.operation !== "PUT")
308
+ throw new EpEnvelopeError("failed-precondition", `the lease record ${key} carries a ${entry.operation} marker; a deletion never resets an authoritative lease (SPEC 13.5)`);
309
+ const lease = { v: 1, state: "leased", sourceSeq, attempt, worker, fencingToken: 1, leaseDeadline, workExpiry };
310
+ try {
311
+ await createRecordEntry(ctx.kv, key, lease);
312
+ return lease;
313
+ }
314
+ catch (e) {
315
+ if (e instanceof EpEnvelopeError && e.code === "conflict")
316
+ continue;
317
+ throw e;
318
+ }
319
+ }
320
+ const stored = parseLease(decodeJson(entry.value, key), key);
321
+ // Settlement DOMINATES every binding check: a never-leased expired settlement carries
322
+ // sentinel coordinates (sourceSeq 0), so binding refusals on a settled item would mislead.
323
+ if (stored.state === "settled")
324
+ throw new EpEnvelopeError("failed-precondition", `the item is already settled ${stored.disposition}; a committed item can never be leased again; observe the terminal and ack the redelivery without effect (SPEC 13.5/13.6)`);
325
+ if (stored.sourceSeq !== sourceSeq)
326
+ throw new EpEnvelopeError("conflict", `the lease for this acceptance identity binds stream sequence ${stored.sourceSeq}, not ${sourceSeq}; a request id becomes new work only after workExpiry AND fact retention pass (SPEC 13.8)`);
327
+ // `workExpiry` is IDENTITY-BOUND: it is set once from the AcceptanceFact and every later
328
+ // lease call for the same execution MUST carry the SAME horizon (a redelivery cannot extend
329
+ // or shorten the absolute work expiry, SPEC 13.8).
330
+ if (stored.workExpiry !== workExpiry)
331
+ throw new EpEnvelopeError("conflict", `the lease binds workExpiry ${stored.workExpiry} but this call supplies ${workExpiry}; the absolute work horizon is fixed at acceptance and never re-set (SPEC 13.8)`);
332
+ if (stored.attempt === attempt)
333
+ return stored; // first-wins: the still-current attempt's lease, verbatim
334
+ if (stored.attempt > attempt)
335
+ throw new EpEnvelopeError("expired", `attempt ${attempt} is superseded: redelivery advanced this item to attempt ${stored.attempt} (SPEC 13.5)`);
336
+ const next = { v: 1, state: "leased", sourceSeq, attempt, worker, fencingToken: stored.fencingToken + 1, leaseDeadline, workExpiry };
337
+ try {
338
+ await updateRecordEntry(ctx.kv, key, next, entry.revision);
339
+ return next;
340
+ }
341
+ catch (e) {
342
+ if (e instanceof EpEnvelopeError && e.code === "conflict")
343
+ continue;
344
+ throw e;
345
+ }
346
+ }
347
+ throw new EpEnvelopeError("conflict", `the lease record ${key} moved twice during one lease call; re-read and re-decide (SPEC 13.8)`);
348
+ }
349
+ /** Validate a terminal fact fully AND bind it to the subject it was read from (§13.4): a garbled
350
+ * or mis-subjected fact never counts as authoritative settlement (which would suppress all
351
+ * future leasing). Exported as the shared codec: the retirement cleaner (§13.1), which holds no
352
+ * pool-owner context, validates the winners it reads through this same seam. */
353
+ export function parseWorkTerminalFact(raw, subject, ref) {
354
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
355
+ throw new EpEnvelopeError("internal", `work terminal fact on ${subject} is not an object; garbled state never authorizes (SPEC 13.6)`);
356
+ const o = raw;
357
+ if (o.v !== 1 || (o.disposition !== "committed" && o.disposition !== "expired" && o.disposition !== "retired"))
358
+ throw new EpEnvelopeError("internal", `work terminal fact on ${subject} has an unknown version/disposition (SPEC 13.6)`);
359
+ if (o.pool !== ref.pool)
360
+ throw new EpEnvelopeError("internal", `work terminal fact on ${subject} names pool ${JSON.stringify(o.pool)}, not ${ref.pool}; a mis-subjected fact never authorizes (SPEC 13.4)`);
361
+ const cr = o.caller;
362
+ if (!cr || typeof cr !== "object" || cr.owner !== ref.acceptance.owner || cr.actor !== ref.acceptance.actor || cr.uid !== ref.acceptance.uid || cr.id !== ref.acceptance.id)
363
+ throw new EpEnvelopeError("internal", `work terminal fact on ${subject} carries a caller identity other than its subject's acceptance identity (SPEC 13.4)`);
364
+ assertClosedKeys(cr, ["owner", "actor", "uid", "id"], `terminal-fact caller on ${subject}`);
365
+ if (typeof o.ts !== "number" || !Number.isSafeInteger(o.ts) || o.ts < 0)
366
+ throw new EpEnvelopeError("internal", `work terminal fact on ${subject} has no valid ts (SPEC 13.6)`);
367
+ if (o.disposition === "committed") {
368
+ assertClosedKeys(o, ["v", "disposition", "pool", "caller", "sourceSeq", "attempt", "fencingToken", "worker", "outcome", "ts"], `committed terminal fact on ${subject}`);
369
+ for (const [n, v] of [["sourceSeq", o.sourceSeq], ["attempt", o.attempt], ["fencingToken", o.fencingToken]])
370
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 1)
371
+ throw new EpEnvelopeError("internal", `committed terminal fact on ${subject} field ${n} is not a positive safe integer; a committed item was delivered and fenced (SPEC 13.6)`);
372
+ if (!("outcome" in o))
373
+ throw new EpEnvelopeError("internal", `committed terminal fact on ${subject} carries no outcome; garbled state never authorizes (SPEC 13.6)`);
374
+ return {
375
+ v: 1, disposition: "committed", pool: ref.pool, caller: ref.acceptance,
376
+ sourceSeq: o.sourceSeq, attempt: o.attempt, fencingToken: o.fencingToken,
377
+ worker: parseWorker(o.worker, subject), outcome: o.outcome, ts: o.ts,
378
+ };
379
+ }
380
+ if (o.disposition === "retired") {
381
+ assertClosedKeys(o, ["v", "disposition", "pool", "caller", "opId", "targetUid", "ts"], `retired terminal fact on ${subject}`);
382
+ if (typeof o.opId !== "string" || typeof o.targetUid !== "string")
383
+ throw new EpEnvelopeError("internal", `retired terminal fact on ${subject} carries no op/target binding (SPEC 13.1)`);
384
+ try {
385
+ assertLifecycleToken(o.opId, "retired terminal opId");
386
+ assertLifecycleToken(o.targetUid, "retired terminal targetUid");
387
+ }
388
+ catch (e) {
389
+ throw new EpEnvelopeError("internal", `retired terminal fact on ${subject} carries a malformed op/target binding: ${e.message} (SPEC 13.1)`);
390
+ }
391
+ return { v: 1, disposition: "retired", pool: ref.pool, caller: ref.acceptance, opId: o.opId, targetUid: o.targetUid, ts: o.ts };
392
+ }
393
+ assertClosedKeys(o, ["v", "disposition", "pool", "caller", "workExpiry", "ts"], `expired terminal fact on ${subject}`);
394
+ if (typeof o.workExpiry !== "number" || !Number.isSafeInteger(o.workExpiry) || o.workExpiry < 0)
395
+ throw new EpEnvelopeError("internal", `expired terminal fact on ${subject} has no valid workExpiry (SPEC 13.6)`);
396
+ return { v: 1, disposition: "expired", pool: ref.pool, caller: ref.acceptance, workExpiry: o.workExpiry, ts: o.ts };
397
+ }
398
+ /** Read the item's cached terminal state (leader-served last-by-subject: the CAS-loser read
399
+ * needs read-your-writes, §13.4). `undefined` when the item has no terminal yet. */
400
+ export async function readWorkTerminal(ctx, itemRef) {
401
+ assertCtx(ctx);
402
+ const ref = snapshotRef(itemRef);
403
+ const subject = workTerminalSubject(ctx.space, ref);
404
+ const raw = await readLastFact(ctx.jsm, epfStreamName(ctx.space), subject);
405
+ return raw === undefined ? undefined : parseWorkTerminalFact(raw, subject, ref);
406
+ }
407
+ /** Build the terminal fact a settled lease derives. */
408
+ function terminalOf(ref, lease) {
409
+ if (lease.disposition === "committed") {
410
+ if (lease.worker === undefined || lease.committedTs === undefined)
411
+ throw new EpEnvelopeError("internal", `a committed lease for ${ref.acceptance.id} is missing its worker/committedTs; garbled state never authorizes (SPEC 13.5)`);
412
+ return { v: 1, disposition: "committed", pool: ref.pool, caller: ref.acceptance, sourceSeq: lease.sourceSeq, attempt: lease.attempt, fencingToken: lease.fencingToken, worker: lease.worker, outcome: lease.outcome, ts: lease.committedTs };
413
+ }
414
+ if (lease.disposition === "retired") {
415
+ if (lease.opId === undefined || lease.targetUid === undefined || lease.committedTs === undefined)
416
+ throw new EpEnvelopeError("internal", `a retired lease for ${ref.acceptance.id} is missing its op/target/committedTs; garbled state never authorizes (SPEC 13.1)`);
417
+ return { v: 1, disposition: "retired", pool: ref.pool, caller: ref.acceptance, opId: lease.opId, targetUid: lease.targetUid, ts: lease.committedTs };
418
+ }
419
+ return { v: 1, disposition: "expired", pool: ref.pool, caller: ref.acceptance, workExpiry: lease.workExpiry, ts: lease.committedTs ?? 0 };
420
+ }
421
+ /** Publish a settled lease's terminal fact create-only. A lost CAS reads the winner and PROVES
422
+ * it canonically equals the fact this caller derived from the authoritative settled lease —
423
+ * every legitimate publisher derives from that same lease, so a mismatch is a foreign/garbled
424
+ * write and fails loud instead of being returned as "the winner". */
425
+ async function publishTerminal(ctx, ref, fact) {
426
+ const subject = workTerminalSubject(ctx.space, ref);
427
+ const res = await publishCreateOnly(ctx.js, subject, new TextEncoder().encode(JSON.stringify(fact)));
428
+ if (res.won)
429
+ return { won: true, fact };
430
+ const winner = await readWorkTerminal(ctx, ref);
431
+ if (winner === undefined)
432
+ throw new EpEnvelopeError("internal", `the terminal CAS for ${subject} was lost but no winning fact is readable (SPEC 13.4)`);
433
+ if (canonicalJson(winner) !== canonicalJson(fact))
434
+ throw new EpEnvelopeError("internal", `the terminal CAS winner on ${subject} does not match the fact derived from the authoritative settled lease; a foreign/garbled terminal never authorizes (SPEC 13.4)`);
435
+ return { won: false, fact: winner };
436
+ }
437
+ /** Settle a claimed item — the reserved `commit` command's handler seam, driven ONLY by the
438
+ * pool-owning endpoint on behalf of the broker-authenticated commit caller (§13.5). Gate order
439
+ * against the OWNER-RECORDED lease and the owner's clock: execution binding (sourceSeq) →
440
+ * token currency (attempt + fencingToken) → the caller IS the lease's bound worker → FRESH
441
+ * epoch currency for an endpoint worker → `now < workExpiry` → `now < leaseDeadline`. Then the
442
+ * FENCE: a revision-pinned CAS advances the lease `leased → settled{committed, outcome}` — the
443
+ * SAME key a redelivery-advance contends on, so a stale attempt cannot slip a commit in after
444
+ * reassignment. Only after winning that CAS is the terminal fact (derived from the settled
445
+ * lease) published. A lost lease CAS means the lease advanced or settled concurrently: a
446
+ * same-tuple settle is a DUPLICATE (return the cached terminal, which DOMINATES lease-expiry —
447
+ * a true duplicate always sees its cached outcome, §13.5); anything else is `expired`/`conflict`.
448
+ *
449
+ * `resolveCurrentEpoch` freshly resolves an endpoint worker's CURRENT process epoch from trusted
450
+ * authority (the lifecycle mapping) — a required seam for endpoint workers, absent for agents. */
451
+ export async function commitWorkItem(ctx, args) {
452
+ assertCtx(ctx);
453
+ // Snapshot EVERY operation input at entry, BEFORE the first await: a caller-shared mutable
454
+ // ref/tuple/outcome/clock/resolver can otherwise split one commit's identity across the lease
455
+ // CAS and the terminal publish (settle item A, publish item B) or move the validated clock.
456
+ const ref = snapshotRef(args.ref);
457
+ // Read the lease reference ONCE into a local, then validate/copy its fields from that one
458
+ // reference: a shifting `args.lease` getter must not splice three separately-read fields into
459
+ // a composite tuple no single input ever carried.
460
+ const leaseArg = args.lease;
461
+ const tuple = {
462
+ sourceSeq: assertPositiveInt(leaseArg?.sourceSeq, "lease.sourceSeq"),
463
+ attempt: assertPositiveInt(leaseArg?.attempt, "lease.attempt"),
464
+ fencingToken: assertPositiveInt(leaseArg?.fencingToken, "lease.fencingToken"),
465
+ };
466
+ const outcomeSnapshot = JSON.parse(canonicalJson(args.outcome === undefined ? null : args.outcome));
467
+ const now = assertSafeInt(args.now, "now");
468
+ const caller = assertWorker(args.caller, "commit caller");
469
+ const resolveCurrentEpoch = args.resolveCurrentEpoch;
470
+ const epochResolveBudgetMs = args.epochResolveBudgetMs;
471
+ const key = leaseKeyOf(ref);
472
+ const entry = await ctx.kv.get(key);
473
+ if (!entry || entry.operation !== "PUT")
474
+ throw new EpEnvelopeError("failed-precondition", `no lease is recorded for this item (${key}); a commit settles only owner-assigned work (SPEC 13.5)`);
475
+ const stored = parseLease(decodeJson(entry.value, key), key);
476
+ // A DUPLICATE (same worker, same lease tuple) always observes its cached terminal — the cache
477
+ // lookup DOMINATES lease-expiry, so a slow-but-legitimate retry after its own commit never
478
+ // sees `expired` (§13.5:1517-1519). A stale/foreign caller does NOT get here (identity + tuple
479
+ // must match the settled lease).
480
+ if (stored.state === "settled") {
481
+ const dup = stored.sourceSeq === tuple.sourceSeq && stored.attempt === tuple.attempt
482
+ && stored.fencingToken === tuple.fencingToken && sameWorker(stored.worker, caller);
483
+ if (dup)
484
+ return publishTerminal(ctx, ref, terminalOf(ref, stored)); // idempotent republish; won:false once it already exists
485
+ throw new EpEnvelopeError("conflict", `the item is already settled ${stored.disposition} under a different attempt/worker; this commit is superseded (SPEC 13.5)`);
486
+ }
487
+ // parseLease guarantees a `leased` record carries its worker; narrow for the checks below.
488
+ const bound = stored.worker;
489
+ if (bound === undefined)
490
+ throw new EpEnvelopeError("internal", `leased record ${key} has no worker; garbled state never authorizes (SPEC 13.5)`);
491
+ if (stored.sourceSeq !== tuple.sourceSeq)
492
+ throw new EpEnvelopeError("expired", `the commit names stream sequence ${tuple.sourceSeq} but the lease binds ${stored.sourceSeq}; a stale execution binding never settles (SPEC 13.5)`);
493
+ if (stored.attempt !== tuple.attempt || stored.fencingToken !== tuple.fencingToken)
494
+ throw new EpEnvelopeError("expired", `stale fencing: the commit carries (attempt ${tuple.attempt}, token ${tuple.fencingToken}) but the lease is (attempt ${stored.attempt}, token ${stored.fencingToken}) (SPEC 13.5)`);
495
+ if (!sameWorker(bound, caller))
496
+ throw new EpEnvelopeError("permission-denied", `the commit caller is not the lease's bound worker (${bound.owner}.${bound.actor}/${bound.lifecycleUid}); the binding is owner-recorded at assignment (SPEC 13.5)`);
497
+ // FRESH lifecycle/epoch currency for an endpoint worker (§13.8): a superseded process cannot
498
+ // settle its predecessor's lease.
499
+ if (bound.kind === "endpoint") {
500
+ if (typeof resolveCurrentEpoch !== "function")
501
+ throw new EpEnvelopeError("failed-precondition", `committing an endpoint worker's item requires a fresh-epoch resolver (SPEC 13.8: lifecycle/epoch currency is validated at the commit boundary)`);
502
+ const budget = epochResolveBudgetMs ?? 5_000;
503
+ if (!Number.isSafeInteger(budget) || budget <= 0)
504
+ throw new EpEnvelopeError("failed-precondition", `epochResolveBudgetMs must be a positive integer; got ${JSON.stringify(epochResolveBudgetMs)}`);
505
+ const current = await resolveWithBudget(resolveCurrentEpoch(bound), budget);
506
+ if (current !== null && (typeof current !== "number" || !Number.isSafeInteger(current) || current < 0))
507
+ throw new EpEnvelopeError("internal", `the fresh-epoch resolver returned ${JSON.stringify(current)}; a non-integer epoch never authorizes (SPEC 13.8)`);
508
+ if (current === null)
509
+ throw new EpEnvelopeError("expired", `the endpoint worker's lifecycle is retired/unknown; a retired worker cannot commit (SPEC 13.8)`);
510
+ if (current !== bound.epoch)
511
+ throw new EpEnvelopeError("expired", `the endpoint worker's lease bound epoch ${bound.epoch} but the current process epoch is ${current}; a superseded process cannot settle (SPEC 13.8)`);
512
+ }
513
+ if (now >= stored.workExpiry)
514
+ throw new EpEnvelopeError("expired", `the item's workExpiry (${stored.workExpiry}) has passed at the owner clock (${now}); the item is dead, leased or not (SPEC 13.8)`);
515
+ if (now >= stored.leaseDeadline)
516
+ throw new EpEnvelopeError("expired", `the lease expired at ${stored.leaseDeadline} (owner clock ${now}); expiry revokes the claim AT that deadline (SPEC 13.5)`);
517
+ // THE FENCE: advance the lease to settled on its OWN revision. A concurrent redelivery-advance
518
+ // or expiry-settle contends on this exact revision; the loser re-reads and re-decides.
519
+ const settled = { ...stored, state: "settled", disposition: "committed", outcome: outcomeSnapshot, committedTs: now };
520
+ try {
521
+ await updateRecordEntry(ctx.kv, key, settled, entry.revision);
522
+ }
523
+ catch (e) {
524
+ if (!(e instanceof EpEnvelopeError && e.code === "conflict"))
525
+ throw e;
526
+ const now2 = parseLease(decodeJson((await ctx.kv.get(key)).value, key), key);
527
+ if (now2.state === "settled" && now2.sourceSeq === tuple.sourceSeq && now2.attempt === tuple.attempt && sameWorker(now2.worker, caller))
528
+ return publishTerminal(ctx, ref, terminalOf(ref, now2)); // our own settle raced in; idempotent
529
+ if (now2.state === "settled")
530
+ throw new EpEnvelopeError("conflict", `the item settled ${now2.disposition} concurrently under a different attempt/worker; this commit is superseded (SPEC 13.5)`);
531
+ throw new EpEnvelopeError("expired", `a concurrent redelivery advanced the lease to attempt ${now2.attempt}; this commit is superseded (SPEC 13.5)`);
532
+ }
533
+ return publishTerminal(ctx, ref, terminalOf(ref, settled));
534
+ }
535
+ /** Bound the fresh-epoch resolver await: past the budget the commit REFUSES `unavailable`
536
+ * (fail-closed, retryable) instead of hanging on a stuck lifecycle authority. */
537
+ async function resolveWithBudget(p, budgetMs) {
538
+ // race Promise.resolve(p) UNCONDITIONALLY: a non-native/cross-realm thenable is not an
539
+ // `instanceof Promise`, and returning it directly would await it unbounded.
540
+ let timer;
541
+ const deadline = new Promise((_, reject) => {
542
+ timer = setTimeout(() => reject(new EpEnvelopeError("unavailable", `the fresh-epoch resolver did not answer within ${budgetMs}ms; a stuck lifecycle authority is a bounded refusal, never a hung commit (SPEC 13.8)`)), budgetMs);
543
+ });
544
+ try {
545
+ return await Promise.race([Promise.resolve(p), deadline]);
546
+ }
547
+ finally {
548
+ clearTimeout(timer);
549
+ }
550
+ }
551
+ async function publishCreateOnly(js, subject, bytes) {
552
+ const h = natsHeaders();
553
+ h.set("Nats-Expected-Last-Subject-Sequence", "0");
554
+ try {
555
+ await js.publish(subject, bytes, { headers: h });
556
+ return { won: true };
557
+ }
558
+ catch (e) {
559
+ const code = e?.code;
560
+ if (code === 10071 || code === 10164)
561
+ return { won: false };
562
+ throw e;
563
+ }
564
+ }
565
+ /** Decide and repair one item against the §13.6 predicate — the canonicalizer's reconciliation
566
+ * seam (§13.9 row), for an ACCEPTED pool-routed item. The LEASE is consulted as the settlement
567
+ * arbiter so reconciliation never contradicts a committing worker:
568
+ * 1. a terminal `wrk` fact exists → SETTLED (the owner acks without effect);
569
+ * 2. the lease is `settled` (a commit fenced it, maybe crashed before publishing) → publish
570
+ * the derived terminal (idempotent) → SETTLED — recovery, never re-enqueued;
571
+ * 3. `now >= workExpiry` → the item is DEAD, leased or not: fence it by CAS-settling the lease
572
+ * `expired` (racing a live commit on the SAME key; a lost CAS re-reads the winner), then
573
+ * publish the derived terminal → EXPIRED-SETTLED; with no lease record, a worker-less
574
+ * `settled:expired` lease is CAS-CREATED on the same key first, so a racing FIRST lease
575
+ * contends there instead of assigning dead work behind the expiry;
576
+ * 4. a live pool entry exists (subject-confined LEADER-SERVED STREAM.MSG.GET, §13.6:1797-1799 —
577
+ * a fencing read whose stale follower miss would re-arm settled work) → LIVE;
578
+ * 5. else re-check the terminal (a commit may have landed since step 1), then re-enqueue the
579
+ * SAME acceptance-derived bytes create-only — the ONLY re-enqueueable state.
580
+ * Fail-closed preconditions: a DEL/PURGE marker on the lease REFUSES before any classification
581
+ * (reconciling over a deletion could recreate authoritative state), and whenever a lease record
582
+ * exists the caller-supplied `workExpiry` must EQUAL the persisted horizon (§13.8) — a mis-wired
583
+ * reconciliation never expires a live lease against a foreign horizon. */
584
+ export async function reconcileWorkItem(ctx, args) {
585
+ assertCtx(ctx);
586
+ // Snapshot the FULL operation input at entry (ref, clock, horizon, and a DETACHED copy of the
587
+ // item bytes): nothing below reads args again, so a mid-flight mutation cannot re-enqueue
588
+ // different bytes or move the horizon between classification and repair.
589
+ const ref = snapshotRef(args.ref);
590
+ const now = assertSafeInt(args.now, "now");
591
+ const workExpiry = assertSafeInt(args.workExpiry, "workExpiry");
592
+ if (!(args.itemBytes instanceof Uint8Array))
593
+ throw new EpEnvelopeError("failed-precondition", `itemBytes must be a Uint8Array (the acceptance-derived stored bytes, SPEC 13.6)`);
594
+ const itemBytes = new Uint8Array(args.itemBytes); // real copy (NOT .slice(): a Node Buffer aliases through .slice(), defeating the detachment; new Uint8Array(...) always copies)
595
+ const key = leaseKeyOf(ref);
596
+ // A DEL/PURGE marker on the lease is REFUSED before any classification (same fail-closed rule
597
+ // as lease/commit): reconciling over a deletion could recreate authoritative state.
598
+ const readLease = (entry) => {
599
+ if (!entry)
600
+ return undefined;
601
+ if (entry.operation !== "PUT")
602
+ throw new EpEnvelopeError("failed-precondition", `the lease record ${key} carries a ${entry.operation} marker; a deletion never resets an authoritative lease - reconcile the store (SPEC 13.5)`);
603
+ const rec = parseLease(decodeJson(entry.value, key), key);
604
+ // The horizon is IDENTITY-BOUND (§13.8): whenever a record exists, the caller-supplied
605
+ // workExpiry must equal the persisted one before ANY classification - a mis-wired
606
+ // reconciliation must not expire a live lease early (or late) against a foreign horizon.
607
+ if (rec.workExpiry !== workExpiry)
608
+ throw new EpEnvelopeError("conflict", `the lease binds workExpiry ${rec.workExpiry} but this reconcile supplies ${workExpiry}; the absolute work horizon is fixed at acceptance and never re-set (SPEC 13.8)`);
609
+ return rec;
610
+ };
611
+ const terminal = await readWorkTerminal(ctx, ref);
612
+ if (terminal !== undefined)
613
+ return { state: terminal.disposition === "expired" ? "expired-settled" : "settled", fact: terminal };
614
+ const leaseEntry = await ctx.kv.get(key);
615
+ const lease = readLease(leaseEntry);
616
+ // (2) a commit fenced the lease but may have crashed before publishing — finalize its terminal.
617
+ if (lease?.state === "settled") {
618
+ const pub = await publishTerminal(ctx, ref, terminalOf(ref, lease));
619
+ return { state: pub.fact.disposition === "expired" ? "expired-settled" : "settled", fact: pub.fact };
620
+ }
621
+ // (3) dead item: the LEASE KEY is the arbiter either way. With a lease, CAS-settle it expired
622
+ // on its revision (a live commit contends on the SAME key); with NO lease, CAS-CREATE a
623
+ // worker-less settled:expired record create-only, so a racing FIRST lease loses its create and
624
+ // observes the settlement instead of assigning dead work. Only a settled lease derives the
625
+ // terminal — the EPF fact never leads the lease.
626
+ if (now >= workExpiry) {
627
+ for (let pass = 0; pass < 2; pass++) {
628
+ const entry = pass === 0 ? leaseEntry : await ctx.kv.get(key);
629
+ const cur = readLease(entry);
630
+ if (cur?.state === "settled") { // a commit/expiry won; finalize the winner's terminal
631
+ const pub = await publishTerminal(ctx, ref, terminalOf(ref, cur));
632
+ return { state: pub.fact.disposition === "expired" ? "expired-settled" : "settled", fact: pub.fact };
633
+ }
634
+ const settledExpired = cur !== undefined
635
+ ? { ...cur, state: "settled", disposition: "expired", committedTs: now }
636
+ : { v: 1, state: "settled", sourceSeq: 0, attempt: 0, fencingToken: 0, leaseDeadline: workExpiry, workExpiry, disposition: "expired", committedTs: now };
637
+ try {
638
+ if (cur !== undefined)
639
+ await updateRecordEntry(ctx.kv, key, settledExpired, entry.revision);
640
+ else
641
+ await createRecordEntry(ctx.kv, key, settledExpired);
642
+ }
643
+ catch (e) {
644
+ if (e instanceof EpEnvelopeError && e.code === "conflict")
645
+ continue; // a lease/commit raced in on the same key; re-read and re-decide
646
+ throw e;
647
+ }
648
+ const pub = await publishTerminal(ctx, ref, terminalOf(ref, settledExpired));
649
+ return { state: "expired-settled", fact: pub.fact };
650
+ }
651
+ throw new EpEnvelopeError("conflict", `the lease record ${key} moved twice during one reconcile; re-read and re-decide (SPEC 13.8)`);
652
+ }
653
+ if (await liveEntryExists(ctx, ref))
654
+ return { state: "live" };
655
+ // (5) re-check the terminal (a commit may have landed during the live probe) before re-enqueue.
656
+ const late = await readWorkTerminal(ctx, ref);
657
+ if (late !== undefined)
658
+ return { state: late.disposition === "expired" ? "expired-settled" : "settled", fact: late };
659
+ const re = await enqueueWorkItem(ctx, ref, itemBytes);
660
+ if (!re.enqueued) {
661
+ if (await liveEntryExists(ctx, ref))
662
+ return { state: "live" };
663
+ const after = await readWorkTerminal(ctx, ref);
664
+ if (after !== undefined)
665
+ return { state: after.disposition === "expired" ? "expired-settled" : "settled", fact: after };
666
+ throw new EpEnvelopeError("failed-precondition", `the item is not settled, not expired, not live, and its create-only re-enqueue is refused by the stream's per-subject history; needs operator reconciliation (SPEC 13.6)`);
667
+ }
668
+ return { state: "re-enqueued", seq: re.seq };
669
+ }
670
+ /** Settle a still-live pool item as `retired` for the §13.1 exact-pool cleaner. Unlike ordinary
671
+ * expiry reconciliation, retirement is target-bound and may settle unexpired work, but it still
672
+ * uses the lease key as the single arbiter: a racing commit, lease advance, or cleaner settlement
673
+ * all contend on this revision before any terminal fact is published. */
674
+ export async function retireWorkItem(ctx, args) {
675
+ assertCtx(ctx);
676
+ const ref = snapshotRef(args.ref);
677
+ const workExpiry = assertSafeInt(args.workExpiry, "workExpiry");
678
+ const opId = assertLifecycleToken(args.opId, "retirement opId");
679
+ const targetUid = assertLifecycleToken(args.targetUid, "retirement targetUid");
680
+ const now = assertSafeInt(args.now, "now");
681
+ const terminal = await readWorkTerminal(ctx, ref);
682
+ if (terminal !== undefined)
683
+ return { won: false, fact: terminal };
684
+ const key = leaseKeyOf(ref);
685
+ for (let pass = 0; pass < 2; pass++) {
686
+ const entry = await ctx.kv.get(key);
687
+ if (entry !== undefined && entry !== null && entry.operation !== "PUT")
688
+ throw new EpEnvelopeError("failed-precondition", `the lease record ${key} carries a ${entry.operation} marker; a deletion never resets an authoritative lease (SPEC 13.5)`);
689
+ const cur = entry === undefined || entry === null ? undefined : parseLease(decodeJson(entry.value, key), key);
690
+ if (cur?.state === "settled")
691
+ return publishTerminal(ctx, ref, terminalOf(ref, cur));
692
+ if (cur !== undefined && cur.workExpiry !== workExpiry)
693
+ throw new EpEnvelopeError("conflict", `the lease binds workExpiry ${cur.workExpiry} but retirement supplies ${workExpiry}; the absolute work horizon is fixed at acceptance and never re-set (SPEC 13.8)`);
694
+ const retired = cur !== undefined
695
+ ? { ...cur, state: "settled", disposition: "retired", opId, targetUid, committedTs: now }
696
+ : { v: 1, state: "settled", sourceSeq: 0, attempt: 0, fencingToken: 0, leaseDeadline: workExpiry, workExpiry, disposition: "retired", opId, targetUid, committedTs: now };
697
+ try {
698
+ if (cur !== undefined)
699
+ await updateRecordEntry(ctx.kv, key, retired, entry.revision);
700
+ else
701
+ await createRecordEntry(ctx.kv, key, retired);
702
+ }
703
+ catch (e) {
704
+ if (e instanceof EpEnvelopeError && e.code === "conflict")
705
+ continue;
706
+ throw e;
707
+ }
708
+ return publishTerminal(ctx, ref, terminalOf(ref, retired));
709
+ }
710
+ throw new EpEnvelopeError("conflict", `the lease record ${key} moved twice during one retirement settle; re-read and re-decide (SPEC 13.8)`);
711
+ }
712
+ /** The §13.6 liveness read: the subject-confined LEADER-SERVED last-by-subject probe on the
713
+ * item's own subject (an acked item has LEFT the WorkQueue; an in-flight one remains readable).
714
+ * STREAM.MSG.GET, never a follower Direct Get (SPEC 13.6:1797-1799): the result gates the
715
+ * re-enqueue decision, so a stale follower miss would re-arm settled work. ONLY the broker's
716
+ * no-message result is absence; every other failure is `unavailable`, never fabricated as "no
717
+ * live entry" (which would drive an incorrect re-enqueue). */
718
+ async function liveEntryExists(ctx, ref) {
719
+ try {
720
+ return (await ctx.jsm.streams.getMessage(epwStreamName(ctx.space), { last_by_subj: workItemSubject(ctx.space, ref) })) !== null;
721
+ }
722
+ catch (e) {
723
+ if (isNoMessage(e))
724
+ return false;
725
+ throw new EpEnvelopeError("unavailable", `the reconciliation liveness probe failed (a failed observation is never absence, SPEC 13.6): ${e?.message ?? String(e)}`);
726
+ }
727
+ }
728
+ //# sourceMappingURL=endpoint-work.js.map