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,312 +0,0 @@
1
- /** Per-agent binding state machine (change edit-lock-deepening, design D3):
2
- * setupAgentOnce / setupAgent / retryBinding / disposeAgent / startupFailure /
3
- * dispositionFor and the closure state they share, extracted from the
4
- * composition (index.js), which now keeps pure wiring: tool registration, the
5
- * pre-execute guard, HTTP endpoints, the /edit-lock command, drivers and event
6
- * listeners. The move is mechanical — no logic changed; every container keeps
7
- * exactly the lifetime it had in the mount closure.
8
- *
9
- * Lifetime table (itemized BEFORE the move; the move changed nothing in it):
10
- *
11
- * state lifetime home after the move
12
- * ------------------ ------------- --------------------------------------
13
- * settled cross-mount sharedDomainRegistryFor entry (existing
14
- * precedent, with recoveryNotices); INJECTED, not owned
15
- * writeScopes host-lifetime writeScopesFor(host) store, surviving a
16
- * preset rebind; INJECTED — the preset-rebind survival
17
- * semantics and the `ctx.root ?? ctx` host key are
18
- * unchanged
19
- * disposedAgents per-mount this module (WeakSet)
20
- * startFailures per-mount this module (WeakMap)
21
- * bindingFailures per-mount this module (WeakMap)
22
- * setupInFlight per-mount this module (WeakMap)
23
- * boundAgents per-mount this module (Map by session id)
24
- * dispositions per-mount this module (Map by session id; the
25
- * latest incarnation stays joinable after synchronous
26
- * cleanup)
27
- * agentDispositions per-mount this module (WeakMap)
28
- * disposalTasks per-mount this module (WeakMap)
29
- * pendingAutoResume per-mount composition turn wiring; INJECTED (a
30
- * successful retry clears an armed auto-resume)
31
- * delegateResumes per-mount composition delegate-lifecycle wiring;
32
- * INJECTED (a fresh bind replays a pending resume)
33
- * turnClassifications per-mount composition turn/end listener; INJECTED
34
- * (dispose awaits the durable classification first)
35
- * replyTools per-mount composition negotiation wiring;
36
- * INJECTED (dispose retires the reply tool)
37
- * expiry per-mount composition retention-timer wiring;
38
- * INJECTED (dispose clears the timer)
39
- *
40
- * Cross-mount state rides the EXISTING sharedDomainRegistryFor entry — no new
41
- * cross-mount container is introduced. Mount-generation flags arrive as
42
- * getters (isClosed/platformFailure) so the machine reads the live value.
43
- * @param {{
44
- * ctx: any, sessionOf: (agent: any) => string | undefined, GUARDED_TOOLS: readonly string[],
45
- * options: any, registry: any, settled: WeakMap<object, any>, writeScopes: WeakMap<object, import('./write-scopes.js').WriteScopeEntry>,
46
- * domainOf: (agent: any) => Promise<any>, servingRuntime: (sessionId: string) => any,
47
- * ownAgent: (agent: any) => boolean, installWriteScope: (agent: any) => import('./write-scopes.js').WriteScopeEntry,
48
- * evidence: any, audit: (session: any, type: any, data: any, options?: any) => void, deliverLocal: (agent: any, text: string, wake?: boolean) => void,
49
- * pendingAutoResume: WeakMap<object, boolean>, delegateResumes: Map<string, any>, turnClassifications: WeakMap<object, Promise<unknown>>,
50
- * replyTools: WeakMap<object, {dispose: () => void, stale: boolean}>, expiry: WeakMap<object, {timer?: any}>, settle: any,
51
- * isClosed: () => boolean, platformFailure: () => any,
52
- * }} deps
53
- */
54
- import { AUDIT_TYPES } from '../shared/audit.js'
55
- import { disposeWriteScope } from './write-scopes.js'
56
-
57
- /** In-session binding-failure notice copy (change edit-lock-binding-transfer,
58
- * design D4): the transferable class (same-process idle holder) is a calm
59
- * self-heal announcement rather than an editing-disabled alarm and never
60
- * carries the raw holder classification or the reservation warning; a mid-turn
61
- * holder names the real state and the retry moment. Template layer stays
62
- * English (AGENTS §3.7). @param {string} reason @param {any} failure */
63
- function bindingFailureNotice(reason, failure) {
64
- if (failure?.code === 'binding-duplicate' && failure.holderKind === 'transferable') {
65
- return "Edit Lock: this session's editing authority is held by an inactive attachment of this same session; it transfers automatically when you send a message."
66
- }
67
- if (failure?.code === 'binding-duplicate' && failure.holderKind === 'serving') {
68
- return "Edit Lock: this session is being edited in another attachment right now; managed file editing stays unavailable until that turn finishes. Do not retry the failed edit; use /edit-lock retry (or the panel's Retry connection) once it finishes — retry does not Continue editing."
69
- }
70
- return `Edit Lock could not start this session's edit domain (${reason}); managed file editing is temporarily unavailable. ${failure?.code === 'binding-duplicate' ? 'Do not retry the failed edit: the binding is retried automatically when the user next sends a message, after which editing works normally. Never remove a live publisher reservation.' : 'Inspect Edit Lock maintenance before continuing.'}`
71
- }
72
-
73
- export function createBindingMachine(deps) {
74
- const {
75
- ctx, sessionOf, GUARDED_TOOLS, options, registry, settled, writeScopes,
76
- domainOf, servingRuntime, ownAgent, installWriteScope,
77
- evidence, audit, deliverLocal,
78
- pendingAutoResume, delegateResumes, turnClassifications, replyTools, expiry, settle,
79
- isClosed, platformFailure,
80
- } = deps
81
- /** Agents disposed in this mount; their undispatched sweep work cancels.
82
- * @type {WeakSet<object>} */
83
- const disposedAgents = new WeakSet()
84
- /** Why registration failed for an agent, for the panel. @type {WeakMap<object, string>} */
85
- const startFailures = new WeakMap()
86
- /** Publisher-classified failures, never inferred from text. @type {WeakMap<object, any>} */
87
- const bindingFailures = new WeakMap()
88
- /** In-flight setup per agent, shared by every entry point (creation,
89
- * preset-switch, catch-up, lazy re-bind) so concurrent callers can never
90
- * double-register. @type {WeakMap<object, Promise<void>>} */
91
- const setupInFlight = new WeakMap()
92
- const boundAgents = new Map()
93
- // Keep the latest incarnation joinable even after synchronous binding cleanup.
94
- // Exact-handle waiters retain their own record when a session activates again.
95
- const dispositions = new Map()
96
- const agentDispositions = new WeakMap()
97
- const disposalTasks = new WeakMap()
98
-
99
- function dispositionFor(agent) {
100
- let record = agentDispositions.get(agent)
101
- if (!record) {
102
- record = Promise.withResolvers()
103
- record.promise.catch(() => {}) // emit initiators have no awaiting caller
104
- agentDispositions.set(agent, record)
105
- dispositions.set(agent.id, record)
106
- }
107
- return record
108
- }
109
-
110
- /** Only this live Agent's diagnostic; no domain reads or startup side effects. @param {any} agent */
111
- function startupFailure(agent) {
112
- const platform = platformFailure()
113
- if (platform) return { state: 'unavailable', reason: platform.message, bindingFailure: null }
114
- if (!agent || settled.has(agent) || !startFailures.has(agent)) return null
115
- let eligible = false
116
- try {
117
- const serving = servingRuntime(agent.id)
118
- eligible = !isClosed() && !disposedAgents.has(agent) && ownAgent(agent)
119
- && (serving === agent
120
- // The registry names ANOTHER live local agent — the dual-agent defect
121
- // this change heals (edit-lock-binding-transfer). The recorded
122
- // duplicate-binding failure is still THIS agent's honest panel state:
123
- // show it, with its Retry action, instead of hiding behind the ghost's
124
- // healthy binding. An empty registry stays hidden (unattestable).
125
- || (serving !== undefined && bindingFailures.get(agent)?.code === 'binding-duplicate'))
126
- } catch { /* no trusted live identity */ }
127
- if (!eligible) return null
128
- const failure = bindingFailures.get(agent)
129
- return { state: 'unavailable', reason: startFailures.get(agent),
130
- bindingFailure: failure ? { ...failure, inFlight: setupInFlight.has(agent) } : null }
131
- }
132
-
133
- /** Per-agent Edit Lock setup, shared by the creation listener, the
134
- * preset-switch re-bind, the late-mount catch-up and the guard/acquire lazy
135
- * re-bind, so the paths can never diverge. Root selection and successful
136
- * startup are separate: explicit binding retry reuses the existing scope
137
- * and root, while ordinary entry points never repeat a failed startup.
138
- * @param {any} agent @param {boolean} [retry] @param {boolean} [transfer]
139
- * transfer (change edit-lock-binding-transfer) marks the driving-ingress
140
- * transfer admission: the domain's transferStart re-adjudicates holder
141
- * idleness at execution, and the post-bind registry-identity check is
142
- * skipped because the registry legitimately still names the idle holder. */
143
- async function setupAgentOnce(agent, retry = false, transfer = false) {
144
- if (isClosed() || settled.has(agent)) return
145
- // No writer scope, root resolution, directory, reservation or session
146
- // authority may be installed when the mount preflight failed.
147
- if (platformFailure()) return
148
- if (!registry.rootOf(agent)) try {
149
- // A previous generation's managed write survives in the agent's own
150
- // scope layer (a preset rebind never disposes own-layer registrations);
151
- // retire it first or the scope registry throws duplicate-registration
152
- // and the re-bind dies here (design D3).
153
- disposeWriteScope(writeScopes, agent)
154
- writeScopes.set(agent, installWriteScope(agent))
155
- registry.bind(agent, options.fixed?.root ?? agent?.session?.header?.cwd)
156
- } catch (error) {
157
- // The denial itself must never escape: restrict() throws on names the
158
- // agent's composition does not know, and an escape from this setup path
159
- // is fiber-fatal during a reload (2026-10-05 host crash). Deny only the
160
- // guarded tools the agent actually has.
161
- try {
162
- const known = GUARDED_TOOLS.filter(name => ctx.tools?.get?.(name, agent) !== undefined)
163
- if (known.length > 0) agent?.ctx?.tools?.restrict?.({ deny: [...known] })
164
- } catch { /* the denial is best-effort; the records below are the visible account */ }
165
- // Failure visibility (design D4): the same reason lands in the panel
166
- // state, the maintenance evidence, an in-session notice and the durable
167
- // audit; the warn stays as the host ring-buffer copy.
168
- const reason = String(/** @type {any} */ (error)?.message ?? error)
169
- startFailures.set(agent, reason)
170
- try { evidence?.recordSessionFailure?.(sessionOf(agent), reason) } catch { /* reporting only */ }
171
- audit(agent?.session ?? null, AUDIT_TYPES.editLockMaintenance, { kind: 'setup-failed', sessionId: sessionOf(agent) ?? null, reason })
172
- try { deliverLocal(agent, `Edit Lock is unavailable for this session (${reason}); managed file editing is disabled and the failed edit must not be retried. This failure is not repaired automatically: the user can restart DeepSeek Harness or start a new conversation.`) } catch { /* the audit record is the durable account */ }
173
- ctx.logger?.warn?.(`edit lock scope failed; edit tools denied: ${reason}`)
174
- return
175
- }
176
- try {
177
- const domain = await domainOf(agent)
178
- if (transfer) await domain.transferStart(agent)
179
- else if (retry) await domain.retryStart(agent)
180
- else await domain.start(agent)
181
- if (isClosed() || disposedAgents.has(agent) || (retry && (!ownAgent(agent) || (!transfer && servingRuntime(agent.id) !== agent)))) {
182
- await domain.forget(agent)
183
- throw new Error('Edit Lock mount changed during binding')
184
- }
185
- startFailures.delete(agent)
186
- bindingFailures.delete(agent)
187
- try { evidence?.clearSessionFailure?.(sessionOf(agent)) } catch { /* reporting cannot undo a successful bind */ }
188
- // A genuine message from a turn that ran while binding was unavailable
189
- // is not a Continue receipt for a later injected wake after connection retry.
190
- if (retry) pendingAutoResume.delete(agent)
191
- settled.set(agent, domain)
192
- boundAgents.set(agent.id, agent)
193
- dispositionFor(agent)
194
- if (!retry) await delegateResumes.get(agent.id)?.resume(agent, domain)
195
- } catch (error) {
196
- // Kept so the panel can say WHY editing is unavailable instead of
197
- // showing "starting" forever; the notice aligns this catch with the
198
- // write-scope failure above.
199
- const reason = String(/** @type {any} */ (error)?.message ?? error)
200
- startFailures.set(agent, reason)
201
- const failure = /** @type {any} */ (error)?.bindingFailure
202
- if (failure?.code === 'binding-duplicate') bindingFailures.set(agent, failure)
203
- else bindingFailures.delete(agent)
204
- try { evidence?.recordSessionFailure?.(sessionOf(agent), reason) } catch { /* reporting only */ }
205
- try { deliverLocal(agent, bindingFailureNotice(reason, failure)) } catch { /* the panel record remains */ }
206
- ctx.logger?.warn?.(`edit lock registration failed for ${sessionOf(agent)}: ${reason}`)
207
- }
208
- }
209
- /** Concurrent entry points (agent/created, preset-switch, lazy re-bind)
210
- * share one in-flight setup per agent; a completed setup short-circuits on
211
- * the root binding. @param {any} agent @returns {Promise<void>} */
212
- function setupAgent(agent) {
213
- const pending = setupInFlight.get(agent)
214
- if (pending) return pending
215
- if (isClosed() || settled.has(agent) || registry.rootOf(agent)) return Promise.resolve()
216
- const inner = setupAgentOnce(agent)
217
- const task = inner.finally(() => { if (setupInFlight.get(agent) === task) setupInFlight.delete(agent) })
218
- setupInFlight.set(agent, task)
219
- return task
220
- }
221
- /** Trusted driving ingresses only: the explicit /edit-lock retry (and panel
222
- * action), the message-armed pre-step retry, and the write-guard lazy
223
- * re-bind. Same Agent/root/mount; no writer reinstallation. Every attempt
224
- * past the eligibility gate is audited — a silently swallowed refusal is a
225
- * defect (change edit-lock-binding-transfer, design D3/D5).
226
- * @param {any} agent @param {'command' | 'message' | 'write-guard'} [via] */
227
- async function retryBinding(agent, via = 'command') {
228
- if (isClosed() || disposedAgents.has(agent) || !ownAgent(agent)) throw new Error('Edit Lock retry requires the current live Agent of this mount')
229
- if (!registry.rootOf(agent) || bindingFailures.get(agent)?.code !== 'binding-duplicate') throw new Error('Edit Lock startup failure is not eligible for binding retry')
230
- const cause = bindingFailures.get(agent)
231
- const sessionId = sessionOf(agent) ?? null
232
- const root = registry.rootOf(agent) ?? undefined
233
- /** @param {string} result @param {Record<string, unknown>} [extra] */
234
- const record = (result, extra = {}) => audit(agent?.session ?? null, AUDIT_TYPES.editLockMaintenance, {
235
- kind: 'binding-transfer', sessionId, code: cause?.code ?? 'binding-duplicate', result, ...extra,
236
- }, { root })
237
- // Attestation trichotomy (design D5): the registry naming THIS claimant
238
- // keeps the existing superseded rebind; naming ANOTHER live local agent
239
- // routes to the transfer admission (holder idleness is re-adjudicated at
240
- // execution, publisher-side); an unavailable or empty registry stays
241
- // fail-closed — the holder cannot be attested.
242
- let serving
243
- try { serving = servingRuntime(agent.id) } catch { serving = undefined }
244
- if (serving === undefined) {
245
- const error = new Error('Edit Lock binding retry refused: the agent registry is unavailable or names no live agent for this session, so the holder cannot be attested. The recorded cause stays; a later retry remains possible.')
246
- record('refused', { reason: 'registry-unattestable', detail: error.message })
247
- throw error
248
- }
249
- const transfer = serving !== agent
250
- try {
251
- let task = setupInFlight.get(agent)
252
- if (!task) {
253
- const inner = setupAgentOnce(agent, true, transfer)
254
- task = inner.finally(() => { if (setupInFlight.get(agent) === task) setupInFlight.delete(agent) })
255
- setupInFlight.set(agent, task)
256
- }
257
- await task
258
- } catch (error) {
259
- record('refused', { reason: /** @type {any} */ (error)?.bindingFailure?.holderKind ?? 'retry-refused', detail: String(/** @type {any} */ (error)?.message ?? error) })
260
- throw error
261
- }
262
- const failure = startFailures.get(agent)
263
- if (failure || !settled.has(agent)) {
264
- // setupAgentOnce records instead of rethrowing: the refusal's holder
265
- // class comes from the freshly re-recorded structured failure.
266
- const error = new Error(failure ?? 'Edit Lock binding remains unavailable')
267
- record('refused', { reason: bindingFailures.get(agent)?.holderKind ?? 'binding-unavailable', detail: error.message })
268
- throw error
269
- }
270
- if (transfer) {
271
- record('transferred', { holder: cause?.holderKind ?? null })
272
- } else if (via === 'message') {
273
- // The message-armed superseded success keeps its established audit shape
274
- // (pinned by the editlock-binding-auto-retry integration scenario).
275
- audit(agent?.session ?? null, AUDIT_TYPES.editLockMaintenance, {
276
- kind: 'binding-auto-retry', sessionId, code: cause?.code ?? 'binding-duplicate', result: 'recovered',
277
- }, { root })
278
- }
279
- return 'Edit Lock connection recovered. Editing remains interrupted; use Continue editing when ready. No failed edit was replayed.'
280
- }
281
- function disposeAgent(agent) {
282
- if (disposedAgents.has(agent)) return disposalTasks.get(agent) ?? Promise.resolve()
283
- if (boundAgents.get(agent.id) === agent) boundAgents.delete(agent.id)
284
- // Undispatched sweep work armed by this agent cancels here (an in-flight
285
- // authority transaction is never cancelled or reinterpreted).
286
- disposedAgents.add(agent)
287
- // The managed write lives on the agent's own scope layer, which dies with
288
- // the agent: retire the host-lifetime registration here (design D3).
289
- disposeWriteScope(writeScopes, agent)
290
- replyTools.get(agent)?.dispose()
291
- replyTools.delete(agent)
292
- const timer = expiry.get(agent)?.timer
293
- if (timer) clearTimeout(timer)
294
- expiry.delete(agent)
295
- settle.settled(agent)
296
- const domain = settled.get(agent)
297
- settled.delete(agent)
298
- // The emit hook must cancel timers/scopes/sweeps synchronously. Only the
299
- // final authority disposal waits for durable abnormal classification.
300
- const record = dispositionFor(agent)
301
- const task = Promise.resolve(turnClassifications.get(agent)).then(() => domain?.dispose(agent))
302
- task.then(record.resolve, record.reject)
303
- disposalTasks.set(agent, record.promise)
304
- return record.promise
305
- }
306
-
307
- return {
308
- disposedAgents, startFailures, bindingFailures, setupInFlight,
309
- boundAgents, dispositions, agentDispositions,
310
- setupAgent, retryBinding, disposeAgent, startupFailure,
311
- }
312
- }
@@ -1,65 +0,0 @@
1
- import { isAbsolute } from 'node:path'
2
- import { canonicalRequestData } from './request-data.js'
3
-
4
- /** Private process-local association, not a durable authority ledger.
5
- * Only a trusted manager may bind calls; consumers must still authenticate the
6
- * captured execution against current durable authority before every mutation. */
7
- export function createCallContexts() {
8
- /** @typedef {{execution: import('./state.js').Execution, cwd: string, effectivePolicy: any, callId: string, signal: AbortSignal}} Context */
9
- /** @type {WeakMap<object, {context: Context, closed: boolean, abort: () => void, detach: () => void}>} */
10
- const calls = new WeakMap()
11
- /** @param {object} call */
12
- function entry(call) {
13
- const value = calls.get(call)
14
- if (!value) throw new Error('unknown call')
15
- if (value.closed) throw new Error('closed call')
16
- return value
17
- }
18
- /** @param {any} data */
19
- function freeze(data) {
20
- if (data && typeof data === 'object') {
21
- for (const value of Object.values(data)) freeze(value)
22
- Object.freeze(data)
23
- }
24
- return data
25
- }
26
- return Object.freeze({
27
- /** @param {import('./state.js').Execution} execution
28
- * @param {{signal: AbortSignal, cwd: string, effectivePolicy: unknown, callId: string}} options
29
- * @param {(execution: import('./state.js').Execution) => void} onAbort */
30
- bind(execution, { signal, cwd, effectivePolicy, callId }, onAbort) {
31
- if (!isAbsolute(cwd) || cwd.includes('\0') || typeof callId !== 'string' || !callId.trim()) throw new Error('invalid call context')
32
- const captured = freeze(JSON.parse(canonicalRequestData({ execution, cwd, effectivePolicy, callId })))
33
- const controller = new AbortController()
34
- const call = Object.freeze({})
35
- let notified = false
36
- const abort = () => {
37
- if (notified) return
38
- notified = true
39
- try { onAbort(captured.execution) } finally { controller.abort() }
40
- }
41
- calls.set(call, { context: Object.freeze({ ...captured, signal: controller.signal }), closed: false, abort,
42
- detach: () => signal.removeEventListener('abort', abort) })
43
- signal.addEventListener('abort', abort, { once: true })
44
- if (signal.aborted) abort()
45
- return call
46
- },
47
- /** @param {object} call */
48
- inspect(call) { return entry(call).context },
49
- /** @param {object} call */
50
- requireLive(call) {
51
- const context = entry(call).context
52
- if (context.signal.aborted) throw new Error('call aborted')
53
- return context
54
- },
55
- /** Trusted manager revocation shares the host-abort denial path.
56
- * @param {object} call */
57
- revoke(call) { entry(call).abort() },
58
- /** @param {object} call */
59
- close(call) {
60
- const value = entry(call)
61
- value.closed = true
62
- value.detach()
63
- },
64
- })
65
- }
@@ -1,67 +0,0 @@
1
- /** Ledger derived facts (change edit-lock-deepening, design D1): the three
2
- * derivation rules an authority image supports — epoch-scoped revocation
3
- * (`revokedOwner`), read-time hold settlement (`settleHoldFacts`) and the
4
- * administrative disposition lookup (`dispositionFor`) — in ONE pure home,
5
- * shared by the manager status path, the kernel's settleHold, the cold view
6
- * and the maintenance inspector. Pure: no IO, no clock, no ctx, no imports.
7
- *
8
- * Each rule takes a `partial` option for the cold read (buildColdView): the
9
- * panel is deliberately more tolerant than the kernel — a partial image (a
10
- * missing collection, a malformed row) degrades to "not revoked / no hold"
11
- * instead of throwing on data the kernel would already have rejected at parse
12
- * time. The strict default is byte-identical to the kernel/ledger rules the
13
- * call sites previously inlined or imported. */
14
-
15
- /**
16
- * Epoch-scoped revocation: a disposition revokes its owner's execution
17
- * authority exactly at the recorded revokedEpoch; the owner stays revoked
18
- * while its recorded epoch is within it, and a trusted resume advancing the
19
- * epoch past every revokedEpoch ends the revoked state without any ledger
20
- * change (append-only: dispositions are never removed or altered). Extracted
21
- * from admin-ledger.js; the cold view applied the same rule inline.
22
- * @param {any} state the authority image (or the in-memory status state)
23
- * @param {string} owner
24
- * @param {{ partial?: boolean }} [options] partial tolerates a partial image:
25
- * missing collections read as empty and malformed rows are skipped; the
26
- * strict default trusts the validated image.
27
- */
28
- export function revokedOwner(state, owner, { partial = false } = {}) {
29
- if (partial) {
30
- const sessions = Array.isArray(state?.sessions) ? state.sessions : []
31
- const session = sessions.find(row => row?.sessionId === owner) ?? null
32
- if (session == null) return false
33
- const ledger = Array.isArray(state?.adminRecoveries) ? state.adminRecoveries : []
34
- return ledger.some(row => row?.owner === owner && Number.isSafeInteger(row?.revokedEpoch) && session.executionEpoch <= row.revokedEpoch)
35
- }
36
- const session = state.sessions?.find(s => s.sessionId === owner)
37
- if (!session) return false
38
- return state.adminRecoveries?.some(row => row.owner === owner && session.executionEpoch <= row.revokedEpoch) === true
39
- }
40
-
41
- /**
42
- * Read-time retention settlement (design D5), byte-identical to the kernel's
43
- * settleHold rule it was extracted from (state.js): a hold counts only while
44
- * the session owns a lock, and the remaining budget is computed against the
45
- * read instant, so a missed expiry timer can never leave stale ownership
46
- * behind a status read. The caller keeps its container lookup and its empty
47
- * case (the kernel's zero row vs the cold view's null): `held` is the
48
- * session's PRESENT hold row, `owns` whether the session owns any lock, `now`
49
- * the read instant.
50
- * @param {string} sessionId @param {any} held @param {boolean} owns @param {number} now
51
- * @param {{ partial?: boolean }} [options] partial keeps the cold path's
52
- * defensive tolerance of a partial image: an undefined holdUntil reads as no
53
- * expiry and only a literal `holding === true` counts.
54
- */
55
- export function settleHoldFacts(sessionId, held, owns, now, { partial = false } = {}) {
56
- const until = held.holdUntil
57
- const remainingMs = (partial ? until == null : until === null) ? 0 : Math.max(0, until - now)
58
- const holding = (partial ? held.holding === true : held.holding) && owns
59
- return { sessionId, held: holding, expired: holding && until !== null && remainingMs === 0,
60
- holdUntil: until, remainingMs, holdCumulativeMs: held.holdCumulativeMs }
61
- }
62
-
63
- /**
64
- * The administrative disposition covering one operation, if any. Extracted
65
- * verbatim from admin-ledger.js; the maintenance inspector's inline copy was
66
- * identical (inspect.js). @param {any} state @param {any} op */
67
- export const dispositionFor = (state, op) => state.adminRecoveries?.find(row => row.owner === op.sessionId && row.operations.some(item => item.operationId === op.operationId))
@@ -1,165 +0,0 @@
1
- /** One interface for the publisher-local and remote-client compositions.
2
- * Every method is trusted host ingress; only `service` reaches tools. */
3
-
4
- /** @param {ReturnType<typeof import('./lifecycle.js').createEditLockLifecycle>} lifecycle
5
- * @param {{close: () => Promise<void>, startLocal?: (agent: object, retry?: boolean) => Promise<any>} | undefined} endpoint */
6
- export function localDomain(lifecycle, endpoint) {
7
- return Object.freeze({
8
- mode: 'publisher',
9
- service: lifecycle.service,
10
- /** @param {object} agent */
11
- start: agent => endpoint?.startLocal ? endpoint.startLocal(agent) : lifecycle.start(agent),
12
- /** Trusted binding retry; binds a revoked session as interrupted without
13
- * restoring authority (the trusted resume path does that). @param {object} agent */
14
- retryStart: async agent => {
15
- lifecycle.assertStartable(agent)
16
- return endpoint?.startLocal ? endpoint.startLocal(agent, true) : lifecycle.start(agent)
17
- },
18
- /** Trusted binding transfer (change edit-lock-binding-transfer): the
19
- * explicit driving-ingress counterpart of retryStart — the holder-side
20
- * admission (local, registry-named, provably idle holder) is re-adjudicated
21
- * at execution and refuses exactly like start() otherwise. @param {object} agent */
22
- transferStart: async agent => {
23
- lifecycle.assertStartable(agent)
24
- return endpoint?.startLocal ? endpoint.startLocal(agent, true, true) : lifecycle.transferStart(agent)
25
- },
26
- /** Synchronous admission fence for an aborted turn. @param {object} agent */
27
- latch: agent => { lifecycle.stop(agent).catch(() => {}) },
28
- /** @param {object} agent */
29
- stop: async agent => { await lifecycle.stop(agent); return lifecycle.status(agent) },
30
- /** @param {object} agent */
31
- dispose: agent => lifecycle.dispose(agent),
32
- /** Revoke and remove all ingress, retaining locks. @param {object} agent */
33
- forget: agent => lifecycle.forget(agent),
34
- /** @param {object} agent @param {string} requestId */
35
- resume: (agent, requestId) => lifecycle.resume(agent, requestId),
36
- /** @param {object} agent @param {string} resourceId */
37
- confirm: async (agent, resourceId) => { await lifecycle.confirm(agent, resourceId); return lifecycle.status(agent) },
38
- /** @param {object} agent */
39
- status: async agent => lifecycle.status(agent),
40
- /** @param {object} _agent */
41
- locks: async _agent => lifecycle.locks(),
42
- /** @param {object} _agent @param {string} resourceId @param {number} generation */
43
- unlock: (_agent, resourceId, generation) => lifecycle.unlock(resourceId, generation),
44
- /** Online administrative recovery, executed publisher-side. The trigger
45
- * session is audit metadata supplied by the trusted caller, never read
46
- * from the request payload.
47
- * @param {object} _agent @param {any} input @param {string | null} [trigger] */
48
- adminRecover: (_agent, input, trigger) => lifecycle.adminRecoverOnline(input, trigger ?? null),
49
- /** Detached stale-lock sweep trigger; the scan runs publisher-side.
50
- * @param {object} _agent @param {string | null} [triggerSessionId] */
51
- sweepStale: (_agent, triggerSessionId) => lifecycle.sweepStale(triggerSessionId ?? null),
52
- /** @param {object} agent @param {string} reason */
53
- classifyAbnormal: (agent, reason) => lifecycle.classifyAbnormal(agent, reason),
54
- /** @param {object} agent */
55
- recoveryUsage: async agent => lifecycle.recoveryUsage(agent),
56
- /** @param {object} agent @param {any} delta */
57
- chargeRecovery: (agent, delta) => lifecycle.chargeRecovery(agent, delta),
58
- /** @param {object} agent @param {number} minutes */
59
- pause: (agent, minutes) => lifecycle.pause(agent, minutes),
60
- /** @param {object} agent @param {number} ms @param {{singleMaxMs: number, cumulativeMaxMs: number}} caps */
61
- hold: (agent, ms, caps) => lifecycle.hold(agent, ms, caps),
62
- /** The holder started a new turn: every held lock leaves holding at once.
63
- * @param {object} agent */
64
- turnStarted: agent => lifecycle.turnStarted(agent),
65
- /** Release this session's active locks at their current generation, the
66
- * disposition used when a finished turn's locks were never settled.
67
- * @param {object} agent */
68
- releaseHeld: agent => lifecycle.settleExpired(agent, true),
69
- /** Read-time settlement plus the release half of an elapsed period. Also the
70
- * trigger for the expiry timer. @param {object} agent */
71
- settleExpired: (agent, idle) => lifecycle.settleExpired(agent, false, idle),
72
- /** @param {object} agent */
73
- retention: agent => lifecycle.retention(agent),
74
- blocks(agent) { try { return lifecycle.status(agent).state !== 'active' } catch { return true } },
75
- async close() {
76
- // Seal remote admission and revoke its channels before local shutdown.
77
- try { await endpoint?.close() } finally { await lifecycle.close() }
78
- },
79
- })
80
- }
81
-
82
- /** @param {ReturnType<typeof import('./remote.js').createRemoteEditLockDomain>} remote */
83
- export function remoteDomain(remote) {
84
- /** @param {string} kind */
85
- const forward = kind => (/** @type {any} */ exec, /** @type {unknown} */ request) =>
86
- remote.call(exec.agent, kind, request, exec.signal, kind === 'publish' || kind === 'batch' ? exec.callId : undefined)
87
- return Object.freeze({
88
- mode: 'client',
89
- service: Object.freeze({
90
- publish: forward('publish'), publishBatch: forward('batch'), acquire: forward('acquire'), release: forward('release'),
91
- locks: (/** @type {any} */ exec) => remote.call(exec.agent, 'locks'), trySteal: forward('trySteal'), reply: forward('reply'),
92
- }),
93
- /** @param {object} agent */
94
- start: async agent => (await remote.channelFor(agent)).state,
95
- /** @param {object} agent */
96
- retryStart: async agent => (await remote.channelFor(agent, true)).state,
97
- /** The wire retry flag carries the transfer admission to the publisher
98
- * (its lifecycle re-adjudicates holder idleness); the client adds nothing. */
99
- /** @param {object} agent */
100
- transferStart: async agent => (await remote.channelFor(agent, true)).state,
101
- /** @param {object} agent */
102
- forget: async agent => { try { await remote.call(agent, 'stop') } finally { remote.drop(agent) } },
103
- /** Closing the channel makes the publisher revoke on EOF. @param {object} agent */
104
- latch: agent => remote.drop(agent),
105
- /** Awaits the publisher's durable revocation. @param {object} agent */
106
- stop: async agent => { const status = await remote.call(agent, 'stop'); remote.setState(agent, 'stopped'); return status },
107
- /** @param {object} agent */
108
- dispose: async agent => {
109
- // The publisher releases this agent's active locks and acknowledges with
110
- // the response; the channel close below is the transport-level fallback
111
- // (EOF also revokes). An unacknowledged disposition must reach the
112
- // awaiting caller: this method rejects with the request's own failure
113
- // while the channel is still dropped exactly once.
114
- try { await remote.call(agent, 'dispose') } finally { remote.drop(agent) }
115
- },
116
- /** @param {object} agent @param {string} requestId */
117
- resume: async (agent, requestId) => {
118
- const status = await remote.call(agent, 'resume', { requestId })
119
- remote.setState(agent, status.state)
120
- return status
121
- },
122
- /** @param {object} agent @param {string} resourceId */
123
- confirm: (agent, resourceId) => remote.call(agent, 'confirm', { resourceId }),
124
- /** @param {object} agent */
125
- status: agent => remote.call(agent, 'status'),
126
- /** @param {object} agent */
127
- locks: agent => remote.call(agent, 'allLocks'),
128
- /** @param {object} agent @param {string} resourceId @param {number} generation */
129
- unlock: (agent, resourceId, generation) => remote.call(agent, 'unlock', { resourceId, generation }),
130
- /** Forward the operator's recovery request through this agent's channel;
131
- * the publisher executes the same transaction and takes the trigger
132
- * identity from the channel, never from the payload.
133
- * @param {object} agent @param {any} input */
134
- adminRecover: (agent, input) => remote.call(agent, 'adminRecover', input),
135
- /** The publisher performs the scan (it shares the filesystem); a client
136
- * process only triggers it through the channel. @param {object} agent
137
- * @param {string | null} [triggerSessionId] */
138
- sweepStale: (agent, triggerSessionId) => remote.call(agent, 'staleSweep', { trigger: triggerSessionId ?? agent?.id ?? null }),
139
- /** @param {object} agent @param {string} reason */
140
- classifyAbnormal: async (agent, reason) => {
141
- const marked = await remote.call(agent, 'classifyAbnormal', { reason })
142
- if (marked.length) remote.setState(agent, 'recovering')
143
- return marked
144
- },
145
- /** @param {object} agent */
146
- recoveryUsage: agent => remote.call(agent, 'recoveryUsage'),
147
- /** @param {object} agent @param {any} delta */
148
- chargeRecovery: (agent, delta) => remote.call(agent, 'chargeRecovery', delta),
149
- pause: (agent, minutes) => remote.call(agent, 'pause', { minutes }),
150
- /** @param {object} agent @param {number} ms @param {{singleMaxMs: number, cumulativeMaxMs: number}} caps */
151
- hold: (agent, ms, caps) => remote.call(agent, 'hold', { ms, singleMaxMs: caps.singleMaxMs, cumulativeMaxMs: caps.cumulativeMaxMs }),
152
- /** @param {object} agent */
153
- turnStarted: agent => remote.call(agent, 'turnStarted'),
154
- /** @param {object} agent */
155
- releaseHeld: agent => remote.call(agent, 'releaseHeld'),
156
- /** @param {object} agent */
157
- /** @param {object} agent @param {boolean} [idle] */
158
- settleExpired: (agent, idle) => remote.call(agent, 'settleExpired', { idle: idle ?? /** @type {any} */ (agent)?.status === 'idle' }),
159
- /** @param {object} agent */
160
- retention: agent => remote.call(agent, 'retention'),
161
- /** @param {object} agent */
162
- blocks: agent => remote.state(agent) !== 'active',
163
- async close() { remote.close() },
164
- })
165
- }