@unson/brainbase-mcp 0.6.0 → 0.7.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 (80) hide show
  1. package/dist/canonical-task-contract.d.ts +9 -0
  2. package/dist/canonical-task-contract.js +39 -0
  3. package/dist/canonical-task-principal.d.ts +16 -0
  4. package/dist/canonical-task-principal.js +36 -0
  5. package/dist/canonical-task-service.d.ts +251 -0
  6. package/dist/canonical-task-service.js +809 -0
  7. package/dist/company-os-evaluation.d.ts +193 -0
  8. package/dist/company-os-evaluation.js +1150 -0
  9. package/dist/company-os-impact-review.d.ts +248 -0
  10. package/dist/company-os-impact-review.js +663 -0
  11. package/dist/company-os-learning-adoption.d.ts +288 -0
  12. package/dist/company-os-learning-adoption.js +944 -0
  13. package/dist/company-os-objectives.d.ts +52 -0
  14. package/dist/company-os-objectives.js +197 -0
  15. package/dist/company-os-receipt-adapter.d.ts +109 -0
  16. package/dist/company-os-receipt-adapter.js +636 -0
  17. package/dist/constraint-exception-store.d.ts +23 -0
  18. package/dist/constraint-exception-store.js +108 -0
  19. package/dist/constraint-resolution.d.ts +175 -0
  20. package/dist/constraint-resolution.js +922 -0
  21. package/dist/decision-adapter.d.ts +145 -0
  22. package/dist/decision-adapter.js +586 -0
  23. package/dist/durable-wait-http.d.ts +51 -0
  24. package/dist/durable-wait-http.js +497 -0
  25. package/dist/durable-waits.d.ts +234 -0
  26. package/dist/durable-waits.js +944 -0
  27. package/dist/execution-authority-http.d.ts +28 -0
  28. package/dist/execution-authority-http.js +359 -0
  29. package/dist/execution-authority.d.ts +201 -0
  30. package/dist/execution-authority.js +634 -0
  31. package/dist/foundation-catalog.d.ts +26 -0
  32. package/dist/foundation-catalog.js +206 -0
  33. package/dist/foundation-constraint-store.d.ts +50 -0
  34. package/dist/foundation-constraint-store.js +215 -0
  35. package/dist/foundation-http.d.ts +90 -0
  36. package/dist/foundation-http.js +439 -0
  37. package/dist/foundation-store.d.ts +132 -0
  38. package/dist/foundation-store.js +654 -0
  39. package/dist/judgment-dag-composition.d.ts +224 -0
  40. package/dist/judgment-dag-composition.js +731 -0
  41. package/dist/judgment-dag.d.ts +2 -0
  42. package/dist/judgment-dag.js +1 -0
  43. package/dist/judgment-problem-snapshot.d.ts +185 -0
  44. package/dist/judgment-problem-snapshot.js +777 -0
  45. package/dist/judgment-view-http.d.ts +22 -0
  46. package/dist/judgment-view-http.js +128 -0
  47. package/dist/judgment-view.d.ts +222 -0
  48. package/dist/judgment-view.js +627 -0
  49. package/dist/knowledge-adapter.d.ts +112 -0
  50. package/dist/knowledge-adapter.js +691 -0
  51. package/dist/ontology-foundation.d.ts +209 -0
  52. package/dist/ontology-foundation.js +1000 -0
  53. package/dist/ontology.d.ts +13 -0
  54. package/dist/ontology.js +14 -0
  55. package/dist/organization-connection.d.ts +164 -0
  56. package/dist/organization-connection.js +428 -0
  57. package/dist/personal-knowledge.d.ts +120 -0
  58. package/dist/personal-knowledge.js +750 -0
  59. package/dist/problem-candidates.d.ts +149 -0
  60. package/dist/problem-candidates.js +898 -0
  61. package/dist/problem-selection.d.ts +238 -0
  62. package/dist/problem-selection.js +995 -0
  63. package/dist/resource-reservation-http.d.ts +31 -0
  64. package/dist/resource-reservation-http.js +371 -0
  65. package/dist/resource-reservations.d.ts +221 -0
  66. package/dist/resource-reservations.js +577 -0
  67. package/dist/server.d.ts +97 -0
  68. package/dist/server.js +180 -1
  69. package/dist/ssot.d.ts +6 -1
  70. package/dist/ssot.js +63 -5
  71. package/dist/streamable-http.d.ts +42 -0
  72. package/dist/streamable-http.js +288 -0
  73. package/dist/types.d.ts +20 -0
  74. package/dist/world-model.d.ts +181 -0
  75. package/dist/world-model.js +873 -0
  76. package/package.json +139 -3
  77. package/ui/judgment-view.css +238 -0
  78. package/ui/judgment-view.js +508 -0
  79. package/ui/objective-editor.css +180 -0
  80. package/ui/objective-editor.js +1139 -0
