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,462 +0,0 @@
1
- /**
2
- * Unmounted, synchronous, in-memory authorization kernel. No IO or runtime imports.
3
- * The embedding manager MUST keep `authority` private, derive session identity from
4
- * trusted execution context, and supply canonical resource IDs. Fencing fields are
5
- * not authentication secrets. checkWrite is an observation, NOT a transferable
6
- * permission to publish later; a future manager must serialize actual publication.
7
- *
8
- * @typedef {{ managerIncarnation: string, sessionId: string, executionEpoch: number }} Execution
9
- * @typedef {Execution & { resourceId: string, generation: number }} Ownership
10
- * @typedef {{ sessionId: string, executionEpoch: number, interrupted: boolean }} Session
11
- * @typedef {{ resourceId: string, owner: string, generation: number, status: 'active' | 'user-interrupted' | 'pending-confirmation' | 'abnormal', reason?: string }} Lock
12
- *
13
- * Retention (design D1) is a SESSION-level budget: one lock batch shares a single
14
- * cumulative allowance, so the user reads one expiry instead of N countdowns.
15
- * `holding` means the session's turn ended while it still owns locks. It is NOT a
16
- * lock status: a held lock keeps its ordinary `active` status and its ordinary
17
- * meaning, so another session is still refused and a transfer is still negotiated.
18
- * `holdUntil` is an absolute epoch-ms instant supplied by the caller (this kernel
19
- * reads no clock). `holdCumulativeMs` only ever grows within a batch and is never
20
- * refunded. A new turn clears `holding`/`holdUntil` but NOT the cumulative usage;
21
- * releasing every lock ends the batch and resets it.
22
- * @typedef {{ sessionId: string, holding: boolean, holdUntil: number | null, holdCumulativeMs: number }} HoldState
23
- * @typedef {{ sessionId: string, held: boolean, expired: boolean, holdUntil: number | null, remainingMs: number, holdCumulativeMs: number }} HoldSettlement
24
- * @typedef {{ now?: number, singleMaxMs: number, cumulativeMaxMs: number }} HoldCaps
25
- */
26
-
27
- import { settleHoldFacts } from './derived-facts.js'
28
- /** Private transfer seam; no raw hydration is exported. */
29
- const cores = new WeakMap()
30
- /** @param {string} managerIncarnation */
31
- export function createEditLockState(managerIncarnation) {
32
- requireId(managerIncarnation)
33
- /** @type {Map<string, Session>} */
34
- const sessions = new Map()
35
- /** @type {Map<string, Lock>} */
36
- const locks = new Map()
37
- /** Retention state per registered session; a session with no batch has none.
38
- * @type {Map<string, HoldState>} */
39
- const holds = new Map()
40
- // Tombstones survive release so the same owner cannot revive an old token.
41
- /** @type {Map<string, number>} */
42
- const generations = new Map()
43
- /** @type {WeakMap<object, Execution & { requestId: string }>} */
44
- const receipts = new WeakMap()
45
- // Keep request tombstones for this in-memory manager lifetime, even after consumption.
46
- /** @type {Map<string, Set<string>>} */
47
- const issuedRequests = new Map()
48
- let revision = 0
49
- const drafts = new WeakMap()
50
- /** @param {object} draft */
51
- function draftFor(draft) {
52
- const entry = drafts.get(draft)
53
- if (!entry || entry.closed || entry.base !== revision) throw new Error('invalid or stale draft')
54
- return entry
55
- }
56
-
57
- /** @param {Execution} execution */
58
- function sessionFor(execution) {
59
- if (!execution || typeof execution !== 'object') throw new Error('invalid execution')
60
- if (execution.managerIncarnation !== managerIncarnation) throw new Error('stale incarnation')
61
- const session = sessions.get(execution.sessionId)
62
- if (!session) throw new Error('unknown session')
63
- if (session.executionEpoch !== execution.executionEpoch) throw new Error('stale epoch')
64
- return session
65
- }
66
-
67
- /** @param {Ownership} token */
68
- function lockFor(token) {
69
- sessionFor(token)
70
- const lock = locks.get(token.resourceId)
71
- if (!lock || lock.owner !== token.sessionId || lock.generation !== token.generation) {
72
- throw new Error('stale ownership')
73
- }
74
- return lock
75
- }
76
-
77
- /** Retention is a session-level budget, never a lock status: a held lock stays
78
- * `active` so other sessions are refused exactly as before and a transfer is
79
- * still negotiated. This block only owns the hold bookkeeping. */
80
- const retention = {
81
- /** @param {string} sessionId */
82
- holdState(sessionId) {
83
- const held = holds.get(sessionId)
84
- return held ? Object.freeze({ ...held }) : undefined
85
- },
86
- /** End the batch once its last lock is gone: the cumulative allowance belongs
87
- * to the batch, not to the session forever. The row itself is NEVER removed —
88
- * the image keeps exactly one row per known session — it returns to zero.
89
- * Idempotent. @param {string} sessionId */
90
- endBatch(sessionId) {
91
- const held = holds.get(sessionId)
92
- if (!held || [...locks.values()].some(lock => lock.owner === sessionId)) return
93
- held.holding = false
94
- held.holdUntil = null
95
- held.holdCumulativeMs = 0
96
- },
97
- /** Clear retention because the holder started a new turn (design D4): its
98
- * locks stay owned and are ordinary active ownership again immediately, while
99
- * the batch's accumulated allowance stays charged. @param {string} sessionId */
100
- endHold(sessionId) {
101
- const held = holds.get(sessionId)
102
- if (!held) return
103
- held.holding = false
104
- held.holdUntil = null
105
- },
106
- /** Pure computation of the retention row a request would produce. The manager
107
- * persists this before installing it, so an IO failure can never leave the live
108
- * kernel ahead of the image. Refusal is always safe, and the caller supplies
109
- * already-resolved caps so this kernel holds no configuration.
110
- * @param {string} sessionId @param {number} ms @param {HoldCaps} caps
111
- * @returns {HoldState} */
112
- holdCandidate(sessionId, ms, caps) {
113
- if (!sessions.has(sessionId)) throw new Error('unknown session')
114
- if (!Number.isSafeInteger(ms) || ms <= 0) throw new Error('hold duration must be a positive whole number of milliseconds')
115
- if (!caps || typeof caps !== 'object') throw new Error('hold caps required')
116
- if (!Number.isFinite(caps.singleMaxMs) || caps.singleMaxMs <= 0) throw new Error('invalid single retention cap')
117
- if (!Number.isFinite(caps.cumulativeMaxMs) || caps.cumulativeMaxMs <= 0) throw new Error('invalid cumulative retention cap')
118
- if (ms > caps.singleMaxMs) throw new Error(`a single retention is limited to ${Math.round(caps.singleMaxMs / 60_000)} minutes`)
119
- if (![...locks.values()].some(lock => lock.owner === sessionId)) throw new Error('this session holds no lock to retain')
120
- const held = holds.get(sessionId) ?? { sessionId, holding: false, holdUntil: null, holdCumulativeMs: 0 }
121
- // A reservation still running is EXTENDED by `ms`, never re-bought.
122
- const now = Number.isFinite(caps.now) ? /** @type {number} */ (caps.now) : null
123
- // A persisted retention must always carry an expiry: without an instant the
124
- // row could never be settled on read, so the caller must supply one.
125
- if (now === null) throw new Error('a retention request requires a finite instant')
126
- const running = held.holding && held.holdUntil !== null && held.holdUntil > now
127
- // Charged: only the added minutes. Bounded: the whole window from now, so
128
- // repeated asks can neither re-buy paid time nor stack past the single cap.
129
- const charged = ms
130
- const windowMs = running ? /** @type {number} */ (held.holdUntil) + ms - now : ms
131
- if (windowMs > caps.singleMaxMs) {
132
- throw new Error(`a single retention is limited to ${Math.round(caps.singleMaxMs / 60_000)} minutes`)
133
- }
134
- if (held.holdCumulativeMs + charged > caps.cumulativeMaxMs) {
135
- throw new Error(`cumulative retention budget exhausted (${Math.floor((caps.cumulativeMaxMs - held.holdCumulativeMs) / 60_000)} minutes left)`)
136
- }
137
- const base = running ? /** @type {number} */ (held.holdUntil) : now
138
- return Object.freeze({ sessionId, holding: true, holdUntil: base + ms, holdCumulativeMs: held.holdCumulativeMs + charged })
139
- },
140
- /** Install a candidate from `holdCandidate`. One-shot and validated, so a
141
- * tampered, replayed or stale row cannot be installed: allowance never
142
- * decreases and an active period is never shortened. @param {HoldState} candidate
143
- * @returns {HoldState} */
144
- hold(candidate) {
145
- if (!candidate || typeof candidate !== 'object') throw new Error('invalid retention candidate')
146
- const current = holds.get(candidate.sessionId)
147
- if (!current) throw new Error('unknown session')
148
- if (candidate.holding !== true || !Number.isFinite(candidate.holdUntil)) throw new Error('retention candidate is not active')
149
- if (!Number.isSafeInteger(candidate.holdCumulativeMs) || candidate.holdCumulativeMs <= current.holdCumulativeMs) {
150
- throw new Error('retention candidate loses allowance')
151
- }
152
- const until = /** @type {number} */ (candidate.holdUntil)
153
- if (current.holding && current.holdUntil !== null && until < current.holdUntil) {
154
- throw new Error('retention candidate shortens an active period')
155
- }
156
- if (![...locks.values()].some(lock => lock.owner === candidate.sessionId)) throw new Error('this session holds no lock to retain')
157
- const held = { sessionId: candidate.sessionId, holding: true, holdUntil: until, holdCumulativeMs: candidate.holdCumulativeMs }
158
- holds.set(candidate.sessionId, held)
159
- return Object.freeze({ ...held })
160
- },
161
- /** Settle retention against a caller-supplied instant (read-time settlement,
162
- * design D5). Idempotent, and it never releases anything, so it is safe on a
163
- * read path. @param {string} sessionId @param {number} now @returns {HoldSettlement} */
164
- settleHold(sessionId, now) {
165
- if (!Number.isFinite(now)) throw new Error('settlement requires a finite instant')
166
- const held = holds.get(sessionId)
167
- if (!held) return Object.freeze({ sessionId, held: false, expired: false, holdUntil: null, remainingMs: 0, holdCumulativeMs: 0 })
168
- const owns = [...locks.values()].some(lock => lock.owner === sessionId)
169
- return Object.freeze(settleHoldFacts(sessionId, held, owns, now))
170
- },
171
- }
172
-
173
- const operations = {
174
- /** @param {Execution} execution @param {string} resourceId @returns {Ownership} */
175
- acquire(execution, resourceId) {
176
- const session = sessionFor(execution)
177
- requireId(resourceId)
178
- if (session.interrupted) throw new Error('session interrupted')
179
- const existing = locks.get(resourceId)
180
- if (existing && existing.owner !== execution.sessionId) throw new Error('resource owned')
181
- if (existing?.status === 'abnormal') throw new Error('lock abnormal')
182
- const lock = existing ?? { resourceId, owner: execution.sessionId, generation: (generations.get(resourceId) ?? 0) + 1, status: 'active' }
183
- lock.status = 'active'
184
- generations.set(resourceId, lock.generation)
185
- locks.set(resourceId, lock)
186
- return { ...execution, resourceId, generation: lock.generation }
187
- },
188
- /** @param {Ownership} token */
189
- release(token) {
190
- lockFor(token)
191
- locks.delete(token.resourceId)
192
- // The batch ends with its last lock: the cumulative allowance is per batch.
193
- retention.endBatch(token.sessionId)
194
- },
195
- /** @param {Ownership} token */
196
- checkWrite(token) {
197
- const lock = lockFor(token)
198
- if (lock.status !== 'active') throw new Error('lock not active')
199
- return { allowed: true }
200
- },
201
- /** Consume an opaque receipt issued only by the trusted authority facet.
202
- * @param {Execution} execution @param {string} requestId @param {object} receipt
203
- * @returns {Execution} */
204
- resume(execution, requestId, receipt) {
205
- const session = sessionFor(execution)
206
- const binding = receipts.get(receipt)
207
- if (!binding || binding.sessionId !== session.sessionId ||
208
- binding.executionEpoch !== session.executionEpoch || binding.requestId !== requestId) {
209
- throw new Error('invalid receipt')
210
- }
211
- receipts.delete(receipt)
212
- session.executionEpoch += 1
213
- session.interrupted = false
214
- for (const lock of locks.values()) {
215
- if (lock.owner === session.sessionId && lock.status === 'user-interrupted') lock.status = 'pending-confirmation'
216
- }
217
- // A trusted resume restores editing authority; it is not a continuing turn,
218
- // so the batch keeps its retention state.
219
- return { managerIncarnation, sessionId: session.sessionId, executionEpoch: session.executionEpoch }
220
- },
221
- status() {
222
- return {
223
- managerIncarnation,
224
- sessions: [...sessions.values()].map(session => ({ ...session })),
225
- locks: [...locks.values()].map(lock => ({ ...lock })),
226
- holds: [...holds.values()].map(held => ({ ...held })),
227
- }
228
- },
229
- }
230
-
231
- const authority = {
232
- /** Import only a validated historical checkpoint into a fresh kernel.
233
- * The opaque candidate has no mutation facets; it can only be inspected,
234
- * installed after persistence, or discarded. No receipts are imported.
235
- * @param {{ managerIncarnation: string, sessions: Session[], locks: Lock[], generations: {resourceId: string, generation: number}[], issuedRequests: {sessionId: string, requestId: string}[], holds?: HoldState[] }} history */
236
- beginRecovery(history) {
237
- if (revision !== 0) throw new Error('recovery requires fresh kernel')
238
- if (!history || history.managerIncarnation === managerIncarnation) throw new Error('recovery requires new incarnation')
239
- requireId(history.managerIncarnation)
240
- const child = createEditLockState(managerIncarnation)
241
- const core = cores.get(child)
242
- for (const session of history.sessions) {
243
- requireId(session.sessionId)
244
- const epoch = session.executionEpoch + (session.interrupted ? 0 : 1)
245
- if (typeof session.interrupted !== 'boolean' || !Number.isSafeInteger(session.executionEpoch) ||
246
- session.executionEpoch < 1 || !Number.isSafeInteger(epoch) || core.sessions.has(session.sessionId)) throw new Error('invalid recovery session')
247
- core.sessions.set(session.sessionId, { sessionId: session.sessionId, executionEpoch: epoch, interrupted: true })
248
- // Recovery restores ownership, never a running retention period, and the
249
- // batch's consumed allowance is carried over, not refunded.
250
- core.holds.set(session.sessionId, { sessionId: session.sessionId, holding: false, holdUntil: null, holdCumulativeMs: 0 })
251
- }
252
- for (const held of history.holds ?? []) {
253
- const restored = core.holds.get(held.sessionId)
254
- if (!restored || typeof held.holding !== 'boolean' || !Number.isSafeInteger(held.holdCumulativeMs) || held.holdCumulativeMs < 0 ||
255
- (held.holdUntil !== null && (!Number.isSafeInteger(held.holdUntil) || held.holdUntil < 0))) throw new Error('invalid recovery hold')
256
- // An expiry never survives a restart: clocks and timers are gone, so the row
257
- // returns to ordinary ownership without a running retention period.
258
- restored.holding = false
259
- restored.holdUntil = null
260
- restored.holdCumulativeMs = held.holdCumulativeMs
261
- }
262
- for (const item of history.generations) {
263
- requireId(item.resourceId)
264
- if (!Number.isSafeInteger(item.generation) || item.generation < 1 || core.generations.has(item.resourceId)) throw new Error('invalid recovery generation')
265
- core.generations.set(item.resourceId, item.generation)
266
- }
267
- for (const lock of history.locks) {
268
- requireId(lock.resourceId)
269
- if (!core.sessions.has(lock.owner) || core.locks.has(lock.resourceId) || core.generations.get(lock.resourceId) !== lock.generation ||
270
- !['active', 'pending-confirmation', 'user-interrupted', 'abnormal'].includes(lock.status)) throw new Error('invalid recovery lock')
271
- if (lock.status === 'abnormal') {
272
- if (typeof lock.reason !== 'string') throw new Error('invalid recovery reason')
273
- requireId(lock.reason)
274
- }
275
- core.locks.set(lock.resourceId, { resourceId: lock.resourceId, owner: lock.owner, generation: lock.generation,
276
- status: lock.status === 'abnormal' ? 'abnormal' : 'user-interrupted',
277
- ...(lock.status === 'abnormal' ? { reason: lock.reason } : {}) })
278
- }
279
- for (const item of history.issuedRequests) {
280
- requireId(item.requestId)
281
- if (!core.sessions.has(item.sessionId)) throw new Error('invalid recovery request')
282
- const issued = core.issuedRequests.get(item.sessionId) ?? new Set()
283
- if (issued.has(item.requestId)) throw new Error('duplicate recovery request')
284
- issued.add(item.requestId)
285
- core.issuedRequests.set(item.sessionId, issued)
286
- }
287
- // Seal the fresh-only ingress and invalidate earlier empty candidates.
288
- revision += 1
289
- const draft = Object.freeze({})
290
- drafts.set(draft, { base: revision, closed: false, child, core, issued: new Map() })
291
- return draft
292
- },
293
- begin() {
294
- const child = createEditLockState(managerIncarnation)
295
- const core = cores.get(child)
296
- for (const [key, value] of sessions) core.sessions.set(key, { ...value })
297
- for (const [key, value] of locks) core.locks.set(key, { ...value })
298
- for (const [key, value] of generations) core.generations.set(key, value)
299
- for (const [key, value] of issuedRequests) core.issuedRequests.set(key, new Set(value))
300
- for (const [key, value] of holds) core.holds.set(key, { ...value })
301
- const entry = { base: revision, closed: false, child, core, issued: new Map() }
302
- /** @param {object} facet */
303
- const guard = facet => Object.freeze(Object.fromEntries(Object.entries(facet).map(([key, method]) => [key,
304
- /** @param {...any} args */
305
- (...args) => {
306
- if (entry.closed || entry.base !== revision) throw new Error('invalid or stale draft')
307
- if (['begin', 'beginRecovery', 'install', 'discard', 'holdState', 'settleHold'].includes(key)) {
308
- throw new Error('operation unavailable in draft')
309
- }
310
- if (key === 'issueExecutionReceipt') {
311
- const receipt = method(...args)
312
- entry.issued.set(receipt, core.receipts.get(receipt))
313
- core.receipts.delete(receipt)
314
- return receipt
315
- }
316
- if (key === 'resume') {
317
- const receipt = args[2]
318
- const binding = receipts.get(receipt)
319
- if (!binding) throw new Error('invalid receipt')
320
- core.receipts.set(receipt, binding)
321
- try {
322
- const result = method(...args)
323
- receipts.delete(receipt)
324
- revision += 1
325
- entry.base = revision
326
- return result
327
- } finally {
328
- core.receipts.delete(receipt)
329
- }
330
- }
331
- return method(...args)
332
- },
333
- ])))
334
- const draft = Object.freeze({ operations: guard(child.operations), authority: guard(child.authority) })
335
- drafts.set(draft, entry)
336
- return draft
337
- },
338
- /** @param {object} draft */
339
- install(draft) {
340
- const entry = draftFor(draft)
341
- entry.closed = true
342
- sessions.clear(); locks.clear(); generations.clear(); issuedRequests.clear(); holds.clear()
343
- for (const [key, value] of entry.core.sessions) sessions.set(key, { ...value })
344
- for (const [key, value] of entry.core.locks) locks.set(key, { ...value })
345
- for (const [key, value] of entry.core.generations) generations.set(key, value)
346
- for (const [key, value] of entry.core.issuedRequests) issuedRequests.set(key, new Set(value))
347
- for (const [key, value] of entry.core.holds) holds.set(key, { ...value })
348
- for (const [receipt, binding] of entry.issued) receipts.set(receipt, binding)
349
- entry.issued.clear()
350
- revision += 1
351
- },
352
- /** @param {object} draft */
353
- discard(draft) {
354
- const entry = draftFor(draft)
355
- entry.closed = true
356
- entry.issued.clear()
357
- },
358
- /** Record only a trusted, already successful native creation, never an acquisition.
359
- * The manager must validate publishing history and canonical identity first.
360
- * @param {Execution} origin @param {string} resourceId @returns {Ownership} */
361
- settleCreated(origin, resourceId) {
362
- requireId(resourceId)
363
- if (!origin || origin.managerIncarnation !== managerIncarnation) throw new Error('stale incarnation')
364
- const session = sessions.get(origin.sessionId)
365
- if (!session) throw new Error('unknown session')
366
- if (!Number.isSafeInteger(origin.executionEpoch) || origin.executionEpoch < 1 ||
367
- origin.executionEpoch > session.executionEpoch) throw new Error('invalid creation epoch')
368
- if (locks.has(resourceId)) throw new Error('resource owned')
369
- const generation = (generations.get(resourceId) ?? 0) + 1
370
- if (!Number.isSafeInteger(generation)) throw new Error('generation exhausted')
371
- const active = !session.interrupted && origin.executionEpoch === session.executionEpoch
372
- locks.set(resourceId, { resourceId, owner: session.sessionId, generation,
373
- status: active ? 'active' : 'user-interrupted' })
374
- generations.set(resourceId, generation)
375
- // A successful creation starts a batch when the session had none yet.
376
- if (!holds.has(session.sessionId)) holds.set(session.sessionId, { sessionId: session.sessionId, holding: false, holdUntil: null, holdCumulativeMs: 0 })
377
- return { managerIncarnation, sessionId: session.sessionId, executionEpoch: session.executionEpoch, resourceId, generation }
378
- },
379
- /** Detached historical core, not a permission or a restore credential.
380
- * @param {object} [draft] */
381
- checkpoint(draft) {
382
- if (draft) return draftFor(draft).child.authority.checkpoint()
383
- return {
384
- ...operations.status(),
385
- generations: [...generations].map(([resourceId, generation]) => ({ resourceId, generation })),
386
- issuedRequests: [...issuedRequests].flatMap(([sessionId, requests]) =>
387
- [...requests].map(requestId => ({ sessionId, requestId }))),
388
- }
389
- },
390
- /** Register a trusted initial execution, never rearm an existing session.
391
- * @param {string} sessionId @returns {Execution} */
392
- openSession(sessionId) {
393
- requireId(sessionId)
394
- if (sessions.has(sessionId)) throw new Error('session already registered')
395
- sessions.set(sessionId, { sessionId, executionEpoch: 1, interrupted: false })
396
- holds.set(sessionId, { sessionId, holding: false, holdUntil: null, holdCumulativeMs: 0 })
397
- return { managerIncarnation, sessionId, executionEpoch: 1 }
398
- },
399
- /** Revoke one trusted execution; the returned epoch allows cleanup, not edits.
400
- * @param {Execution} execution @returns {Execution} */
401
- cancel(execution) {
402
- const session = sessionFor(execution)
403
- session.executionEpoch += 1
404
- session.interrupted = true
405
- for (const lock of locks.values()) {
406
- if (lock.owner === session.sessionId && lock.status !== 'abnormal') lock.status = 'user-interrupted'
407
- }
408
- // Precedence (spec: Lock lifecycle disclosure): a stopped session's locks are
409
- // handled as abnormal locks, so no retention period applies to them any more.
410
- retention.endHold(session.sessionId)
411
- return { managerIncarnation, sessionId: session.sessionId, executionEpoch: session.executionEpoch }
412
- },
413
- /** Retain a trusted abnormal classification; no automatic recovery is provided.
414
- * @param {Ownership} token @param {string} reason */
415
- markAbnormal(token, reason) {
416
- const lock = lockFor(token)
417
- requireId(reason)
418
- lock.status = 'abnormal'
419
- lock.reason = reason
420
- },
421
- /** Trusted explicit-intent boundary, NOT a model-callable tool or classifier.
422
- * Call only after authenticating a human execution request outside this kernel.
423
- * Receipts are process-local object capabilities; copies/JSON cannot redeem them.
424
- * @param {Execution} execution @param {string} requestId @returns {object} */
425
- issueExecutionReceipt(execution, requestId) {
426
- sessionFor(execution)
427
- requireId(requestId)
428
- const issued = issuedRequests.get(execution.sessionId) ?? new Set()
429
- if (issued.has(requestId)) throw new Error('request already issued')
430
- issued.add(requestId)
431
- issuedRequests.set(execution.sessionId, issued)
432
- const receipt = Object.freeze({})
433
- receipts.set(receipt, { ...execution, requestId })
434
- return receipt
435
- },
436
- }
437
- // Advance even on rejection: conservatively invalidate outstanding candidates.
438
- /** @param {Record<string, (...args: any[]) => any>} facet @param {string[]} names */
439
- function track(facet, names) {
440
- for (const name of names) {
441
- const method = facet[name]
442
- /** @param {...any} args */
443
- facet[name] = (...args) => { revision += 1; return method(...args) }
444
- }
445
- }
446
- // Retention lives with the operations facet: both answer "may this session do
447
- // this now" against the same core. `holdState`/`settleHold` are reads and stay
448
- // callable anywhere (including a draft); the mutations advance the revision so
449
- // an outstanding draft is invalidated rather than silently stale.
450
- Object.assign(operations, retention)
451
- track(operations, ['acquire', 'release', 'resume', 'hold', 'endHold', 'endBatch'])
452
- track(authority, ['openSession', 'cancel', 'markAbnormal', 'issueExecutionReceipt', 'settleCreated'])
453
- const kernel = { operations: Object.freeze(operations), authority: Object.freeze(authority) }
454
- cores.set(kernel, { sessions, locks, generations, issuedRequests, receipts, holds })
455
- return Object.freeze(kernel)
456
- }
457
-
458
- /** Validate opaque identifiers without interpreting or normalizing paths.
459
- * @param {string} value */
460
- function requireId(value) {
461
- if (typeof value !== 'string' || value.length === 0) throw new Error('invalid identifier')
462
- }