weir-harness 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/cordis.patch.yml +7 -18
  2. package/lib/client.file-lease-badge.js +307 -0
  3. package/lib/client.js +20 -30
  4. package/lib/client.settings-page.js +10 -34
  5. package/package.json +2 -2
  6. package/src/capabilities/store/checked-contract.d.ts +2 -2
  7. package/src/capabilities/store/fs-adapter.js +2 -3
  8. package/src/context-guard/index.js +2 -2
  9. package/src/delegate/supervision-mount.js +5 -5
  10. package/src/delegate/tool.js +2 -10
  11. package/src/file-lease/client.js +1066 -0
  12. package/src/file-lease/index.js +30 -0
  13. package/src/file-lease/lineage.js +207 -0
  14. package/src/file-lease/node-contracts.d.ts +42 -0
  15. package/src/file-lease/plugin.js +789 -0
  16. package/src/file-lease/singleton.js +155 -0
  17. package/src/file-lease/spawn.js +62 -0
  18. package/src/file-lease/tools.js +459 -0
  19. package/src/file-lease/view.js +144 -0
  20. package/src/hashline-edit/index.js +76 -42
  21. package/src/hashline-edit/write-tool.js +90 -0
  22. package/src/intent-gate/matcher.js +27 -0
  23. package/src/lease-broker/entry.js +98 -0
  24. package/src/lease-broker/node-contracts.d.ts +63 -0
  25. package/src/lease-broker/protocol.js +236 -0
  26. package/src/lease-broker/server.js +399 -0
  27. package/src/lease-broker/state.js +425 -0
  28. package/src/lsp/admin.js +24 -13
  29. package/src/lsp/index.js +5 -2
  30. package/src/lsp/registry.js +83 -17
  31. package/src/lsp/tools.js +137 -27
  32. package/src/notify/commands.js +15 -2
  33. package/src/notify/index.js +48 -10
  34. package/src/notify/web-channel.js +26 -3
  35. package/src/process/index.js +11 -0
  36. package/src/settings/index.js +2 -44
  37. package/src/settings/sections.js +29 -28
  38. package/src/shared/audit.js +6 -1
  39. package/src/todo-driver/index.js +1 -6
  40. package/src/worktree/cards.js +6 -21
  41. package/src/worktree/lane-core.js +1 -19
  42. package/src/worktree/lane-workflows.js +11 -32
  43. package/lib/client.edit-lock-maintenance.js +0 -592
  44. package/lib/client.edit-lock-panel.js +0 -405
  45. package/src/edit-lock/adapter.js +0 -57
  46. package/src/edit-lock/admin-ledger.js +0 -149
  47. package/src/edit-lock/admin-recovery.js +0 -151
  48. package/src/edit-lock/admission.js +0 -82
  49. package/src/edit-lock/binding-attestation.js +0 -29
  50. package/src/edit-lock/binding-guidance.js +0 -72
  51. package/src/edit-lock/binding-machine.js +0 -312
  52. package/src/edit-lock/call-context.js +0 -65
  53. package/src/edit-lock/derived-facts.js +0 -67
  54. package/src/edit-lock/domain.js +0 -165
  55. package/src/edit-lock/domains.js +0 -170
  56. package/src/edit-lock/host.js +0 -149
  57. package/src/edit-lock/incarnations.js +0 -150
  58. package/src/edit-lock/index.js +0 -1303
  59. package/src/edit-lock/inspect.js +0 -219
  60. package/src/edit-lock/lifecycle.js +0 -579
  61. package/src/edit-lock/limits.js +0 -61
  62. package/src/edit-lock/maintenance.js +0 -310
  63. package/src/edit-lock/manager.js +0 -875
  64. package/src/edit-lock/negotiation.js +0 -132
  65. package/src/edit-lock/operation-history.js +0 -214
  66. package/src/edit-lock/peer-client.js +0 -86
  67. package/src/edit-lock/peer-transport.js +0 -76
  68. package/src/edit-lock/peer.js +0 -87
  69. package/src/edit-lock/publisher.js +0 -191
  70. package/src/edit-lock/read-authority.js +0 -101
  71. package/src/edit-lock/recovery.js +0 -139
  72. package/src/edit-lock/remote.js +0 -362
  73. package/src/edit-lock/request-binding.js +0 -42
  74. package/src/edit-lock/request-data.js +0 -40
  75. package/src/edit-lock/reservation.js +0 -301
  76. package/src/edit-lock/reserved-runtime.js +0 -45
  77. package/src/edit-lock/resource-identity.js +0 -130
  78. package/src/edit-lock/runtime.js +0 -145
  79. package/src/edit-lock/settle.js +0 -144
  80. package/src/edit-lock/snapshot.js +0 -175
  81. package/src/edit-lock/stale-sweep.js +0 -120
  82. package/src/edit-lock/state.js +0 -462
  83. package/src/edit-lock/store.js +0 -327
  84. package/src/edit-lock/tool-scope.js +0 -29
  85. package/src/edit-lock/view.js +0 -170
  86. package/src/edit-lock/write-scopes.js +0 -48
  87. package/src/edit-lock/write-tool.js +0 -45
  88. package/src/shared/delegate-edit-lifecycle.js +0 -36
  89. package/src/worktree/authority.js +0 -56