@@ -0,0 +1,944 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { defaultJudgmentProblemSnapshotAccessProvider } from './judgment-problem-snapshot.js';
3
+ import { initializePersonalOs, loadPersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
4
+ /** Story13's persisted contract. The ledger is deliberately separate from the
5
+ * canonical graph but is committed by the same Personal OS sidecar transaction.
6
+ */
7
+ export const DURABLE_WAIT_VERSION = 'durable-wait.v1';
8
+ export const DURABLE_WAIT_SIDECAR_PATH = 'durable-waits/ledger.json';
9
+ export const DURABLE_WAIT_LEDGER_VERSION = DURABLE_WAIT_VERSION;
10
+ export class DurableWaitError extends Error {
11
+ code;
12
+ wait_id;
13
+ constructor(code, message, wait_id) {
14
+ super(message);
15
+ this.name = 'DurableWaitError';
16
+ this.code = code;
17
+ this.wait_id = wait_id;
18
+ }
19
+ }
20
+ /** Internal marker for the CAS branch that can reconcile a concurrent claim. */
21
+ class DurableWaitPreflightConflict extends DurableWaitError {
22
+ constructor(message, wait_id) {
23
+ super('state_conflict', message, wait_id);
24
+ }
25
+ }
26
+ const SNAPSHOT_ID_PATTERN = /^sha256:[0-9a-f]{64}$/u;
27
+ const DEFAULT_LEASE_MS = 5 * 60 * 1000;
28
+ const WAIT_STATES = [
29
+ 'waiting', 'claimed', 'resumed', 'handoff_required', 'reconciliation_wait', 'failed', 'cancelled'
30
+ ];
31
+ const RESUME_METHODS = [
32
+ 'resume_run', 'new_problem', 'reconcile_external_effect', 'manual_review'
33
+ ];
34
+ const FAILURE_POLICIES = [
35
+ 'handoff', 'new_problem', 'reconciliation_wait', 'fail'
36
+ ];
37
+ const TRIGGERS = ['event', 'timer', 'manual'];
38
+ export const defaultDurableWaitAccessProvider = {
39
+ authorize({ action, principal, acl }) {
40
+ return defaultJudgmentProblemSnapshotAccessProvider.authorize({
41
+ action: action === 'write' ? 'write' : 'read',
42
+ context: { principal },
43
+ acl
44
+ });
45
+ }
46
+ };
47
+ export class DurableWaitStore {
48
+ dataDir;
49
+ problemSnapshot;
50
+ access;
51
+ clock;
52
+ sidecarPath;
53
+ defaultLeaseMs;
54
+ constructor(options) {
55
+ assertText(options?.dataDir, 'dataDir');
56
+ if (!options.problemSnapshot || typeof options.problemSnapshot.verify !== 'function') {
57
+ throw new DurableWaitError('invalid_request', 'problemSnapshot.verify is required');
58
+ }
59
+ this.dataDir = options.dataDir;
60
+ this.problemSnapshot = options.problemSnapshot;
61
+ this.access = options.access ?? defaultDurableWaitAccessProvider;
62
+ this.clock = options.clock ?? (() => new Date());
63
+ this.sidecarPath = options.sidecarPath ?? DURABLE_WAIT_SIDECAR_PATH;
64
+ assertSidecarPath(this.sidecarPath);
65
+ this.defaultLeaseMs = options.defaultLeaseMs ?? DEFAULT_LEASE_MS;
66
+ assertLeaseMs(this.defaultLeaseMs, 'defaultLeaseMs');
67
+ }
68
+ async initialize() {
69
+ await initializePersonalOs(this.dataDir);
70
+ }
71
+ async create(input) {
72
+ const normalized = normalizeCreateInput(input, this.now());
73
+ await this.initialize();
74
+ const preflight = await this.captureMutationPreflight();
75
+ await this.authorize('write', normalized.principal, normalized.read_policy);
76
+ await this.verifyProblem(normalized.principal, normalized.problem_snapshot, 'save');
77
+ return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
78
+ this.assertMutationPreflight(current, content, preflight);
79
+ const ledger = parseLedger(content);
80
+ if (ledger.waits.some((wait) => wait.wait_id === normalized.wait_id)) {
81
+ throw new DurableWaitError('state_conflict', `Wait ${normalized.wait_id} already exists`, normalized.wait_id);
82
+ }
83
+ const now = normalized.created_at;
84
+ const record = freezeClone({
85
+ wait_id: normalized.wait_id,
86
+ version: DURABLE_WAIT_VERSION,
87
+ owner_scope: normalized.owner_scope,
88
+ read_policy: normalized.read_policy,
89
+ problem_snapshot: normalized.problem_snapshot,
90
+ condition: normalized.condition,
91
+ ...(normalized.deadline === undefined ? {} : { deadline: normalized.deadline }),
92
+ responsible: normalized.responsible,
93
+ resume_method: normalized.resume_method,
94
+ failure_policy: normalized.failure_policy,
95
+ run_ref: normalized.run_ref,
96
+ state: 'waiting',
97
+ created_at: now,
98
+ updated_at: now,
99
+ history: [{
100
+ action: 'create',
101
+ occurred_at: now,
102
+ principal: normalized.principal,
103
+ to_state: 'waiting',
104
+ responsible: normalized.responsible
105
+ }]
106
+ });
107
+ ledger.waits.push(record);
108
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(record) };
109
+ });
110
+ }
111
+ async get(input) {
112
+ const normalized = normalizeReadInput(input);
113
+ await this.initialize();
114
+ const record = await this.readStored(normalized.wait_id);
115
+ await this.authorize('read', normalized.principal, record.read_policy, record);
116
+ await this.verifyProblem(normalized.principal, record.problem_snapshot);
117
+ return freezeClone(record);
118
+ }
119
+ /** Alias used by adapters that model stores as read operations. */
120
+ async read(input) {
121
+ return this.get(input);
122
+ }
123
+ async list(input) {
124
+ assertText(input?.principal, 'principal');
125
+ await this.initialize();
126
+ const ledger = parseLedger(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
127
+ const result = [];
128
+ for (const record of ledger.waits) {
129
+ if (input.owner_scope_id !== undefined && record.owner_scope.id !== input.owner_scope_id)
130
+ continue;
131
+ try {
132
+ await this.authorize('read', input.principal, record.read_policy, record);
133
+ await this.verifyProblem(input.principal, record.problem_snapshot);
134
+ result.push(freezeClone(record));
135
+ }
136
+ catch (error) {
137
+ if (error instanceof DurableWaitError && error.code === 'unauthorized')
138
+ continue;
139
+ throw error;
140
+ }
141
+ }
142
+ return result;
143
+ }
144
+ async claim(input) {
145
+ const normalized = normalizeClaimInput(input, this.now(), this.defaultLeaseMs);
146
+ await this.initialize();
147
+ const preflight = await this.captureMutationPreflight(normalized.wait_id);
148
+ const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
149
+ await this.authorize('claim', normalized.principal, preflightRecord.read_policy, preflightRecord);
150
+ await this.verifyProblem(normalized.principal, preflightRecord.problem_snapshot);
151
+ try {
152
+ return await mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
153
+ const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
154
+ const record = findWait(ledger, normalized.wait_id);
155
+ if (record.state === 'reconciliation_wait') {
156
+ throw new DurableWaitError('reconciliation_required', `Wait ${record.wait_id} requires external-effect reconciliation`, record.wait_id);
157
+ }
158
+ if (record.state === 'handoff_required' || record.state === 'resumed' || record.state === 'failed' || record.state === 'cancelled') {
159
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
160
+ }
161
+ // Validate the incoming event/timer before treating an existing claim as
162
+ // a duplicate. A different, not-yet-ready trigger must not receive a
163
+ // claim it cannot legitimately resume.
164
+ assertTriggerReady(record, normalized);
165
+ if (record.claim !== undefined) {
166
+ if (Date.parse(record.claim.lease.expires_at) > normalized.nowMs) {
167
+ return {
168
+ next: current,
169
+ sidecarContent: serializeLedger(ledger),
170
+ result: {
171
+ wait: freezeClone(record),
172
+ claim: freezeClone(record.claim),
173
+ duplicate: true,
174
+ claimed_by_this_request: record.claim.request_id === normalized.request_id
175
+ && record.claim.claimed_by === normalized.principal
176
+ }
177
+ };
178
+ }
179
+ throw new DurableWaitError('lease_expired', `Wait ${record.wait_id} requires handoff after lease expiry`, record.wait_id);
180
+ }
181
+ const claim = makeClaim(normalized, this.nowFromMillis(normalized.nowMs), normalized.leaseMs);
182
+ const now = this.timestamp(normalized.nowMs);
183
+ const updated = updateRecord(record, {
184
+ state: 'claimed',
185
+ claim,
186
+ updated_at: now,
187
+ history: appendHistory(record, {
188
+ action: 'claim',
189
+ occurred_at: now,
190
+ principal: normalized.principal,
191
+ from_state: record.state,
192
+ to_state: 'claimed',
193
+ detail: normalized.trigger === 'event' ? `event:${normalized.event_type ?? ''}` : normalized.trigger
194
+ })
195
+ });
196
+ replaceWait(ledger, updated);
197
+ return {
198
+ next: current,
199
+ sidecarContent: serializeLedger(ledger),
200
+ result: { wait: freezeClone(updated), claim: freezeClone(claim), duplicate: false, claimed_by_this_request: true }
201
+ };
202
+ });
203
+ }
204
+ catch (error) {
205
+ if (!(error instanceof DurableWaitPreflightConflict))
206
+ throw error;
207
+ // A concurrent winner has already committed the claim. Re-read once,
208
+ // outside the SSOT mutation lock, and return that winner only after the
209
+ // current ACL, snapshot access, trigger context, and lease are checked.
210
+ return this.reconcileClaimAfterPreflightConflict(normalized);
211
+ }
212
+ }
213
+ async handoff(input) {
214
+ const normalized = normalizeHandoffInput(input, this.now(), this.defaultLeaseMs);
215
+ await this.initialize();
216
+ const preflight = await this.captureMutationPreflight(normalized.wait_id);
217
+ const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
218
+ await this.authorize('claim', normalized.principal, preflightRecord.read_policy, preflightRecord);
219
+ await this.verifyProblem(normalized.principal, preflightRecord.problem_snapshot);
220
+ return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
221
+ const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
222
+ const record = findWait(ledger, normalized.wait_id);
223
+ if (record.state !== 'claimed' || record.claim === undefined) {
224
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} has no active claim to hand off`, record.wait_id);
225
+ }
226
+ if (Date.parse(record.claim.lease.expires_at) > normalized.nowMs) {
227
+ throw new DurableWaitError('lease_active', `Wait ${record.wait_id} lease is still active`, record.wait_id);
228
+ }
229
+ const now = this.timestamp(normalized.nowMs);
230
+ const claim = {
231
+ ...record.claim,
232
+ request_id: normalized.request_id,
233
+ lease: makeLease(normalized.responsible.principal, this.nowFromMillis(normalized.nowMs), normalized.leaseMs),
234
+ claimed_by: normalized.responsible.principal,
235
+ claimed_at: now
236
+ };
237
+ const updated = updateRecord(record, {
238
+ state: 'claimed',
239
+ claim,
240
+ responsible: normalized.responsible,
241
+ updated_at: now,
242
+ history: appendHistory(record, {
243
+ action: 'handoff',
244
+ occurred_at: now,
245
+ principal: normalized.principal,
246
+ from_state: record.state,
247
+ to_state: 'claimed',
248
+ detail: normalized.reason,
249
+ responsible: normalized.responsible
250
+ })
251
+ });
252
+ replaceWait(ledger, updated);
253
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(updated) };
254
+ });
255
+ }
256
+ /** Explicit name for scheduler adapters that only hand off expired leases. */
257
+ async handoffExpired(input) {
258
+ return this.handoff(input);
259
+ }
260
+ async resume(input) {
261
+ const normalized = normalizeResumeInput(input);
262
+ await this.initialize();
263
+ const preflight = await this.captureMutationPreflight(normalized.wait_id);
264
+ const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
265
+ await this.authorize('claim', normalized.principal, preflightRecord.read_policy, preflightRecord);
266
+ await this.verifyProblem(normalized.principal, preflightRecord.problem_snapshot);
267
+ return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
268
+ const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
269
+ const record = findWait(ledger, normalized.wait_id);
270
+ if (record.resume_receipt !== undefined) {
271
+ assertResumeBinding(record, normalized);
272
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(record.resume_receipt) };
273
+ }
274
+ if (record.state === 'reconciliation_wait') {
275
+ throw new DurableWaitError('reconciliation_required', `Wait ${record.wait_id} requires reconciliation before resume`, record.wait_id);
276
+ }
277
+ if (record.state === 'handoff_required') {
278
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} requires a new Problem before resume`, record.wait_id);
279
+ }
280
+ if (record.state !== 'claimed' || record.claim === undefined) {
281
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is not claimed`, record.wait_id);
282
+ }
283
+ assertResumeBinding(record, normalized);
284
+ if (Date.parse(record.claim.lease.expires_at) <= this.now().getTime()) {
285
+ throw new DurableWaitError('lease_expired', `Wait ${record.wait_id} lease has expired`, record.wait_id);
286
+ }
287
+ const now = this.timestamp(this.now().getTime());
288
+ const receipt = {
289
+ resume_id: `resume-${randomUUID()}`,
290
+ wait_id: record.wait_id,
291
+ claim_id: record.claim.claim_id,
292
+ run_ref: record.run_ref,
293
+ recorded_at: now
294
+ };
295
+ const updated = updateRecord(record, {
296
+ state: 'resumed',
297
+ resume_receipt: receipt,
298
+ updated_at: now,
299
+ history: appendHistory(record, {
300
+ action: 'resume',
301
+ occurred_at: now,
302
+ principal: normalized.principal,
303
+ from_state: record.state,
304
+ to_state: 'resumed'
305
+ })
306
+ });
307
+ replaceWait(ledger, updated);
308
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(receipt) };
309
+ });
310
+ }
311
+ async markPremiseChanged(input) {
312
+ const normalized = normalizePremiseChangedInput(input);
313
+ await this.initialize();
314
+ const preflight = await this.captureMutationPreflight(normalized.wait_id);
315
+ const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
316
+ await this.authorize('claim', normalized.principal, preflightRecord.read_policy, preflightRecord);
317
+ await this.verifyProblem(normalized.principal, preflightRecord.problem_snapshot);
318
+ await this.verifyProblem(normalized.principal, normalized.new_problem_snapshot);
319
+ return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
320
+ const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
321
+ const record = findWait(ledger, normalized.wait_id);
322
+ if (record.state === 'resumed' || record.state === 'cancelled' || record.state === 'failed') {
323
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
324
+ }
325
+ const now = this.now().toISOString();
326
+ const updated = updateRecord(record, {
327
+ state: 'handoff_required',
328
+ next_problem: normalized.new_problem_snapshot,
329
+ ...(normalized.responsible === undefined ? {} : { responsible: normalized.responsible }),
330
+ updated_at: now,
331
+ history: appendHistory(record, {
332
+ action: 'premise_changed',
333
+ occurred_at: now,
334
+ principal: normalized.principal,
335
+ from_state: record.state,
336
+ to_state: 'handoff_required',
337
+ detail: normalized.reason,
338
+ ...(normalized.responsible === undefined ? {} : { responsible: normalized.responsible })
339
+ })
340
+ });
341
+ replaceWait(ledger, updated);
342
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(updated) };
343
+ });
344
+ }
345
+ async markEffectUnknown(input) {
346
+ const normalized = normalizeEffectUnknownInput(input);
347
+ await this.initialize();
348
+ const preflight = await this.captureMutationPreflight(normalized.wait_id);
349
+ const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
350
+ await this.authorize('claim', normalized.principal, preflightRecord.read_policy, preflightRecord);
351
+ await this.verifyProblem(normalized.principal, preflightRecord.problem_snapshot);
352
+ return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
353
+ const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
354
+ const record = findWait(ledger, normalized.wait_id);
355
+ if (record.state === 'resumed' || record.state === 'cancelled' || record.state === 'failed') {
356
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
357
+ }
358
+ const now = this.now().toISOString();
359
+ const responsible = normalized.responsible ?? record.responsible;
360
+ const updated = updateRecord(record, {
361
+ state: 'reconciliation_wait',
362
+ responsible,
363
+ reconciliation: { reason: normalized.reason, recorded_at: now, responsible },
364
+ updated_at: now,
365
+ history: appendHistory(record, {
366
+ action: 'effect_unknown',
367
+ occurred_at: now,
368
+ principal: normalized.principal,
369
+ from_state: record.state,
370
+ to_state: 'reconciliation_wait',
371
+ detail: normalized.reason,
372
+ responsible
373
+ })
374
+ });
375
+ replaceWait(ledger, updated);
376
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(updated) };
377
+ });
378
+ }
379
+ async captureMutationPreflight(waitId) {
380
+ // These reads intentionally happen outside mutatePersonalOsWithSidecar's
381
+ // lock. A trusted provider may use GraphFoundationRevisionStore or read
382
+ // another sidecar rooted at the same dataDir, both of which acquire the
383
+ // canonical lock themselves.
384
+ const sidecarContent = await readPersonalOsSidecar(this.dataDir, this.sidecarPath);
385
+ const ledger = parseLedger(sidecarContent);
386
+ const record = waitId === undefined ? undefined : findWait(ledger, waitId);
387
+ const current = await loadPersonalOs(this.dataDir);
388
+ return {
389
+ aggregateFingerprint: fingerprintPersonalOs(current),
390
+ sidecarFingerprint: fingerprintText(sidecarContent),
391
+ ...(record === undefined ? {} : { record, waitFingerprint: fingerprintRecord(record) })
392
+ };
393
+ }
394
+ assertMutationPreflight(current, sidecarContent, preflight, waitId) {
395
+ // This is the compare-and-swap boundary. Providers have already run, so
396
+ // no external await is permitted in the SSOT lock callback. If either
397
+ // aggregate changed after preflight, retry against the newly authorized
398
+ // state rather than committing a stale decision.
399
+ if (fingerprintPersonalOs(current) !== preflight.aggregateFingerprint
400
+ || fingerprintText(sidecarContent) !== preflight.sidecarFingerprint) {
401
+ throw new DurableWaitPreflightConflict('Personal OS changed while validating the durable wait; retry', waitId);
402
+ }
403
+ const ledger = parseLedger(sidecarContent);
404
+ if (waitId !== undefined) {
405
+ const record = findWait(ledger, waitId);
406
+ if (preflight.waitFingerprint === undefined || fingerprintRecord(record) !== preflight.waitFingerprint) {
407
+ throw new DurableWaitPreflightConflict(`Wait ${waitId} changed while validating the durable wait; retry`, waitId);
408
+ }
409
+ }
410
+ return ledger;
411
+ }
412
+ async readStored(waitId) {
413
+ const ledger = parseLedger(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
414
+ return findWait(ledger, waitId);
415
+ }
416
+ async reconcileClaimAfterPreflightConflict(input) {
417
+ const record = await this.readStored(input.wait_id);
418
+ await this.authorize('claim', input.principal, record.read_policy, record);
419
+ await this.verifyProblem(input.principal, record.problem_snapshot);
420
+ if (record.state === 'reconciliation_wait') {
421
+ throw new DurableWaitError('reconciliation_required', `Wait ${record.wait_id} requires external-effect reconciliation`, record.wait_id);
422
+ }
423
+ if (record.state === 'handoff_required' || record.state === 'resumed' || record.state === 'failed' || record.state === 'cancelled') {
424
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
425
+ }
426
+ assertTriggerReady(record, input);
427
+ if (record.state !== 'claimed' || record.claim === undefined) {
428
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} has no winning claim`, record.wait_id);
429
+ }
430
+ if (Date.parse(record.claim.lease.expires_at) <= input.nowMs) {
431
+ throw new DurableWaitError('lease_expired', `Wait ${record.wait_id} requires handoff after lease expiry`, record.wait_id);
432
+ }
433
+ return {
434
+ wait: freezeClone(record),
435
+ claim: freezeClone(record.claim),
436
+ duplicate: true,
437
+ claimed_by_this_request: record.claim.request_id === input.request_id
438
+ && record.claim.claimed_by === input.principal
439
+ };
440
+ }
441
+ async authorize(action, principal, acl, wait) {
442
+ try {
443
+ const allowed = await this.access.authorize({ action, principal, acl, wait });
444
+ if (allowed === false)
445
+ throw new Error('provider denied access');
446
+ }
447
+ catch (error) {
448
+ throw new DurableWaitError('unauthorized', `${action} access was denied: ${error instanceof Error ? error.message : String(error)}`);
449
+ }
450
+ }
451
+ async verifyProblem(principal, reference, phase = 'read') {
452
+ try {
453
+ const verified = await this.problemSnapshot.verify({ principal, reference, phase });
454
+ if (verified === false)
455
+ throw new Error('problem snapshot provider rejected reference');
456
+ }
457
+ catch (error) {
458
+ if (error instanceof DurableWaitError)
459
+ throw error;
460
+ throw new DurableWaitError('problem_snapshot_invalid', `Problem snapshot ${reference.problem_id}@${reference.revision} could not be verified`);
461
+ }
462
+ }
463
+ now() {
464
+ const value = this.clock();
465
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
466
+ throw new DurableWaitError('invalid_request', 'clock returned an invalid date');
467
+ }
468
+ return new Date(value.getTime());
469
+ }
470
+ nowFromMillis(value) {
471
+ const date = new Date(value);
472
+ if (!Number.isFinite(date.getTime()))
473
+ throw new DurableWaitError('invalid_request', 'timestamp is invalid');
474
+ return date;
475
+ }
476
+ timestamp(value) {
477
+ return this.nowFromMillis(value).toISOString();
478
+ }
479
+ }
480
+ export function createDurableWaitStore(options) {
481
+ return new DurableWaitStore(options);
482
+ }
483
+ function normalizeCreateInput(input, now) {
484
+ assertText(input?.principal, 'principal');
485
+ const waitId = input.wait_id ?? `wait-${randomUUID()}`;
486
+ assertText(waitId, 'wait_id');
487
+ const ownerScope = normalizeScope(input.owner_scope, 'owner_scope');
488
+ const readPolicy = normalizeAcl(input.read_policy, 'read_policy');
489
+ const problem = input.problem_snapshot ?? input.problem;
490
+ if (problem === undefined)
491
+ throw new DurableWaitError('invalid_request', 'problem_snapshot is required');
492
+ const problemSnapshot = normalizeProblemReference(problem, 'problem_snapshot');
493
+ const condition = normalizeCondition(input.condition);
494
+ const deadline = input.deadline === undefined ? undefined : normalizeDeadline(input.deadline, now);
495
+ if (condition.event === undefined && deadline === undefined) {
496
+ throw new DurableWaitError('invalid_request', 'event condition or deadline is required');
497
+ }
498
+ if (input.problem_snapshot !== undefined && input.problem !== undefined && !sameProblem(input.problem_snapshot, input.problem)) {
499
+ throw new DurableWaitError('invalid_request', 'problem and problem_snapshot must identify the same immutable snapshot');
500
+ }
501
+ const responsible = normalizeResponsible(input.responsible, 'responsible');
502
+ const resumeMethod = enumValue(input.resume_method, RESUME_METHODS, 'resume_method');
503
+ const failurePolicy = enumValue(input.failure_policy, FAILURE_POLICIES, 'failure_policy');
504
+ const runRef = normalizeRunReference(input.run_ref);
505
+ const createdAt = input.created_at ?? now.toISOString();
506
+ assertTimestamp(createdAt, 'created_at');
507
+ return {
508
+ wait_id: waitId,
509
+ principal: input.principal,
510
+ owner_scope: ownerScope,
511
+ read_policy: readPolicy,
512
+ problem_snapshot: problemSnapshot,
513
+ condition,
514
+ ...(deadline === undefined ? {} : { deadline }),
515
+ responsible,
516
+ resume_method: resumeMethod,
517
+ failure_policy: failurePolicy,
518
+ run_ref: runRef,
519
+ created_at: createdAt
520
+ };
521
+ }
522
+ function normalizeReadInput(input) {
523
+ assertText(input?.wait_id, 'wait_id');
524
+ assertText(input?.principal, 'principal');
525
+ return { wait_id: input.wait_id, principal: input.principal };
526
+ }
527
+ function normalizeClaimInput(input, now, defaultLeaseMs) {
528
+ const base = normalizeReadInput(input);
529
+ const requestId = boundedText(input.request_id, 'request_id');
530
+ const trigger = enumValue(input.trigger, TRIGGERS, 'trigger');
531
+ const leaseMs = input.lease_ms ?? defaultLeaseMs;
532
+ assertLeaseMs(leaseMs, 'lease_ms');
533
+ const nowMs = now.getTime();
534
+ if (input.occurred_at !== undefined)
535
+ assertTimestamp(input.occurred_at, 'occurred_at');
536
+ if (input.event_type !== undefined)
537
+ assertText(input.event_type, 'event_type');
538
+ if (input.event_id !== undefined)
539
+ assertText(input.event_id, 'event_id');
540
+ return { ...base, request_id: requestId, trigger, ...(input.event_type === undefined ? {} : { event_type: input.event_type }), ...(input.event_id === undefined ? {} : { event_id: input.event_id }), ...(input.occurred_at === undefined ? {} : { occurred_at: input.occurred_at }), lease_ms: leaseMs, nowMs, leaseMs };
541
+ }
542
+ function normalizeHandoffInput(input, now, defaultLeaseMs) {
543
+ const base = normalizeReadInput(input);
544
+ const requestId = boundedText(input.request_id, 'request_id');
545
+ const responsible = normalizeResponsible(input.responsible, 'responsible');
546
+ const leaseMs = input.lease_ms ?? defaultLeaseMs;
547
+ assertLeaseMs(leaseMs, 'lease_ms');
548
+ if (input.now !== undefined)
549
+ assertTimestamp(input.now, 'now');
550
+ if (input.reason !== undefined)
551
+ assertText(input.reason, 'reason');
552
+ const nowMs = input.now === undefined ? now.getTime() : Date.parse(input.now);
553
+ return { ...base, request_id: requestId, responsible, lease_ms: leaseMs, now: input.now, ...(input.reason === undefined ? {} : { reason: input.reason }), nowMs, leaseMs };
554
+ }
555
+ function normalizeResumeInput(input) {
556
+ const base = normalizeReadInput(input);
557
+ const requestId = boundedText(input.request_id, 'request_id');
558
+ assertText(input.claim_id, 'claim_id');
559
+ assertText(input.lease_id, 'lease_id');
560
+ assertText(input.lease_token, 'lease_token');
561
+ return { ...base, request_id: requestId, claim_id: input.claim_id, lease_id: input.lease_id, lease_token: input.lease_token };
562
+ }
563
+ function normalizePremiseChangedInput(input) {
564
+ const base = normalizeReadInput(input);
565
+ const problem = input.new_problem_snapshot ?? input.new_problem;
566
+ if (problem === undefined)
567
+ throw new DurableWaitError('invalid_request', 'new_problem_snapshot is required');
568
+ const newProblem = normalizeProblemReference(problem, 'new_problem_snapshot');
569
+ if (input.new_problem_snapshot !== undefined && input.new_problem !== undefined && !sameProblem(input.new_problem_snapshot, input.new_problem)) {
570
+ throw new DurableWaitError('invalid_request', 'new_problem and new_problem_snapshot must identify the same snapshot');
571
+ }
572
+ assertText(input.reason, 'reason');
573
+ const responsible = input.responsible === undefined ? undefined : normalizeResponsible(input.responsible, 'responsible');
574
+ return { ...base, new_problem_snapshot: newProblem, reason: input.reason, ...(responsible === undefined ? {} : { responsible }) };
575
+ }
576
+ function normalizeEffectUnknownInput(input) {
577
+ const base = normalizeReadInput(input);
578
+ assertText(input.reason, 'reason');
579
+ const responsible = input.responsible === undefined ? undefined : normalizeResponsible(input.responsible, 'responsible');
580
+ return { ...base, reason: input.reason, ...(responsible === undefined ? {} : { responsible }) };
581
+ }
582
+ function normalizeCondition(value) {
583
+ if (value === undefined || value === null || typeof value !== 'object')
584
+ throw new DurableWaitError('invalid_request', 'condition is required');
585
+ const event = value.event === undefined ? undefined : normalizeEventCondition(value.event);
586
+ const expression = value.expression === undefined ? undefined : boundedText(value.expression, 'condition.expression');
587
+ if (event === undefined && expression === undefined)
588
+ throw new DurableWaitError('invalid_request', 'condition must contain an event or expression');
589
+ return { ...(event === undefined ? {} : { event }), ...(expression === undefined ? {} : { expression }) };
590
+ }
591
+ function normalizeEventCondition(value) {
592
+ const eventType = boundedText(value?.event_type, 'condition.event.event_type');
593
+ const eventId = value.event_id === undefined ? undefined : boundedText(value.event_id, 'condition.event.event_id');
594
+ const predicate = value.predicate === undefined ? undefined : boundedText(value.predicate, 'condition.event.predicate');
595
+ return { event_type: eventType, ...(eventId === undefined ? {} : { event_id: eventId }), ...(predicate === undefined ? {} : { predicate }) };
596
+ }
597
+ function normalizeDeadline(value, now) {
598
+ if (value === undefined || value === null || typeof value !== 'object')
599
+ throw new DurableWaitError('invalid_request', 'deadline must be an object');
600
+ const dueAt = value.due_at === undefined ? undefined : boundedText(value.due_at, 'deadline.due_at');
601
+ if (dueAt !== undefined)
602
+ assertTimestamp(dueAt, 'deadline.due_at');
603
+ const interval = value.review_interval_ms;
604
+ if (dueAt === undefined && interval === undefined)
605
+ throw new DurableWaitError('invalid_request', 'deadline requires due_at or review_interval_ms');
606
+ if (interval !== undefined)
607
+ assertLeaseMs(interval, 'deadline.review_interval_ms');
608
+ const nextReviewAt = interval === undefined ? undefined : new Date(now.getTime() + interval).toISOString();
609
+ return { ...(dueAt === undefined ? {} : { due_at: dueAt }), ...(interval === undefined ? {} : { review_interval_ms: interval }), ...(nextReviewAt === undefined ? {} : { next_review_at: nextReviewAt }) };
610
+ }
611
+ function normalizeScope(value, label) {
612
+ if (value === undefined || value === null || typeof value !== 'object')
613
+ throw new DurableWaitError('invalid_request', `${label} is required`);
614
+ assertText(value.type, `${label}.type`);
615
+ if (!['personal', 'project', 'organization'].includes(value.type))
616
+ throw new DurableWaitError('invalid_request', `${label}.type is invalid`);
617
+ assertText(value.id, `${label}.id`);
618
+ return { type: value.type, id: value.id };
619
+ }
620
+ function normalizeAcl(value, label) {
621
+ if (value === undefined || value === null || typeof value !== 'object')
622
+ throw new DurableWaitError('invalid_request', `${label} is required`);
623
+ assertText(value.ownerId, `${label}.ownerId`);
624
+ if (!['private', 'project', 'organization', 'public'].includes(value.visibility))
625
+ throw new DurableWaitError('invalid_request', `${label}.visibility is invalid`);
626
+ if (!Array.isArray(value.readerIds) || !Array.isArray(value.writerIds))
627
+ throw new DurableWaitError('invalid_request', `${label}.readerIds and writerIds are required`);
628
+ const readerIds = value.readerIds.map((id) => boundedText(id, `${label}.readerIds`));
629
+ const writerIds = value.writerIds.map((id) => boundedText(id, `${label}.writerIds`));
630
+ if (new Set(readerIds).size !== readerIds.length || new Set(writerIds).size !== writerIds.length)
631
+ throw new DurableWaitError('invalid_request', `${label} ACL ids must be unique`);
632
+ return { ownerId: value.ownerId, visibility: value.visibility, readerIds, writerIds };
633
+ }
634
+ function normalizeProblemReference(value, label) {
635
+ if (value === undefined || value === null || typeof value !== 'object')
636
+ throw new DurableWaitError('invalid_request', `${label} is required`);
637
+ const snapshotId = boundedText(value.snapshot_id, `${label}.snapshot_id`);
638
+ if (!SNAPSHOT_ID_PATTERN.test(snapshotId))
639
+ throw new DurableWaitError('invalid_request', `${label}.snapshot_id must be a sha256 snapshot ID`);
640
+ assertText(value.problem_id, `${label}.problem_id`);
641
+ assertText(value.revision, `${label}.revision`);
642
+ if (!/^[1-9]\d*$/u.test(value.revision))
643
+ throw new DurableWaitError('invalid_request', `${label}.revision must be a positive decimal revision`);
644
+ return { snapshot_id: snapshotId, problem_id: value.problem_id, revision: value.revision };
645
+ }
646
+ function normalizeResponsible(value, label) {
647
+ if (value === undefined || value === null || typeof value !== 'object')
648
+ throw new DurableWaitError('invalid_request', `${label} is required`);
649
+ const principal = boundedText(value.principal, `${label}.principal`);
650
+ const scope = value.scope === undefined ? undefined : boundedText(value.scope, `${label}.scope`);
651
+ return { principal, ...(scope === undefined ? {} : { scope }) };
652
+ }
653
+ function normalizeRunReference(value) {
654
+ if (value === undefined || value === null || typeof value !== 'object')
655
+ throw new DurableWaitError('invalid_request', 'run_ref is required');
656
+ const runId = boundedText(value.run_id, 'run_ref.run_id');
657
+ const nodeId = value.node_id === undefined ? undefined : boundedText(value.node_id, 'run_ref.node_id');
658
+ return { run_id: runId, ...(nodeId === undefined ? {} : { node_id: nodeId }) };
659
+ }
660
+ function assertTriggerReady(record, input) {
661
+ if (input.trigger === 'event') {
662
+ const condition = record.condition.event;
663
+ if (condition === undefined || input.event_type !== condition.event_type || (condition.event_id !== undefined && input.event_id !== condition.event_id)) {
664
+ throw new DurableWaitError('condition_unmet', `Event does not satisfy Wait ${record.wait_id}`, record.wait_id);
665
+ }
666
+ return;
667
+ }
668
+ if (input.trigger === 'timer') {
669
+ const due = record.deadline?.due_at === undefined ? undefined : Date.parse(record.deadline.due_at);
670
+ const review = record.deadline?.next_review_at === undefined ? undefined : Date.parse(record.deadline.next_review_at);
671
+ if ((due === undefined || due > input.nowMs) && (review === undefined || review > input.nowMs)) {
672
+ throw new DurableWaitError('condition_unmet', `Wait ${record.wait_id} is not due`, record.wait_id);
673
+ }
674
+ }
675
+ }
676
+ function makeClaim(input, now, leaseMs) {
677
+ return {
678
+ claim_id: `claim-${randomUUID()}`,
679
+ request_id: input.request_id,
680
+ trigger: input.trigger,
681
+ claimed_by: input.principal,
682
+ claimed_at: now.toISOString(),
683
+ lease: makeLease(input.principal, now, leaseMs),
684
+ ...(input.event_id === undefined ? {} : { event_id: input.event_id })
685
+ };
686
+ }
687
+ function makeLease(holder, now, leaseMs) {
688
+ return {
689
+ lease_id: `lease-${randomUUID()}`,
690
+ holder,
691
+ token: randomUUID(),
692
+ expires_at: new Date(now.getTime() + leaseMs).toISOString()
693
+ };
694
+ }
695
+ function assertResumeBinding(record, input) {
696
+ const claim = record.claim;
697
+ if (claim === undefined
698
+ || claim.claim_id !== input.claim_id
699
+ || claim.claimed_by !== input.principal
700
+ || claim.request_id !== input.request_id
701
+ || claim.lease.holder !== input.principal
702
+ || claim.lease.lease_id !== input.lease_id
703
+ || claim.lease.token !== input.lease_token) {
704
+ throw new DurableWaitError('unauthorized', `Resume lease does not match Wait ${record.wait_id}`, record.wait_id);
705
+ }
706
+ }
707
+ function updateRecord(record, changes) {
708
+ return { ...record, ...changes };
709
+ }
710
+ function appendHistory(record, entry) {
711
+ return [...record.history, entry];
712
+ }
713
+ function findWait(ledger, waitId) {
714
+ const record = ledger.waits.find((wait) => wait.wait_id === waitId);
715
+ if (record === undefined)
716
+ throw new DurableWaitError('not_found', `Wait ${waitId} was not found`, waitId);
717
+ return record;
718
+ }
719
+ function replaceWait(ledger, updated) {
720
+ const index = ledger.waits.findIndex((wait) => wait.wait_id === updated.wait_id);
721
+ if (index < 0)
722
+ throw new DurableWaitError('not_found', `Wait ${updated.wait_id} was not found`, updated.wait_id);
723
+ ledger.waits[index] = updated;
724
+ }
725
+ function parseLedger(content) {
726
+ if (content === undefined || content.trim() === '')
727
+ return { version: DURABLE_WAIT_VERSION, waits: [] };
728
+ let value;
729
+ try {
730
+ value = JSON.parse(content);
731
+ }
732
+ catch {
733
+ throw new DurableWaitError('ledger_invalid', 'Durable wait ledger is not valid JSON');
734
+ }
735
+ if (!isRecord(value) || value.version !== DURABLE_WAIT_VERSION || !Array.isArray(value.waits)) {
736
+ throw new DurableWaitError('ledger_invalid', 'Durable wait ledger has an unsupported shape');
737
+ }
738
+ try {
739
+ const waits = value.waits.map((item) => validateStoredRecord(item));
740
+ const ids = new Set(waits.map((wait) => wait.wait_id));
741
+ if (ids.size !== waits.length)
742
+ throw new Error('duplicate wait_id');
743
+ return { version: DURABLE_WAIT_VERSION, waits };
744
+ }
745
+ catch (error) {
746
+ if (error instanceof DurableWaitError)
747
+ throw error;
748
+ throw new DurableWaitError('ledger_invalid', `Durable wait ledger is invalid: ${error instanceof Error ? error.message : String(error)}`);
749
+ }
750
+ }
751
+ function validateStoredRecord(value) {
752
+ if (!isRecord(value))
753
+ throw new Error('wait record must be an object');
754
+ const state = enumValue(value.state, WAIT_STATES, 'wait.state');
755
+ if (value.version !== DURABLE_WAIT_VERSION)
756
+ throw new Error('wait.version is unsupported');
757
+ assertText(value.wait_id, 'wait.wait_id');
758
+ const ownerScope = normalizeScope(value.owner_scope, 'wait.owner_scope');
759
+ const acl = normalizeAcl(value.read_policy, 'wait.read_policy');
760
+ const problem = normalizeProblemReference(value.problem_snapshot, 'wait.problem_snapshot');
761
+ const condition = normalizeCondition(value.condition);
762
+ const deadline = value.deadline === undefined ? undefined : normalizeStoredDeadline(value.deadline);
763
+ const responsible = normalizeResponsible(value.responsible, 'wait.responsible');
764
+ const resumeMethod = enumValue(value.resume_method, RESUME_METHODS, 'wait.resume_method');
765
+ const failurePolicy = enumValue(value.failure_policy, FAILURE_POLICIES, 'wait.failure_policy');
766
+ const runRef = normalizeRunReference(value.run_ref);
767
+ assertTimestamp(value.created_at, 'wait.created_at');
768
+ assertTimestamp(value.updated_at, 'wait.updated_at');
769
+ if (!Array.isArray(value.history))
770
+ throw new Error('wait.history must be an array');
771
+ const history = value.history.map((entry) => validateHistory(entry));
772
+ const claim = value.claim === undefined ? undefined : validateClaim(value.claim);
773
+ const resumeReceipt = value.resume_receipt === undefined ? undefined : validateResumeReceipt(value.resume_receipt);
774
+ const nextProblem = value.next_problem === undefined ? undefined : normalizeProblemReference(value.next_problem, 'wait.next_problem');
775
+ const reconciliation = value.reconciliation === undefined ? undefined : validateReconciliation(value.reconciliation);
776
+ return {
777
+ wait_id: value.wait_id,
778
+ version: DURABLE_WAIT_VERSION,
779
+ owner_scope: ownerScope,
780
+ read_policy: acl,
781
+ problem_snapshot: problem,
782
+ condition,
783
+ ...(deadline === undefined ? {} : { deadline }),
784
+ responsible,
785
+ resume_method: resumeMethod,
786
+ failure_policy: failurePolicy,
787
+ run_ref: runRef,
788
+ state,
789
+ created_at: value.created_at,
790
+ updated_at: value.updated_at,
791
+ ...(claim === undefined ? {} : { claim }),
792
+ ...(resumeReceipt === undefined ? {} : { resume_receipt: resumeReceipt }),
793
+ ...(nextProblem === undefined ? {} : { next_problem: nextProblem }),
794
+ ...(reconciliation === undefined ? {} : { reconciliation }),
795
+ history
796
+ };
797
+ }
798
+ function normalizeStoredDeadline(value) {
799
+ if (value === undefined || value === null || typeof value !== 'object')
800
+ throw new Error('wait.deadline must be an object');
801
+ const dueAt = value.due_at === undefined ? undefined : boundedText(value.due_at, 'wait.deadline.due_at');
802
+ if (dueAt !== undefined)
803
+ assertTimestamp(dueAt, 'wait.deadline.due_at');
804
+ if (value.review_interval_ms !== undefined)
805
+ assertLeaseMs(value.review_interval_ms, 'wait.deadline.review_interval_ms');
806
+ const nextReviewAt = value.next_review_at === undefined ? undefined : boundedText(value.next_review_at, 'wait.deadline.next_review_at');
807
+ if (nextReviewAt !== undefined)
808
+ assertTimestamp(nextReviewAt, 'wait.deadline.next_review_at');
809
+ if (dueAt === undefined && value.review_interval_ms === undefined)
810
+ throw new Error('wait.deadline has no trigger');
811
+ return { ...(dueAt === undefined ? {} : { due_at: dueAt }), ...(value.review_interval_ms === undefined ? {} : { review_interval_ms: value.review_interval_ms }), ...(nextReviewAt === undefined ? {} : { next_review_at: nextReviewAt }) };
812
+ }
813
+ function validateClaim(value) {
814
+ if (!isRecord(value))
815
+ throw new Error('wait.claim must be an object');
816
+ assertText(value.claim_id, 'wait.claim.claim_id');
817
+ assertText(value.request_id, 'wait.claim.request_id');
818
+ const trigger = enumValue(value.trigger, TRIGGERS, 'wait.claim.trigger');
819
+ assertText(value.claimed_by, 'wait.claim.claimed_by');
820
+ assertTimestamp(value.claimed_at, 'wait.claim.claimed_at');
821
+ if (value.event_id !== undefined)
822
+ assertText(value.event_id, 'wait.claim.event_id');
823
+ if (!isRecord(value.lease))
824
+ throw new Error('wait.claim.lease must be an object');
825
+ const lease = {
826
+ lease_id: boundedText(value.lease.lease_id, 'wait.claim.lease.lease_id'),
827
+ holder: boundedText(value.lease.holder, 'wait.claim.lease.holder'),
828
+ token: boundedText(value.lease.token, 'wait.claim.lease.token'),
829
+ expires_at: boundedText(value.lease.expires_at, 'wait.claim.lease.expires_at')
830
+ };
831
+ assertTimestamp(lease.expires_at, 'wait.claim.lease.expires_at');
832
+ if (lease.holder !== value.claimed_by)
833
+ throw new Error('wait.claim.lease.holder must match claimed_by');
834
+ return { claim_id: value.claim_id, request_id: value.request_id, trigger, claimed_by: value.claimed_by, claimed_at: value.claimed_at, lease, ...(value.event_id === undefined ? {} : { event_id: value.event_id }) };
835
+ }
836
+ function validateResumeReceipt(value) {
837
+ if (!isRecord(value))
838
+ throw new Error('wait.resume_receipt must be an object');
839
+ assertText(value.resume_id, 'wait.resume_receipt.resume_id');
840
+ assertText(value.wait_id, 'wait.resume_receipt.wait_id');
841
+ assertText(value.claim_id, 'wait.resume_receipt.claim_id');
842
+ const runRef = normalizeRunReference(value.run_ref);
843
+ assertTimestamp(value.recorded_at, 'wait.resume_receipt.recorded_at');
844
+ return { resume_id: value.resume_id, wait_id: value.wait_id, claim_id: value.claim_id, run_ref: runRef, recorded_at: value.recorded_at };
845
+ }
846
+ function validateHistory(value) {
847
+ if (!isRecord(value))
848
+ throw new Error('wait.history entry must be an object');
849
+ const action = enumValue(value.action, ['create', 'claim', 'handoff', 'premise_changed', 'effect_unknown', 'resume'], 'wait.history.action');
850
+ assertTimestamp(value.occurred_at, 'wait.history.occurred_at');
851
+ assertText(value.principal, 'wait.history.principal');
852
+ const toState = enumValue(value.to_state, WAIT_STATES, 'wait.history.to_state');
853
+ const fromState = value.from_state === undefined ? undefined : enumValue(value.from_state, WAIT_STATES, 'wait.history.from_state');
854
+ const detail = value.detail === undefined ? undefined : boundedText(value.detail, 'wait.history.detail');
855
+ const responsible = value.responsible === undefined ? undefined : normalizeResponsible(value.responsible, 'wait.history.responsible');
856
+ return { action, occurred_at: value.occurred_at, principal: value.principal, ...(fromState === undefined ? {} : { from_state: fromState }), to_state: toState, ...(detail === undefined ? {} : { detail }), ...(responsible === undefined ? {} : { responsible }) };
857
+ }
858
+ function validateReconciliation(value) {
859
+ if (!isRecord(value))
860
+ throw new Error('wait.reconciliation must be an object');
861
+ const reason = boundedText(value.reason, 'wait.reconciliation.reason');
862
+ const recordedAt = boundedText(value.recorded_at, 'wait.reconciliation.recorded_at');
863
+ assertTimestamp(recordedAt, 'wait.reconciliation.recorded_at');
864
+ const responsible = normalizeResponsible(value.responsible, 'wait.reconciliation.responsible');
865
+ return { reason, recorded_at: recordedAt, responsible };
866
+ }
867
+ function serializeLedger(ledger) {
868
+ const sorted = {
869
+ version: DURABLE_WAIT_VERSION,
870
+ waits: [...ledger.waits].sort((left, right) => left.wait_id.localeCompare(right.wait_id))
871
+ };
872
+ return `${JSON.stringify(sorted, null, 2)}\n`;
873
+ }
874
+ function sameProblem(left, right) {
875
+ return left.snapshot_id === right.snapshot_id && left.problem_id === right.problem_id && left.revision === right.revision;
876
+ }
877
+ function requireMutationRecord(preflight, waitId) {
878
+ if (preflight.record === undefined) {
879
+ throw new DurableWaitError('not_found', `Wait ${waitId} was not found`, waitId);
880
+ }
881
+ return preflight.record;
882
+ }
883
+ function fingerprintPersonalOs(value) {
884
+ return fingerprintValue({
885
+ graph: value.graph,
886
+ personalKg: value.personalKg,
887
+ relationships: value.relationships,
888
+ decisions: value.decisions,
889
+ sourceCount: value.sourceCount
890
+ });
891
+ }
892
+ function fingerprintRecord(value) {
893
+ return fingerprintValue(value);
894
+ }
895
+ function fingerprintText(value) {
896
+ return fingerprintValue(value === undefined ? { missing: true } : { content: value });
897
+ }
898
+ function fingerprintValue(value) {
899
+ return createHash('sha256').update(JSON.stringify(value), 'utf8').digest('hex');
900
+ }
901
+ function enumValue(value, values, label) {
902
+ if (typeof value !== 'string' || !values.includes(value))
903
+ throw new DurableWaitError('invalid_request', `${label} is invalid`);
904
+ return value;
905
+ }
906
+ function boundedText(value, label) {
907
+ assertText(value, label);
908
+ return value;
909
+ }
910
+ function assertText(value, label) {
911
+ if (typeof value !== 'string' || value.length === 0 || value.length > 512 || /[\u0000-\u001f\u007f]/u.test(value)) {
912
+ throw new DurableWaitError('invalid_request', `${label} must be a bounded text value`);
913
+ }
914
+ }
915
+ function assertTimestamp(value, label) {
916
+ assertText(value, label);
917
+ if (!Number.isFinite(Date.parse(value)))
918
+ throw new DurableWaitError('invalid_request', `${label} must be an ISO timestamp`);
919
+ }
920
+ function assertLeaseMs(value, label) {
921
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0 || value > 31_536_000_000) {
922
+ throw new DurableWaitError('invalid_request', `${label} must be a positive millisecond interval`);
923
+ }
924
+ }
925
+ function assertSidecarPath(value) {
926
+ if (!value || value.startsWith('/') || value.includes('\\') || value.split('/').some((part) => !part || part === '.' || part === '..')) {
927
+ throw new DurableWaitError('invalid_request', 'sidecarPath must remain relative to the SSOT root');
928
+ }
929
+ }
930
+ function isRecord(value) {
931
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
932
+ }
933
+ function freezeClone(value) {
934
+ const copy = structuredClone(value);
935
+ return deepFreeze(copy);
936
+ }
937
+ function deepFreeze(value) {
938
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
939
+ Object.freeze(value);
940
+ for (const child of Object.values(value))
941
+ deepFreeze(child);
942
+ }
943
+ return value;
944
+ }