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.
- package/cordis.patch.yml +7 -18
- package/lib/client.file-lease-badge.js +307 -0
- package/lib/client.js +20 -30
- package/lib/client.settings-page.js +10 -34
- package/package.json +2 -2
- package/src/capabilities/store/checked-contract.d.ts +2 -2
- package/src/capabilities/store/fs-adapter.js +2 -3
- package/src/context-guard/index.js +2 -2
- package/src/delegate/supervision-mount.js +5 -5
- package/src/delegate/tool.js +2 -10
- package/src/file-lease/client.js +1066 -0
- package/src/file-lease/index.js +30 -0
- package/src/file-lease/lineage.js +207 -0
- package/src/file-lease/node-contracts.d.ts +42 -0
- package/src/file-lease/plugin.js +789 -0
- package/src/file-lease/singleton.js +155 -0
- package/src/file-lease/spawn.js +62 -0
- package/src/file-lease/tools.js +459 -0
- package/src/file-lease/view.js +144 -0
- package/src/hashline-edit/index.js +76 -42
- package/src/hashline-edit/write-tool.js +90 -0
- package/src/intent-gate/matcher.js +27 -0
- package/src/lease-broker/entry.js +98 -0
- package/src/lease-broker/node-contracts.d.ts +63 -0
- package/src/lease-broker/protocol.js +236 -0
- package/src/lease-broker/server.js +399 -0
- package/src/lease-broker/state.js +425 -0
- package/src/lsp/admin.js +24 -13
- package/src/lsp/index.js +5 -2
- package/src/lsp/registry.js +83 -17
- package/src/lsp/tools.js +137 -27
- package/src/notify/commands.js +15 -2
- package/src/notify/index.js +48 -10
- package/src/notify/web-channel.js +26 -3
- package/src/process/index.js +11 -0
- package/src/settings/index.js +2 -44
- package/src/settings/sections.js +29 -28
- package/src/shared/audit.js +6 -1
- package/src/todo-driver/index.js +1 -6
- package/src/worktree/cards.js +6 -21
- package/src/worktree/lane-core.js +1 -19
- package/src/worktree/lane-workflows.js +11 -32
- package/lib/client.edit-lock-maintenance.js +0 -592
- package/lib/client.edit-lock-panel.js +0 -405
- package/src/edit-lock/adapter.js +0 -57
- package/src/edit-lock/admin-ledger.js +0 -149
- package/src/edit-lock/admin-recovery.js +0 -151
- package/src/edit-lock/admission.js +0 -82
- package/src/edit-lock/binding-attestation.js +0 -29
- package/src/edit-lock/binding-guidance.js +0 -72
- package/src/edit-lock/binding-machine.js +0 -312
- package/src/edit-lock/call-context.js +0 -65
- package/src/edit-lock/derived-facts.js +0 -67
- package/src/edit-lock/domain.js +0 -165
- package/src/edit-lock/domains.js +0 -170
- package/src/edit-lock/host.js +0 -149
- package/src/edit-lock/incarnations.js +0 -150
- package/src/edit-lock/index.js +0 -1303
- package/src/edit-lock/inspect.js +0 -219
- package/src/edit-lock/lifecycle.js +0 -579
- package/src/edit-lock/limits.js +0 -61
- package/src/edit-lock/maintenance.js +0 -310
- package/src/edit-lock/manager.js +0 -875
- package/src/edit-lock/negotiation.js +0 -132
- package/src/edit-lock/operation-history.js +0 -214
- package/src/edit-lock/peer-client.js +0 -86
- package/src/edit-lock/peer-transport.js +0 -76
- package/src/edit-lock/peer.js +0 -87
- package/src/edit-lock/publisher.js +0 -191
- package/src/edit-lock/read-authority.js +0 -101
- package/src/edit-lock/recovery.js +0 -139
- package/src/edit-lock/remote.js +0 -362
- package/src/edit-lock/request-binding.js +0 -42
- package/src/edit-lock/request-data.js +0 -40
- package/src/edit-lock/reservation.js +0 -301
- package/src/edit-lock/reserved-runtime.js +0 -45
- package/src/edit-lock/resource-identity.js +0 -130
- package/src/edit-lock/runtime.js +0 -145
- package/src/edit-lock/settle.js +0 -144
- package/src/edit-lock/snapshot.js +0 -175
- package/src/edit-lock/stale-sweep.js +0 -120
- package/src/edit-lock/state.js +0 -462
- package/src/edit-lock/store.js +0 -327
- package/src/edit-lock/tool-scope.js +0 -29
- package/src/edit-lock/view.js +0 -170
- package/src/edit-lock/write-scopes.js +0 -48
- package/src/edit-lock/write-tool.js +0 -45
- package/src/shared/delegate-edit-lifecycle.js +0 -36
- 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))
|
package/src/edit-lock/domain.js
DELETED
|
@@ -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
|
-
}
|