@@ -1,875 +0,0 @@
1
- import { createHash, randomUUID } from 'node:crypto'
2
- import { createEditLockState } from './state.js'
3
- import { bindRequest } from './request-binding.js'
4
- import { lookupOperation } from './operation-history.js'
5
- import { canonicalRequestData } from './request-data.js'
6
- import { admitMutation as admitHistory, publicationCandidate } from './admission.js'
7
- import { administrativeState, AUTOMATIC_RECOVERY_ACTOR, digest, LATE_WRITER_RISK, ONLINE_RECOVERY_ACTOR } from './admin-ledger.js'
8
- import { dispositionFor, revokedOwner } from './derived-facts.js'
9
- import { canonical } from './snapshot.js'
10
- import { incarnationProcess, withIncarnation } from './incarnations.js'
11
-
12
- /**
13
- * Unmounted trusted manager core. Caller exclusively owns the store lifecycle;
14
- * this factory does not elect a singleton or expose any file publication path.
15
- * This fresh-store factory is separate from the trusted recovery entrypoint.
16
- * The optional process identity (shared Liveness adapter, design D2) is
17
- * registered in the durable image with the first committed write. The
18
- * optional domain root and backup writer enable the online administrative
19
- * recovery transaction (design D4); without them that transaction refuses.
20
- * @param {{store: Awaited<ReturnType<typeof import('./store.js').openEditLockStore>>, managerIncarnation: string, processIdentity?: import('./incarnations.js').ProcessIdentity|null, root?: string|null, writeBackup?: ((file: string, bytes: Buffer) => Promise<void>)|null}} options
21
- */
22
- export function createEditLockManager({ store, managerIncarnation, processIdentity = null, root = null, writeBackup = null }) {
23
- const confirmed = store.snapshot()
24
- if (confirmed.revision !== 0 || confirmed.state.managerIncarnation !== null) {
25
- throw new Error('fresh store required; use trusted recovery entrypoint')
26
- }
27
- return managerCore(store, createEditLockState(managerIncarnation), { processIdentity, root, writeBackup })
28
- }
29
-
30
- /**
31
- * Design D3 eligibility for one recovery open: an owner is settled only when
32
- * it is not already revoked, every one of its unresolved (unknown) operations
33
- * originates from an incarnation whose recorded process identity differs from
34
- * the recovering process, AND the shared Liveness adapter proves that process
35
- * dead. Null identities and same-process remount incarnations never qualify.
36
- * The records reuse the v5 adminRecoveries shape; the actor marks them
37
- * automatic. @param {any} previous @param {any} base
38
- * @param {{liveness: import('../capabilities/store/liveness.js').Liveness, root: string,
39
- * processIdentity: import('./incarnations.js').ProcessIdentity, now: () => number}} options
40
- */
41
- async function automaticSettlementRecords(previous, base, { liveness, root, processIdentity, now }) {
42
- /** @type {Map<string, any[]>} */
43
- const unknownByOwner = new Map()
44
- for (const operation of base.operations) {
45
- if (operation.phase !== 'unknown') continue
46
- // An operation an earlier disposition already covers stays disposed; a
47
- // re-revocation after repair binds only genuinely new unknowns.
48
- if (dispositionFor(base, operation)) continue
49
- const ops = unknownByOwner.get(operation.sessionId) ?? []
50
- ops.push(operation)
51
- unknownByOwner.set(operation.sessionId, ops)
52
- }
53
- /** @type {any[]} */
54
- const records = []
55
- for (const [owner, ops] of unknownByOwner) {
56
- if (revokedOwner(base, owner)) continue
57
- const session = base.sessions.find(s => s.sessionId === owner)
58
- if (!session?.interrupted) continue
59
- let eligible = true
60
- for (const operation of ops) {
61
- const process = incarnationProcess(base, operation.origin.managerIncarnation)
62
- if (!process) { eligible = false; break }
63
- if (process.pid === processIdentity.pid && process.bootNonce === processIdentity.bootNonce) { eligible = false; break }
64
- const state = await liveness.state({ pid: process.pid, host: process.host, startIdentity: { osStart: process.osStart, bootNonce: process.bootNonce } })
65
- if (state !== 'dead') { eligible = false; break }
66
- }
67
- if (!eligible) continue
68
- const recoveryId = `automatic-${randomUUID()}`
69
- const operationIds = ops.map(operation => operation.operationId).sort()
70
- records.push({
71
- recoveryId, root, owner,
72
- expectedRevision: previous.revision, committedRevision: previous.revision + 1,
73
- at: now(), actor: AUTOMATIC_RECOVERY_ACTOR,
74
- reason: 'Every unresolved publication of this owner originated in a manager incarnation whose process is proven dead (pid and start identity), so that lifetime has no live writer; settled automatically on recovery open.',
75
- risk: LATE_WRITER_RISK,
76
- confirmation: digest({ root, owner, expectedRevision: previous.revision, operationIds, risk: LATE_WRITER_RISK }),
77
- // Filled from the recovered pre-image bytes before the commit.
78
- backup: { file: `admin-backup-${digest([root, recoveryId])}.json`, sha256: '', bytes: 0 },
79
- operations: ops.map(operation => ({ operationId: operation.operationId, sha256: digest(operation) })),
80
- releasedLocks: base.locks.filter((/** @param {any} lock */ lock) => lock.owner === owner),
81
- revokedEpoch: session.executionEpoch + 1,
82
- })
83
- }
84
- return records
85
- }
86
-
87
- /** Trusted lifecycle only: caller must establish exclusive ownership and old
88
- * publisher quiescence before opening the store. No IPC or automatic election.
89
- * When the shared Liveness adapter, the domain root and a backup writer are
90
- * supplied, unknown-phase publications of provably dead incarnation processes
91
- * are administratively settled in the SAME durable commit as the recovery
92
- * (design D3): one snapshot commit, one ledger record per settled owner, the
93
- * unknown outcomes and history preserved.
94
- * @param {{store: Awaited<ReturnType<typeof import('./store.js').openEditLockStore>>, managerIncarnation: string,
95
- * processIdentity?: import('./incarnations.js').ProcessIdentity|null,
96
- * liveness?: import('../capabilities/store/liveness.js').Liveness|null, root?: string|null,
97
- * writeBackup?: ((file: string, bytes: Buffer) => Promise<void>)|null, now?: () => number,
98
- * onAutomaticRecovery?: (info: {records: any[], revision: number}) => void}} options */
99
- export async function recoverEditLockManager({ store, managerIncarnation, processIdentity = null, liveness = null, root = null, writeBackup = null, now = Date.now, onAutomaticRecovery = undefined }) {
100
- const previous = store.snapshot()
101
- if (!previous.state.managerIncarnation) throw new Error('historical store required')
102
- // Phase 1: the ordinary recovery mapping (staging kernel, never installed).
103
- const staging = createEditLockState(managerIncarnation)
104
- const staged = staging.authority.beginRecovery({ ...previous.state, managerIncarnation: previous.state.managerIncarnation })
105
- const mapped = staging.authority.checkpoint(staged)
106
- staging.authority.discard(staged)
107
- const operations = previous.state.operations.map(operation => {
108
- if (operation.phase === 'prepared') return { ...operation, phase: 'not-published',
109
- outcome: { kind: 'not-published', reason: 'rejected-before-dispatch' } }
110
- if (operation.phase === 'publishing') return { ...operation, phase: 'unknown', outcome: { kind: 'unknown' } }
111
- return operation
112
- })
113
- let base = withIncarnation({ ...previous.state, ...mapped, operations }, managerIncarnation, processIdentity, previous.state.managerIncarnation)
114
- if (base.version === 6 && !(base.incarnations ?? []).some(entry => entry.incarnation === managerIncarnation)) {
115
- // An identity-less recovery of a v6 image still registers the new
116
- // incarnation — with a null identity, so it can never be auto-settled —
117
- // because the registry must cover every incarnation the image references.
118
- base = { ...base, incarnations: [...base.incarnations, { incarnation: managerIncarnation, process: null }] }
119
- }
120
- const settle = liveness !== null && root !== null && writeBackup !== null && processIdentity !== null
121
- const records = settle ? await automaticSettlementRecords(previous, base, { liveness, root, processIdentity, now }) : []
122
- if (records.length > 0) {
123
- // Every record backs up the same recovered pre-image; the backup fields
124
- // are part of the immutable ledger row, so they are filled BEFORE the
125
- // administrative fold builds the committed state.
126
- const bytes = store.recoveredBytes()
127
- if (!bytes) throw new Error('automatic settlement requires the recovered pre-image bytes')
128
- const sha256 = createHash('sha256').update(bytes).digest('hex')
129
- for (const record of records) record.backup = { file: record.backup.file, sha256, bytes: bytes.length }
130
- for (const record of records) await writeBackup(record.backup.file, bytes)
131
- }
132
- const nextState = records.reduce((state, record) => administrativeState(state, record), base)
133
- // Phase 2: the installed kernel hydrates from the FINAL kernel fields
134
- // (post-settlement), so the in-memory authority matches the durable image.
135
- const kernel = createEditLockState(managerIncarnation)
136
- const draft = kernel.authority.beginRecovery({ ...nextState, managerIncarnation: previous.state.managerIncarnation })
137
- if (records.length === 0) {
138
- await store.record({ expectedRevision: previous.revision, nextState })
139
- } else {
140
- await store.recordAdministrativeRecovery({ expectedRevision: previous.revision, nextState, base })
141
- }
142
- kernel.authority.install(draft)
143
- if (records.length > 0) onAutomaticRecovery?.({ records: structuredClone(records), revision: previous.revision + 1 })
144
- return managerCore(store, kernel, { processIdentity, root, writeBackup })
145
- }
146
-
147
- /** @param {Awaited<ReturnType<typeof import('./store.js').openEditLockStore>>} store
148
- * @param {ReturnType<typeof createEditLockState>} kernel
149
- * @param {{processIdentity?: import('./incarnations.js').ProcessIdentity|null, root?: string|null, writeBackup?: ((file: string, bytes: Buffer) => Promise<void>)|null}} [options] */
150
- function managerCore(store, kernel, { processIdentity = null, root = null, writeBackup = null } = {}) {
151
- const { operations, authority } = kernel
152
- const managerIncarnation = operations.status().managerIncarnation
153
- // Every durable write of an identity-aware manager carries the incarnation
154
- // registry (design D2); a no-op once this incarnation is registered.
155
- /** @param {any} state @returns {any} */
156
- const withIdentity = processIdentity ? state => withIncarnation(state, managerIncarnation, processIdentity) : state => state
157
- let confirmed = store.snapshot()
158
- // Only validated administrative dispositions lift a historical fence. The
159
- // immutable history still participates in operation-ID lookup before admission.
160
- function admitMutation(history, candidate, mode = 'normal') {
161
- return admitHistory(history.filter(op => !dispositionFor(confirmed.state, op)), candidate, mode)
162
- }
163
- function checkNotRevoked(sessionId) {
164
- if (revokedOwner(confirmed.state, sessionId)) throw new Error('owner revoked by administrative recovery')
165
- }
166
- let tail = Promise.resolve()
167
- /** @type {unknown} */
168
- let poison
169
- let closed = false
170
- // Denial only: never a second ownership ledger or a grant source.
171
- const cancelled = new Map()
172
- function healthy() {
173
- if (closed) throw new Error('manager closed')
174
- if (poison) throw new Error('manager poisoned', { cause: poison })
175
- }
176
- /** @typedef {{validate: () => void, publish: () => Promise<{version: string}>, identify: () => string}} Publisher
177
- * @type {WeakMap<object, {execution: import('./state.js').Execution, request: any, hooks: Publisher}>} */
178
- const submissions = new WeakMap()
179
- /** @param {import('./state.js').Execution} execution */
180
- function checkExecution(execution) {
181
- healthy()
182
- checkNotRevoked(execution.sessionId)
183
- const session = operations.status().sessions.find(s => s.sessionId === execution.sessionId)
184
- if (!session || execution.managerIncarnation !== managerIncarnation ||
185
- execution.executionEpoch !== session.executionEpoch || session.interrupted || cancelled.has(execution.sessionId)) {
186
- throw new Error('execution is not active')
187
- }
188
- }
189
- /** @param {string} sessionId @returns {import('./admission.js').Candidate} */
190
- function sessionResources(sessionId) {
191
- return { kind: 'resources', resourceIds: operations.status().locks.filter(lock => lock.owner === sessionId).map(lock => lock.resourceId) }
192
- }
193
- /** @param {(draft: ReturnType<typeof authority.begin>) => any} transition
194
- * @param {import('./admission.js').Candidate | (() => import('./admission.js').Candidate)} candidate
195
- * @param {import('./admission.js').Mode} [mode]
196
- * @param {(state: any) => object} [patch] Extra durable fields (recovery budgets only). */
197
- function transact(transition, candidate, mode = 'normal', patch = undefined) {
198
- const pending = tail.then(async () => {
199
- healthy()
200
- admitMutation(confirmed.state.operations, typeof candidate === 'function' ? candidate() : candidate, mode)
201
- const draft = authority.begin()
202
- let result
203
- try { result = transition(draft) } catch (error) {
204
- authority.discard(draft)
205
- throw error
206
- }
207
- try {
208
- const nextState = { ...confirmed.state, ...authority.checkpoint(draft) }
209
- if (patch) Object.assign(nextState, patch(nextState))
210
- const saved = await store.record({ expectedRevision: confirmed.revision, nextState: withIdentity(nextState) })
211
- authority.install(draft)
212
- confirmed = saved
213
- return result
214
- } catch (error) {
215
- poison = error
216
- throw new Error('manager persistence or installation failed; poisoned', { cause: error })
217
- }
218
- })
219
- tail = pending.then(() => {}, () => {})
220
- return pending
221
- }
222
- return Object.freeze({
223
- /** Historical target lookup for a trusted authenticated session; never acquires.
224
- * @param {string} sessionId @param {string} operationId */
225
- history(sessionId, operationId) {
226
- healthy()
227
- if (!operations.status().sessions.some(s => s.sessionId === sessionId)) throw new Error('unknown session')
228
- const operation = confirmed.state.operations.find(o => o.sessionId === sessionId && o.operationId === operationId)
229
- return operation ? structuredClone(operation) : null
230
- },
231
- /** Trusted adapter ingress: binding and observed target are immutable data.
232
- * validate must synchronously recheck policy/topology/signal at queue position.
233
- * Existing history is returned before current authority checks, never replayed.
234
- * @param {import('./state.js').Execution} execution
235
- * @param {Parameters<typeof bindRequest>[0] & {operationId: string}} request
236
- * @param {Publisher} publisher */
237
- prepare(execution, request, publisher) {
238
- const hooks = Object.freeze({ validate: publisher.validate, publish: publisher.publish, identify: publisher.identify })
239
- const captured = { ...execution }
240
- const data = JSON.parse(canonicalRequestData(request))
241
- const binding = bindRequest(data)
242
- if (typeof data.operationId !== 'string' || !data.operationId.trim()) throw new Error('invalid operation id')
243
- const pending = tail.then(async () => {
244
- healthy()
245
- if (!operations.status().sessions.some(s => s.sessionId === captured.sessionId)) throw new Error('unknown session')
246
- const previous = lookupOperation(confirmed.state, { sessionId: captured.sessionId, operationId: data.operationId, binding })
247
- if (previous) return { kind: 'history', operation: previous }
248
- checkExecution(captured)
249
- admitMutation(confirmed.state.operations, publicationCandidate(binding.target))
250
- if (binding.target.kind === 'update') operations.checkWrite({ ...captured,
251
- resourceId: binding.target.resourceId, generation: binding.target.generation })
252
- hooks.validate()
253
- /** @type {import('./operation-history.js').Operation} */
254
- const operation = { sessionId: captured.sessionId, operationId: data.operationId,
255
- origin: { executionEpoch: captured.executionEpoch, managerIncarnation: captured.managerIncarnation },
256
- binding, phase: 'prepared', fence: null, outcome: null, closeouts: [] }
257
- try {
258
- confirmed = await store.record({ expectedRevision: confirmed.revision,
259
- nextState: withIdentity({ ...confirmed.state, operations: [...confirmed.state.operations, operation] }) })
260
- } catch (error) { poison = error; throw error }
261
- // A cancellation during durability must not deliver a ready submission.
262
- if (cancelled.has(captured.sessionId)) {
263
- const rejected = { ...operation, phase: /** @type {const} */ ('not-published'),
264
- outcome: { kind: /** @type {const} */ ('not-published'), reason: /** @type {const} */ ('cancelled-before-dispatch') } }
265
- try {
266
- confirmed = await store.record({ expectedRevision: confirmed.revision,
267
- nextState: withIdentity({ ...confirmed.state, operations: confirmed.state.operations.map(o => o === confirmed.state.operations.at(-1) ? rejected : o) }) })
268
- } catch (error) { poison = error; throw error }
269
- throw new Error('session cancelled during preparation')
270
- }
271
- const submission = Object.freeze({})
272
- submissions.set(submission, { execution: captured, request: data, hooks })
273
- return { kind: 'ready', submission }
274
- })
275
- tail = pending.then(() => {}, () => {})
276
- return pending
277
- },
278
- /** Trusted publisher hooks must close over the original fs and frozen call.
279
- * No caller may dispatch outside this serialized task.
280
- * @param {object} submission */
281
- commit(submission) {
282
- const job = submissions.get(submission)
283
- if (!job) throw new Error('unknown submission')
284
- const { execution, request, hooks } = job
285
- const binding = bindRequest(request)
286
- const key = { sessionId: execution.sessionId, operationId: request.operationId, binding }
287
- const pending = tail.then(async () => {
288
- healthy()
289
- const original = lookupOperation(confirmed.state, key)
290
- if (!original) throw new Error('missing prepared history')
291
- if (original.phase !== 'prepared') return original
292
- /** @param {import('./operation-history.js').Operation} operation
293
- * @param {ReturnType<typeof authority.begin>} [draft] */
294
- const save = async (operation, draft) => {
295
- try {
296
- const saved = await store.record({ expectedRevision: confirmed.revision, nextState: withIdentity({
297
- ...confirmed.state, ...(draft ? authority.checkpoint(draft) : {}),
298
- operations: confirmed.state.operations.map(o => o.sessionId === key.sessionId && o.operationId === key.operationId ? operation : o),
299
- }) })
300
- if (draft) authority.install(draft)
301
- confirmed = saved
302
- } catch (error) { poison = error; throw error }
303
- }
304
- const validate = () => {
305
- checkExecution(execution)
306
- if (binding.target.kind === 'update') operations.checkWrite({ ...execution,
307
- resourceId: binding.target.resourceId, generation: binding.target.generation })
308
- hooks.validate()
309
- }
310
- try {
311
- admitMutation(confirmed.state.operations, publicationCandidate(binding.target))
312
- validate()
313
- } catch (error) {
314
- await save({ ...original, phase: 'not-published', outcome: { kind: 'not-published',
315
- reason: cancelled.has(execution.sessionId) ? 'cancelled-before-dispatch' : 'rejected-before-dispatch' } })
316
- throw error
317
- }
318
- /** @type {import('./operation-history.js').Operation} */
319
- const publishing = { ...original, phase: 'publishing', fence: binding.target.kind === 'update'
320
- ? { kind: 'resource', resourceId: binding.target.resourceId }
321
- : { kind: 'subtree', ancestor: binding.target.ancestor, basis: 'observed-ancestor' } }
322
- let attempt
323
- try {
324
- attempt = await store.beginPublication({ expectedRevision: confirmed.revision,
325
- nextState: withIdentity({ ...confirmed.state, operations: confirmed.state.operations.map(o =>
326
- o.sessionId === key.sessionId && o.operationId === key.operationId ? publishing : o) }) }, key, hooks.publish)
327
- confirmed = store.snapshot()
328
- } catch (error) { poison = error; throw error }
329
- try { validate() } catch (error) {
330
- try {
331
- confirmed = await attempt.finishWithoutDispatch(cancelled.has(execution.sessionId)
332
- ? 'cancelled-before-dispatch' : 'rejected-before-dispatch')
333
- } catch (failure) { poison = failure; throw failure }
334
- throw error
335
- }
336
- let draft
337
- try {
338
- const result = await attempt.invoke()
339
- const resourceId = hooks.identify()
340
- draft = authority.begin()
341
- let ownership
342
- if (binding.target.kind === 'create') ownership = draft.authority.settleCreated(execution, resourceId)
343
- else {
344
- if (resourceId !== binding.target.resourceId) throw new Error('published identity changed')
345
- ownership = { ...execution, resourceId, generation: binding.target.generation }
346
- draft.operations.checkWrite(ownership)
347
- }
348
- if (cancelled.has(execution.sessionId)) draft.authority.cancel(execution)
349
- const kind = binding.target.kind === 'create' ? 'created' : 'updated'
350
- const completed = { ...publishing, phase: /** @type {'created'|'updated'} */ (kind),
351
- outcome: { kind: /** @type {'created'|'updated'} */ (kind), resourceId, generation: ownership.generation, version: result.version } }
352
- await save(completed, draft)
353
- draft = undefined
354
- // Cancellation can arrive while the successful active image is syncing.
355
- const session = operations.status().sessions.find(s => s.sessionId === execution.sessionId)
356
- if (cancelled.has(execution.sessionId) && session && !session.interrupted) {
357
- const corrective = authority.begin()
358
- corrective.authority.cancel(execution)
359
- await save(completed, corrective)
360
- }
361
- return completed
362
- } catch (error) {
363
- if (draft) { try { authority.discard(draft) } catch {} }
364
- if (!poison) await save({ ...publishing, phase: 'unknown', outcome: { kind: 'unknown' } })
365
- throw error
366
- }
367
- })
368
- tail = pending.then(() => {}, () => {})
369
- return pending
370
- },
371
- /** Only authenticated human intent may reach this trusted ingress.
372
- * @param {import('./state.js').Execution} execution @param {string} requestId */
373
- issueExecutionReceipt(execution, requestId) {
374
- const captured = { ...execution }
375
- return transact(draft => {
376
- if (cancelled.has(captured.sessionId)) throw new Error('session cancelled')
377
- // A revoked owner is NOT refused here: receipt issuance is half of the
378
- // trusted repair path; the epoch binding still fences stale authority.
379
- return draft.authority.issueExecutionReceipt(captured, requestId)
380
- }, { kind: 'none' }).then(async receipt => {
381
- const cancellation = cancelled.get(captured.sessionId)
382
- if (cancellation) {
383
- await cancellation
384
- throw new Error('session cancelled during receipt issuance')
385
- }
386
- healthy()
387
- const current = operations.status().sessions.find(s => s.sessionId === captured.sessionId)
388
- if (current?.executionEpoch !== captured.executionEpoch) throw new Error('stale receipt execution')
389
- return receipt
390
- })
391
- },
392
- /** @param {import('./state.js').Execution} execution @param {string} requestId @param {object} receipt */
393
- resume(execution, requestId, receipt) {
394
- const captured = { ...execution }
395
- return transact(draft => {
396
- if (cancelled.has(captured.sessionId)) throw new Error('session cancelled')
397
- // A revoked owner is NOT refused here: the resume commits the epoch
398
- // advance that atomically ends the revocation (no ledger change).
399
- // Admission runs in the resume mode: the epoch advance publishes nothing,
400
- // so unresolved resource-level fences never block the trusted Continue;
401
- // the fenced resources stay barred to every other ingress.
402
- return draft.operations.resume(captured, requestId, receipt)
403
- }, () => sessionResources(captured.sessionId), 'resume').then(async resumed => {
404
- const cancellation = cancelled.get(captured.sessionId)
405
- if (cancellation) {
406
- await cancellation
407
- throw new Error('session cancelled during resume')
408
- }
409
- healthy()
410
- const current = operations.status().sessions.find(s => s.sessionId === captured.sessionId)
411
- if (current?.interrupted || current?.executionEpoch !== resumed.executionEpoch) throw new Error('stale resumed execution')
412
- return resumed
413
- })
414
- },
415
- /** @param {string} sessionId */
416
- openSession(sessionId) {
417
- return transact(draft => {
418
- if (cancelled.has(sessionId)) throw new Error('session cancelled')
419
- checkNotRevoked(sessionId)
420
- return draft.authority.openSession(sessionId)
421
- }, { kind: 'none' }).then(async execution => {
422
- const cancellation = cancelled.get(sessionId)
423
- if (cancellation) {
424
- await cancellation
425
- throw new Error('session cancelled during registration')
426
- }
427
- healthy()
428
- return execution
429
- })
430
- },
431
- /** Trusted lifecycle ingress, never a model-facing authority argument.
432
- * Cancels the session at its queue position, including pending registration.
433
- * @param {string} sessionId */
434
- cancelSession(sessionId) {
435
- healthy()
436
- if (typeof sessionId !== 'string' || !sessionId.trim()) throw new Error('invalid session')
437
- const existing = cancelled.get(sessionId)
438
- if (existing) return existing
439
- const pending = transact(draft => {
440
- const session = operations.status().sessions.find(s => s.sessionId === sessionId)
441
- if (!session) throw new Error('unknown session')
442
- if (session.interrupted) return { sessionId, executionEpoch: session.executionEpoch, managerIncarnation }
443
- return draft.authority.cancel({ sessionId, executionEpoch: session.executionEpoch, managerIncarnation })
444
- }, { kind: 'none' }, 'revocation')
445
- cancelled.set(sessionId, pending)
446
- void pending.then(() => cancelled.delete(sessionId), () => {})
447
- return pending
448
- },
449
- /** Trusted canonical identity input only; not a path-based tool interface.
450
- * @param {import('./state.js').Execution} execution @param {string} resourceId */
451
- acquire(execution, resourceId) {
452
- const captured = { ...execution }
453
- return transact(draft => {
454
- if (cancelled.has(captured.sessionId)) throw new Error('session cancelled')
455
- checkNotRevoked(captured.sessionId)
456
- return draft.operations.acquire(captured, resourceId)
457
- }, { kind: 'resources', resourceIds: [resourceId] }).then(async token => {
458
- const cancellation = cancelled.get(captured.sessionId)
459
- if (cancellation) {
460
- await cancellation
461
- throw new Error('session cancelled during acquisition')
462
- }
463
- healthy()
464
- operations.checkWrite(token)
465
- return token
466
- })
467
- },
468
- /** Trusted per-file confirmation after explicit resume. Never acquires an
469
- * unowned resource: only this session's pending-confirmation lock activates.
470
- * @param {import('./state.js').Execution} execution @param {string} resourceId */
471
- confirm(execution, resourceId) {
472
- const captured = { ...execution }
473
- return transact(draft => {
474
- if (cancelled.has(captured.sessionId)) throw new Error('session cancelled')
475
- const lock = operations.status().locks.find(item => item.resourceId === resourceId)
476
- if (!lock || lock.owner !== captured.sessionId || lock.status !== 'pending-confirmation') {
477
- throw new Error('resource has no pending confirmation for this session')
478
- }
479
- return draft.operations.acquire(captured, resourceId)
480
- }, { kind: 'resources', resourceIds: [resourceId] }).then(async token => {
481
- const cancellation = cancelled.get(captured.sessionId)
482
- if (cancellation) {
483
- await cancellation
484
- throw new Error('session cancelled during confirmation')
485
- }
486
- healthy()
487
- operations.checkWrite(token)
488
- return token
489
- })
490
- },
491
- /** Success-only cleanup is atomic; cancellation retains all batch ownership.
492
- * @param {import('./state.js').Ownership[]} tokens */
493
- releaseMany(tokens) {
494
- const captured = tokens.map(token => ({ ...token }))
495
- return transact(draft => {
496
- for (const token of captured) {
497
- if (cancelled.has(token.sessionId)) throw new Error('session cancelled')
498
- draft.operations.checkWrite(token)
499
- }
500
- for (const token of captured) draft.operations.release(token)
501
- }, { kind: 'resources', resourceIds: captured.map(token => token.resourceId) }, 'release')
502
- },
503
- /** Atomic acquisition of existing canonical resources for a trusted batch editor.
504
- * Pre-existing ownership is retained and explicitly excluded from cleanup.
505
- * @param {import('./state.js').Execution} execution @param {string[]} resourceIds */
506
- acquireMany(execution, resourceIds) {
507
- const captured = { ...execution }
508
- const ids = [...new Set(resourceIds)].sort()
509
- return transact(draft => {
510
- if (cancelled.has(captured.sessionId)) throw new Error('session cancelled')
511
- const before = operations.status().locks
512
- if (before.some(lock => ids.includes(lock.resourceId) && lock.status === 'pending-confirmation')) {
513
- throw new Error('resource requires explicit per-file confirmation (/edit-lock confirm <path>)')
514
- }
515
- const tokens = ids.map(resourceId => draft.operations.acquire(captured, resourceId))
516
- for (const token of tokens) draft.operations.checkWrite(token)
517
- return { tokens, temporary: tokens.filter(token => !before.some(lock => lock.resourceId === token.resourceId)) }
518
- }, { kind: 'resources', resourceIds: ids }).then(async result => {
519
- const cancellation = cancelled.get(captured.sessionId)
520
- if (cancellation) { await cancellation; throw new Error('session cancelled during batch acquisition') }
521
- healthy()
522
- for (const token of result.tokens) operations.checkWrite(token)
523
- return result
524
- })
525
- },
526
- /** Cleanup authenticates the captured owner/generation through the kernel.
527
- * @param {import('./state.js').Ownership} token */
528
- release(token) {
529
- const captured = { ...token }
530
- return transact(draft => draft.operations.release(captured), { kind: 'resources', resourceIds: [captured.resourceId] }, 'release')
531
- },
532
- /** Give up every active lock of one session: the disposition used when a
533
- * finished turn's locks were never sorted out. Retention is a policy decision
534
- * that lives above this layer, so the caller supplies the cap. Refusal is
535
- * always safe. @param {string} sessionId @param {number} now */
536
- releaseActive(sessionId, now, requireExpired = false) {
537
- return transact(draft => {
538
- if (requireExpired && !operations.settleHold(sessionId, now).expired) return []
539
- const status = operations.status()
540
- const session = status.sessions.find(item => item.sessionId === sessionId)
541
- if (!session) throw new Error('unknown session')
542
- const released = []
543
- for (const lock of status.locks) {
544
- if (lock.owner !== sessionId || lock.status !== 'active') continue
545
- draft.operations.release({ managerIncarnation, sessionId, executionEpoch: session.executionEpoch, resourceId: lock.resourceId, generation: lock.generation })
546
- released.push(lock.resourceId)
547
- }
548
- return released
549
- }, () => ({ kind: 'resources', resourceIds: operations.status().locks
550
- .filter(lock => lock.owner === sessionId && lock.status === 'active').map(lock => lock.resourceId) }), 'release')
551
- },
552
- /** Charge and record an explicit retention request (design D2). The caller
553
- * resolves the configured caps and supplies the instant, so this layer holds
554
- * no policy and no clock. Ownership is untouched: retention only extends it.
555
- * @param {string} sessionId @param {number} ms
556
- * @param {{now: number, singleMaxMs: number, cumulativeMaxMs: number}} caps
557
- * @returns {Promise<import('./state.js').HoldState>} */
558
- hold(sessionId, ms, caps) {
559
- // The candidate is computed on the draft, i.e. at this request's FIFO
560
- // position: computed earlier on the live kernel it could be based on a hold
561
- // that a queued turn start is about to end, and would revive it. The draft
562
- // is only installed after persistence, so a store failure changes nothing.
563
- return transact(draft => draft.operations.hold(draft.operations.holdCandidate(sessionId, ms, caps)), () => sessionResources(sessionId))
564
- },
565
- /** Retention state of one session, settled against the caller's instant so a
566
- * missed timer cannot leave stale ownership. Pure read; never mutates.
567
- * @param {string} sessionId @param {number} now */
568
- settlement(sessionId, now) {
569
- healthy()
570
- return operations.settleHold(sessionId, now)
571
- },
572
- /** The holder started a new turn: every lock it holds leaves the holding state
573
- * at once (design D4). Never releases. @param {string} sessionId @param {number} now */
574
- endHold(sessionId, now) {
575
- healthy()
576
- const held = operations.settleHold(sessionId, now)
577
- if (!held.held) return held
578
- return transact(draft => draft.operations.endHold(sessionId), { kind: 'none' }, 'revocation').then(() => operations.settleHold(sessionId, now))
579
- },
580
- /** Consented transfer: release the holder's exact generation and acquire
581
- * for the requester in ONE durable transaction. Either side cancelled,
582
- * stale or interrupted rejects the whole transfer and ownership stays.
583
- * @param {import('./state.js').Ownership} holder @param {import('./state.js').Execution} requester */
584
- transfer(holder, requester) {
585
- const from = { ...holder }, to = { ...requester }
586
- return transact(draft => {
587
- if (cancelled.has(from.sessionId) || cancelled.has(to.sessionId)) throw new Error('session cancelled')
588
- draft.operations.release(from)
589
- return draft.operations.acquire(to, from.resourceId)
590
- }, { kind: 'resources', resourceIds: [from.resourceId] }).then(async token => {
591
- const cancellation = cancelled.get(to.sessionId)
592
- if (cancellation) { await cancellation; throw new Error('session cancelled during transfer') }
593
- healthy()
594
- return token
595
- })
596
- },
597
- /** Controlled human unlock. Runs at its FIFO position, so every earlier
598
- * commit has settled; refuses removal of unresolved publication ownership
599
- * or a generation other than the expected current generation. A
600
- * prepared old-owner write then fails its generation check. There is no
601
- * unconditional variant. @param {string} resourceId @param {number} generation */
602
- adminUnlock(resourceId, generation) {
603
- return transact(draft => {
604
- const status = operations.status()
605
- const lock = status.locks.find(item => item.resourceId === resourceId)
606
- if (!lock) throw new Error('resource is not locked')
607
- if (lock.generation !== generation) throw new Error(`expected generation ${generation}, current is ${lock.generation}`)
608
- const session = status.sessions.find(item => item.sessionId === lock.owner)
609
- if (!session) throw new Error('owner session unknown')
610
- draft.operations.release({ managerIncarnation, sessionId: lock.owner, executionEpoch: session.executionEpoch, resourceId, generation })
611
- return { resourceId, generation, owner: lock.owner }
612
- }, { kind: 'resources', resourceIds: [resourceId] }, 'release')
613
- },
614
- /** Trusted online administrative recovery (design D4): the settings-plane
615
- * operator's one explicit click, executed as ONE serialized transaction of
616
- * the running manager — locally or forwarded over the peer channel. Every
617
- * check the offline ADMIN OVERRIDE performs is re-verified HERE at the
618
- * FIFO execution point against the live confirmed state: owner
619
- * interrupted, every unresolved operation of the owner unknown-phase, the
620
- * operation id set exact (sorted compare), the revision unmoved and the
621
- * confirmation digest matching the SAME scoped algorithm (canonical JSON
622
- * of {root, owner, expectedRevision, operationIds sorted, risk:
623
- * LATE_WRITER_RISK}, SHA-256, "ADMIN OVERRIDE <hex>"). The durable commit
624
- * is the offline commit's exact shape: the adminRecoveries ledger record
625
- * (actor marks it online), the owner's locks released and its epoch
626
- * revoked, admission unblocked; unknown outcomes and operation history
627
- * are byte-identical. A retry with the same recoveryId returns the
628
- * existing record WITHOUT re-releasing or appending. Refusals carry a
629
- * `weir-edit-lock/*` code and never persist; a backup-writer failure
630
- * happens before the commit attempt and changes nothing; only a failed
631
- * or uncertain durable commit poisons (same discipline as transact).
632
- * @param {any} rawInput
633
- * @returns {Promise<{revision: number, idempotent: boolean, record: any}>} */
634
- adminRecoverOnline(rawInput) {
635
- healthy()
636
- const refuse = (/** @type {string} */ code, /** @type {string} */ message) => {
637
- throw Object.assign(new Error(message), { code: `weir-edit-lock/${code}`, recoveryRefusal: true })
638
- }
639
- if (!root || !writeBackup) refuse('unavailable', 'online administrative recovery requires a domain root and backup writer in this composition')
640
- // Capture before the first await: the operator request cannot change in flight.
641
- const input = structuredClone(rawInput)
642
- const keys = ['owner', 'expectedRevision', 'operationIds', 'recoveryId', 'reason', 'acceptLateWriterRisk', 'confirmation']
643
- if (!input || typeof input !== 'object' || Array.isArray(input) ||
644
- Object.keys(input).length !== keys.length || !keys.every(key => Object.hasOwn(input, key))) {
645
- refuse('invalid-recovery', 'Exact recovery fields required')
646
- }
647
- for (const key of ['owner', 'recoveryId', 'reason']) {
648
- if (typeof input[key] !== 'string' || !input[key].trim() ||
649
- input[key].length > (key === 'reason' ? 2000 : key === 'recoveryId' ? 128 : 512) || input[key].includes('\0')) {
650
- refuse('invalid-recovery', `Invalid ${key}`)
651
- }
652
- }
653
- if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) refuse('invalid-recovery', 'Expected revision required')
654
- if (!Array.isArray(input.operationIds) || input.operationIds.length === 0 || input.operationIds.length > 10000 ||
655
- !input.operationIds.every((/** @type {any} */ id) => typeof id === 'string' && id.trim() && id.length <= 1024)) {
656
- refuse('invalid-recovery', 'Operation IDs required')
657
- }
658
- if (new Set(input.operationIds).size !== input.operationIds.length) refuse('invalid-recovery', 'Duplicate operation ID')
659
- if (input.acceptLateWriterRisk !== true) refuse('invalid-recovery', 'Explicit late-writer risk acceptance required')
660
- const scoped = { root, owner: input.owner, expectedRevision: input.expectedRevision, operationIds: [...input.operationIds].sort(), risk: LATE_WRITER_RISK }
661
- if (input.confirmation !== `ADMIN OVERRIDE ${digest(scoped)}`) refuse('confirmation-mismatch', 'Explicit scoped ADMIN OVERRIDE confirmation required')
662
- const pending = tail.then(async () => {
663
- healthy()
664
- const state = confirmed.state
665
- // Idempotent retry: the same recoveryId returns the committed record
666
- // without re-releasing, re-revoking or appending.
667
- const existing = state.adminRecoveries?.find((/** @type {any} */ row) => row.recoveryId === input.recoveryId)
668
- if (existing) {
669
- if (existing.confirmation !== digest(scoped) || existing.reason !== input.reason) refuse('invalid-recovery', 'Recovery ID reused with different request')
670
- return { revision: confirmed.revision, idempotent: true, record: structuredClone(existing) }
671
- }
672
- if (confirmed.revision !== input.expectedRevision) refuse('revision-conflict', 'Authority revision changed; inspect and confirm again')
673
- if (revokedOwner(state, input.owner)) refuse('invalid-recovery', 'owner already revoked by administrative recovery')
674
- const session = state.sessions.find((/** @type {any} */ row) => row.sessionId === input.owner)
675
- if (!session || !session.interrupted) refuse('invalid-recovery', 'Only an interrupted historical owner can be overridden')
676
- const unresolved = state.operations.filter((/** @type {any} */ op) => op.sessionId === input.owner && ['prepared', 'publishing', 'unknown'].includes(op.phase) && !dispositionFor(state, op))
677
- if (!(unresolved.length > 0 && unresolved.every((/** @type {any} */ op) => op.phase === 'unknown'))) refuse('invalid-recovery', 'Recover to stable unknown history before administrative override')
678
- if (canonical(unresolved.map((/** @type {any} */ op) => op.operationId).sort()) !== canonical(scoped.operationIds)) refuse('invalid-recovery', 'Confirm every unresolved operation for this owner')
679
- const bytes = store.currentBytes()
680
- if (!bytes) refuse('unavailable', 'online administrative recovery requires the committed authority bytes')
681
- const backup = { file: `admin-backup-${digest([root, input.recoveryId])}.json`,
682
- sha256: createHash('sha256').update(bytes).digest('hex'), bytes: bytes.length }
683
- const record = { recoveryId: input.recoveryId, root, owner: input.owner,
684
- expectedRevision: confirmed.revision, committedRevision: confirmed.revision + 1, at: Date.now(), actor: ONLINE_RECOVERY_ACTOR,
685
- reason: input.reason, risk: LATE_WRITER_RISK, confirmation: digest(scoped), backup,
686
- operations: unresolved.map((/** @type {any} */ op) => ({ operationId: op.operationId, sha256: digest(op) })),
687
- releasedLocks: state.locks.filter((/** @type {any} */ lock) => lock.owner === input.owner), revokedEpoch: session.executionEpoch + 1 }
688
- // The kernel fold is exactly administrativeState's ordinary part:
689
- // revoke the owner's epoch (cancel) and release its exact locks; the
690
- // batch allowance ends with the last lock. The ledger append and the
691
- // version discipline are applied on top of the checkpoint, so the
692
- // committed state is byte-equal to administrativeState(base, record).
693
- const draft = authority.begin()
694
- try {
695
- draft.authority.cancel({ managerIncarnation, sessionId: session.sessionId, executionEpoch: session.executionEpoch })
696
- for (const lock of record.releasedLocks) {
697
- draft.operations.release({ managerIncarnation, sessionId: session.sessionId, executionEpoch: record.revokedEpoch, resourceId: lock.resourceId, generation: lock.generation })
698
- }
699
- } catch (error) {
700
- authority.discard(draft)
701
- throw error
702
- }
703
- const base = withIdentity(state)
704
- const nextState = { ...base, ...authority.checkpoint(draft), version: /** @type {5|6} */ (base.version === 6 ? 6 : 5),
705
- adminRecoveries: [...(base.adminRecoveries ?? []), structuredClone(record)] }
706
- // The backup must be durable BEFORE the commit that references it; a
707
- // failure here attempted no persistence and leaves the manager healthy.
708
- try { await writeBackup(record.backup.file, bytes) } catch (error) {
709
- try { authority.discard(draft) } catch { /* draft already closed */ }
710
- throw error
711
- }
712
- try {
713
- const saved = await store.recordAdministrativeRecovery({ expectedRevision: confirmed.revision, nextState, base })
714
- authority.install(draft)
715
- confirmed = saved
716
- return { revision: saved.revision, idempotent: false, record }
717
- } catch (error) {
718
- try { authority.discard(draft) } catch { /* draft already closed */ }
719
- poison = error
720
- throw new Error('manager persistence or installation failed; poisoned', { cause: error })
721
- }
722
- })
723
- tail = pending.then(() => {}, () => {})
724
- return pending
725
- },
726
- /** Trusted maintenance ingress for the stale-lock sweep: conditionally
727
- * release rows a publisher-side scan observed with a missing target. Every
728
- * row runs in its OWN FIFO transaction that re-verifies the full
729
- * observation at the execution point — owner, generation, owner execution
730
- * epoch and lock status — and re-probes through the caller's isMissing
731
- * that the resource is still missing; only then an ordinary release.
732
- * Scope is every observed row regardless of owner state (active, holding,
733
- * pending-confirmation, user-interrupted, abnormal): the row predicate,
734
- * not the owner class, provides the safety. Any mismatch, an already-gone
735
- * row, or the ordinary unresolved-publication fence admission refusal is
736
- * a SKIP — a discarded draft, never a persisted no-op, never an error —
737
- * and every other row is still processed. Persistence, poison and any
738
- * unexpected error are NOT skips: the row lands in `failed` so the caller
739
- * can log it (fail-closed semantics untouched: nothing was persisted).
740
- * The missing-target probe is injected: this layer performs no filesystem IO.
741
- * @param {{resourceId: string, owner: string, generation: number, executionEpoch: number, status: string}[]} observed
742
- * @param {(resourceId: string) => boolean} isMissing
743
- * @returns {Promise<{released: {resourceId: string, owner: string, generation: number}[], skipped: {row: object, reason: string}[], failed: {row: object, reason: string}[]}>} */
744
- async releaseStale(observed, isMissing) {
745
- healthy()
746
- if (typeof isMissing !== 'function') throw new Error('stale release requires a missing-target probe')
747
- const released = []
748
- const skipped = []
749
- /** @type {{row: object, reason: string}[]} */
750
- const failed = []
751
- /** An expected skip: the observation no longer holds at the execution
752
- * point; a discarded draft, never a persisted no-op, never an error.
753
- * @param {string} reason */
754
- const staleSkip = reason => Object.assign(new Error(reason), { staleSkip: true })
755
- for (const input of Array.isArray(observed) ? observed : []) {
756
- const row = { resourceId: input?.resourceId, owner: input?.owner, generation: input?.generation, executionEpoch: input?.executionEpoch, status: input?.status }
757
- try {
758
- released.push(await transact(draft => {
759
- const status = operations.status()
760
- const lock = status.locks.find(item => item.resourceId === row.resourceId)
761
- if (!lock) throw staleSkip('stale row already released')
762
- const session = status.sessions.find(item => item.sessionId === lock.owner)
763
- if (!session || lock.owner !== row.owner || lock.generation !== row.generation ||
764
- lock.status !== row.status || session.executionEpoch !== row.executionEpoch) {
765
- throw staleSkip('stale row changed before execution')
766
- }
767
- if (!isMissing(row.resourceId)) throw staleSkip('stale row target exists again')
768
- draft.operations.release({ managerIncarnation, sessionId: lock.owner, executionEpoch: session.executionEpoch, resourceId: lock.resourceId, generation: lock.generation })
769
- return { resourceId: lock.resourceId, owner: lock.owner, generation: lock.generation }
770
- }, { kind: 'resources', resourceIds: [row.resourceId] }, 'release'))
771
- } catch (error) {
772
- const reason = String(/** @type {any} */ (error)?.message ?? error)
773
- // Quiet, counted skips: the row was gone, the predicate no longer
774
- // matched, or ordinary fence admission refused. Anything else —
775
- // persistence failure, poison, unexpected — is a surfaced failure.
776
- if (/** @type {any} */ (error)?.staleSkip === true || /** @type {any} */ (error)?.admissionRefusal === true) skipped.push({ row, reason })
777
- else failed.push({ row, reason })
778
- }
779
- }
780
- return { released, skipped, failed }
781
- },
782
- /** Trusted classifier only (durable turn/end error). Marks every retained,
783
- * not yet abnormal lock of the session abnormal; never releases. Subtractive,
784
- * so it may cross an unresolved publication fence.
785
- * @param {string} sessionId @param {string} reason */
786
- markAbnormal(sessionId, reason) {
787
- return transact(draft => {
788
- const status = operations.status()
789
- const session = status.sessions.find(item => item.sessionId === sessionId)
790
- if (!session) throw new Error('unknown session')
791
- const marked = []
792
- for (const lock of status.locks) {
793
- if (lock.owner !== sessionId || lock.status === 'abnormal') continue
794
- draft.authority.markAbnormal({ managerIncarnation, sessionId, executionEpoch: session.executionEpoch, resourceId: lock.resourceId, generation: lock.generation }, reason)
795
- marked.push(lock.resourceId)
796
- }
797
- return marked
798
- }, { kind: 'none' }, 'revocation')
799
- },
800
- /** Monotonic durable recovery budget charge; restart never refunds it.
801
- * @param {string} sessionId @param {{attempts?: number, elapsedMs?: number, pauseMs?: number}} delta */
802
- chargeRecovery(sessionId, delta) {
803
- for (const value of [delta.attempts ?? 0, delta.elapsedMs ?? 0, delta.pauseMs ?? 0]) {
804
- if (!Number.isSafeInteger(value) || value < 0) throw new Error('invalid recovery charge')
805
- }
806
- /** @type {any} */
807
- let charged
808
- return transact(() => {
809
- if (!operations.status().sessions.some(item => item.sessionId === sessionId)) throw new Error('unknown session')
810
- }, { kind: 'none' }, 'revocation', state => {
811
- const recovery = state.recovery.map(/** @param {any} item */ item => ({ ...item }))
812
- let entry = recovery.find(/** @param {any} item */ item => item.sessionId === sessionId)
813
- if (!entry) { entry = { sessionId, attempts: 0, elapsedMs: 0, pauseMs: 0 }; recovery.push(entry) }
814
- entry.attempts += delta.attempts ?? 0
815
- entry.elapsedMs += delta.elapsedMs ?? 0
816
- entry.pauseMs += delta.pauseMs ?? 0
817
- charged = { ...entry }
818
- return { recovery }
819
- }).then(() => charged)
820
- },
821
- /** Durable recovery usage of one session (zeros when never charged).
822
- * @param {string} sessionId */
823
- recoveryUsage(sessionId) {
824
- healthy()
825
- const entry = confirmed.state.recovery.find(/** @param {any} item */ item => item.sessionId === sessionId)
826
- return entry ? { ...entry } : { sessionId, attempts: 0, elapsedMs: 0, pauseMs: 0 }
827
- },
828
- /** Synchronous denial overlay must be checked before publication.
829
- * @param {import('./state.js').Ownership} token */
830
- checkWrite(token) {
831
- healthy()
832
- if (cancelled.has(token.sessionId)) throw new Error('session cancelled')
833
- return operations.checkWrite(token)
834
- },
835
- /** @param {import('./state.js').Execution} execution */
836
- cancel(execution) {
837
- healthy()
838
- execution = { ...execution }
839
- const session = operations.status().sessions.find(s => s.sessionId === execution.sessionId)
840
- if (execution.managerIncarnation !== managerIncarnation || !session || session.executionEpoch !== execution.executionEpoch) {
841
- throw new Error('invalid cancellation execution')
842
- }
843
- const existing = cancelled.get(execution.sessionId)
844
- if (existing) return existing
845
- const pending = transact(draft => {
846
- // Admission authenticated the live execution; cancellation also seals an
847
- // earlier queued resume that installs before this revocation is durable.
848
- const current = operations.status().sessions.find(s => s.sessionId === execution.sessionId)
849
- if (!current) throw new Error('unknown session')
850
- if (current.interrupted) return { ...execution, executionEpoch: current.executionEpoch }
851
- return draft.authority.cancel({ ...execution, executionEpoch: current.executionEpoch })
852
- }, { kind: 'none' }, 'revocation')
853
- cancelled.set(execution.sessionId, pending)
854
- // Keep denial on failure; poisoned state must never expose rollback authority.
855
- void pending.then(() => cancelled.delete(execution.sessionId), () => {})
856
- return pending
857
- },
858
- /** Host shutdown waits for every queued authority and publication mutation. */
859
- async drain() { await tail },
860
- /** Seal authority after draining; never a permission to enable unlocked edits. */
861
- async close() { closed = true; await tail },
862
- status() {
863
- healthy()
864
- const snapshot = operations.status()
865
- for (const session of snapshot.sessions) {
866
- if (cancelled.has(session.sessionId)) session.interrupted = true
867
- session.revoked = revokedOwner(confirmed.state, session.sessionId)
868
- }
869
- for (const lock of snapshot.locks) {
870
- if (cancelled.has(lock.owner) && lock.status !== 'abnormal') lock.status = 'user-interrupted'
871
- }
872
- return snapshot
873
- },
874
- })
875
- }