weir-harness 1.2.0 → 1.2.2
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 +0 -2
- package/lib/client.blackboard-model.js +172 -56
- package/lib/client.capability-manager.js +0 -1
- package/lib/client.capability-model.js +378 -31
- package/lib/client.chain-model.js +80 -0
- package/lib/client.disabled-categories-editor.js +22 -0
- package/lib/client.edit-lock-maintenance.js +239 -246
- package/lib/client.edit-lock-panel.js +110 -0
- package/lib/client.hash-edit-model.js +48 -0
- package/lib/client.js +267 -1549
- package/lib/client.lsp-model.js +92 -0
- package/lib/client.lsp-panel.js +11 -16
- package/lib/client.lsp-toggle.js +18 -0
- package/lib/client.notify-permissions.js +112 -20
- package/lib/client.notify-web.js +14 -11
- package/lib/client.robash-model.js +26 -0
- package/lib/client.settings-page.js +287 -17
- package/lib/client.worktree-model.js +225 -0
- package/package.json +5 -2
- package/src/blackboard/durable.js +12 -0
- package/src/blackboard/index.js +2 -2
- package/src/blackboard/kernel.js +23 -6
- package/src/blackboard/notify.js +10 -3
- package/src/blackboard/remote.js +31 -13
- package/src/blackboard/storage.js +13 -5
- package/src/capabilities/apply-engine.js +189 -101
- package/src/capabilities/defaults-transaction.js +136 -1
- package/src/capabilities/lifecycle-snapshot.js +83 -204
- package/src/capabilities/mcp-admission.js +1 -23
- package/src/capabilities/mcp-drain.js +30 -19
- package/src/capabilities/mcp-facade.js +23 -3
- package/src/capabilities/preset-library.js +247 -0
- package/src/capabilities/preset-package.js +212 -1
- package/src/capabilities/same-name-policy.js +3 -41
- package/src/capabilities/selection-notify.js +324 -21
- package/src/capabilities/skill-office-adapter.js +7 -1
- package/src/capabilities/skill-selection-plugin.js +315 -729
- package/src/capabilities/store/checked-contract.d.ts +5 -0
- package/src/capabilities/store/fs-adapter.js +52 -0
- package/src/capabilities/store/lock.js +86 -0
- package/src/capabilities/store/paths.js +5 -0
- package/src/capabilities/store/store.js +105 -5
- package/src/creative-guide/guide.js +1 -1
- package/src/delegate/child-composition.js +258 -0
- package/src/delegate/group-coordinator.js +25 -27
- package/src/delegate/index.js +21 -2
- package/src/delegate/rehydrate.js +24 -4
- package/src/delegate/robash-guard-core.js +2 -2
- package/src/delegate/spawn-adapter.js +214 -100
- package/src/delegate/status-tool.js +9 -6
- package/src/delegate/supervision-mount.js +35 -6
- package/src/delegate/tool.js +29 -4
- package/src/{shared → delegate}/whitelist-defaults.js +2 -2
- package/src/edit-lock/admin-ledger.js +8 -11
- package/src/edit-lock/admin-recovery.js +2 -1
- package/src/edit-lock/admission.js +12 -2
- package/src/edit-lock/binding-guidance.js +28 -10
- package/src/edit-lock/binding-machine.js +312 -0
- package/src/edit-lock/derived-facts.js +67 -0
- package/src/edit-lock/index.js +44 -244
- package/src/edit-lock/inspect.js +113 -1
- package/src/edit-lock/lifecycle.js +8 -5
- package/src/edit-lock/limits.js +61 -0
- package/src/edit-lock/maintenance.js +5 -2
- package/src/edit-lock/manager.js +6 -2
- package/src/edit-lock/operation-history.js +1 -1
- package/src/edit-lock/peer-client.js +9 -2
- package/src/edit-lock/peer-transport.js +7 -1
- package/src/edit-lock/remote.js +11 -4
- package/src/edit-lock/state.js +2 -4
- package/src/edit-lock/view.js +10 -11
- package/src/lsp/admin.js +2 -24
- package/src/lsp/index.js +1 -1
- package/src/lsp/manager.js +1 -1
- package/src/notify/permissions-admin.js +1 -1
- package/src/{lsp/child-process.js → process/index.js} +23 -6
- package/src/settings/env-admin.js +1 -1
- package/src/settings/index.js +1 -1
- package/src/settings/sections.js +0 -54
- package/src/shared/audit.js +12 -0
- package/src/shared/child-scope.js +3 -1
- package/src/shared/http-json.js +28 -0
- package/src/{delegate/audit-readers.js → shared/terminal-evidence.js} +34 -4
- package/src/worktree/errors.js +3 -0
- package/src/worktree/index.js +1 -2
- package/src/worktree/lane-core.js +641 -0
- package/src/worktree/lane-views.js +147 -0
- package/src/worktree/lane-workflows.js +853 -0
- package/src/worktree/lanes.js +24 -1513
- package/src/worktree/runner.js +5 -4
- package/src/capabilities/content-pin.js +0 -88
- package/src/capabilities/content-refresh.js +0 -331
- package/src/capabilities/distribution-manifest.js +0 -78
- package/src/capabilities/isolated-exec.js +0 -106
- package/src/capabilities/publisher.js +0 -129
- package/src/capabilities/skill-admission.js +0 -181
- package/src/capabilities/update-lifecycle.js +0 -106
- package/src/edit-lock/remote-service.js +0 -28
- package/src/worktree/contract-guidance.test.js +0 -100
package/src/blackboard/kernel.js
CHANGED
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
// wire delivery. The kernel is concept-isomorphic to Edit Lock but shares
|
|
13
13
|
// none of its code: no persistence, no recovery, no cross-process story.
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* One live write token; `timer` is the injected timer handle (opaque here).
|
|
17
|
+
* @typedef {{ key: string, holder: string, expiresAt: number, timer: unknown }} Token
|
|
18
|
+
* One registered subscription row (rebuilt from the owning runtime's snapshot).
|
|
19
|
+
* @typedef {{ key: string, subscriberIds: string[] }} Subscription
|
|
20
|
+
* The release-event shape every listener receives (slice 2 delivery wiring).
|
|
21
|
+
* @typedef {{ boardId: string, key: string, holder: string, reason: 'write' | 'delete' | 'expire' | 'terminate', subscriberIds: string[], at: number }} ReleaseEvent
|
|
22
|
+
* A release listener; onRelease returns the matching unregister.
|
|
23
|
+
* @typedef {(event: ReleaseEvent) => void} ReleaseListener
|
|
24
|
+
* One board's volatile state: durable entries plus the arbitration maps.
|
|
25
|
+
* @typedef {{ entries: Map<string, object>, tokens: Map<string, Token>, subscribers: Map<string, Set<string>> }} BoardState
|
|
26
|
+
* The runtime snapshot `restore` rebuilds a board's volatile state from.
|
|
27
|
+
* @typedef {{ tokens: Token[], subscriptions: Subscription[] }} RuntimeState
|
|
28
|
+
*/
|
|
15
29
|
/** The closed entry-type taxonomy (D4); schema `enum` and runtime both pin it. */
|
|
16
30
|
export const ENTRY_TYPES = Object.freeze(['map', 'contract', 'deadend', 'wiring', 'recipe', 'why'])
|
|
17
31
|
|
|
@@ -101,9 +115,9 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
|
|
|
101
115
|
})
|
|
102
116
|
const disarm = clearTimer ?? ((/** @type {any} */ handle) => globalThis.clearTimeout(handle))
|
|
103
117
|
|
|
104
|
-
/** @type {Map<string,
|
|
118
|
+
/** @type {Map<string, BoardState>} */
|
|
105
119
|
const boards = new Map()
|
|
106
|
-
/** @type {Set<
|
|
120
|
+
/** @type {Set<ReleaseListener>} release listeners (slice 2 delivery wiring) */
|
|
107
121
|
const listeners = new Set()
|
|
108
122
|
|
|
109
123
|
/** @param {string} boardId @returns {any} the board, created lazily */
|
|
@@ -122,7 +136,7 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
|
|
|
122
136
|
* best-effort and NEVER breaks arbitration; events only fire when someone
|
|
123
137
|
* actually waits (subscriberIds non-empty) so slice 2 gets a signal, not
|
|
124
138
|
* noise.
|
|
125
|
-
* @param {
|
|
139
|
+
* @param {ReleaseEvent} event
|
|
126
140
|
*/
|
|
127
141
|
function fireRelease(event) {
|
|
128
142
|
if (!Array.isArray(event.subscriberIds) || event.subscriberIds.length === 0) return
|
|
@@ -153,7 +167,7 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
|
|
|
153
167
|
const state = boards.get(boardId)
|
|
154
168
|
const token = state?.tokens.get(key)
|
|
155
169
|
if (!token || token.expiresAt > at) return
|
|
156
|
-
state.tokens.delete(key)
|
|
170
|
+
/** @type {BoardState} */ (state).tokens.delete(key)
|
|
157
171
|
if (token.timer !== undefined) disarm(token.timer)
|
|
158
172
|
release(state, boardId, key, token.holder, 'expire', at)
|
|
159
173
|
}
|
|
@@ -438,11 +452,14 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
|
|
|
438
452
|
|
|
439
453
|
// Internal transaction seam. Only entries are durable; authority comes
|
|
440
454
|
// exclusively from the owning runtime's snapshot, never from disk.
|
|
455
|
+
/**
|
|
456
|
+
* @param {string} boardId @param {any} entries @param {RuntimeState} [runtime]
|
|
457
|
+
*/
|
|
441
458
|
restore(boardId, entries, runtime = { tokens: [], subscriptions: [] }) {
|
|
442
459
|
const state = board(boardId)
|
|
443
460
|
state.entries = new Map(entries.map(entry => [entry.key, structuredClone(entry)]))
|
|
444
|
-
state.tokens = new Map(runtime.tokens.map(token => [token.key, { ...token }]))
|
|
445
|
-
state.subscribers = new Map(runtime.subscriptions.map(row => [row.key, new Set(row.subscriberIds)]))
|
|
461
|
+
state.tokens = new Map(runtime.tokens.map(token => /** @type {[string, Token]} */ ([token.key, { ...token }])))
|
|
462
|
+
state.subscribers = new Map(runtime.subscriptions.map(row => /** @type {[string, Set<string>]} */ ([row.key, new Set(row.subscriberIds)])))
|
|
446
463
|
},
|
|
447
464
|
|
|
448
465
|
expire(boardId, at = now()) {
|
package/src/blackboard/notify.js
CHANGED
|
@@ -32,6 +32,12 @@ export const RELEASE_NOTICE_PREFIX = 'Blackboard notice'
|
|
|
32
32
|
/** Delivery attempts before the final failure is logged and dropped. */
|
|
33
33
|
export const MAX_DELIVERY_ATTEMPTS = 3
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The subagent delivery face a between-turns subscriber is reached through: asserted
|
|
37
|
+
* present on the one path that needs it (no runtime guard is added, design D6).
|
|
38
|
+
* @typedef {{ sendMessage: (parent: any, childId: string, blocks: Array<{ type: string, text: string }>, options: object) => unknown }} SubagentSendFace
|
|
39
|
+
*/
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* The human-readable release clause per reason. Instance content (the key) is
|
|
37
43
|
* embedded verbatim; the template is English.
|
|
@@ -117,7 +123,7 @@ export function createReleaseNotifier({ agents, parentOf, subagents, logger, set
|
|
|
117
123
|
* the live agent works, followup when it is idle, and — for a delegated
|
|
118
124
|
* child whose agent handle is dropped between turns — the subagents channel
|
|
119
125
|
* through its live parent.
|
|
120
|
-
* @param {string} subscriberId @param {
|
|
126
|
+
* @param {string} subscriberId @param {{ content: Array<{ type: string, text: string }> }} message @param {number} attempt
|
|
121
127
|
*/
|
|
122
128
|
function deliver(subscriberId, message, attempt) {
|
|
123
129
|
const agent = safeGet(agents, subscriberId)
|
|
@@ -137,7 +143,7 @@ export function createReleaseNotifier({ agents, parentOf, subagents, logger, set
|
|
|
137
143
|
}
|
|
138
144
|
const parent = typeof parentId === 'string' && parentId.length > 0 ? safeGet(agents, parentId) : undefined
|
|
139
145
|
if (!parent) throw new Error(`subscriber "${subscriberId}" is between turns and its parent agent is unavailable`)
|
|
140
|
-
return channel.sendMessage(parent, subscriberId, [{ type: 'text', text: message.content[0].text }], { signal: new AbortController().signal })
|
|
146
|
+
return /** @type {SubagentSendFace} */ (channel).sendMessage(parent, subscriberId, [{ type: 'text', text: message.content[0].text }], { signal: new AbortController().signal })
|
|
141
147
|
}, (error) => {
|
|
142
148
|
logger?.warn?.(`blackboard: release notice delivery to "${subscriberId}" failed after ${MAX_DELIVERY_ATTEMPTS} attempts: ${error?.message ?? error}`)
|
|
143
149
|
})
|
|
@@ -145,7 +151,8 @@ export function createReleaseNotifier({ agents, parentOf, subagents, logger, set
|
|
|
145
151
|
|
|
146
152
|
return (event) => {
|
|
147
153
|
if (!Array.isArray(event.subscriberIds) || event.subscriberIds.length === 0) return
|
|
148
|
-
|
|
154
|
+
// userTextMessage declares only `object`; assert the shape deliver reads (S13 UserMessage contract).
|
|
155
|
+
const message = /** @type {{ content: Array<{ type: string, text: string }> }} */ (userTextMessage(renderReleaseNotice(event), RELEASE_SOURCE_KIND))
|
|
149
156
|
for (const subscriberId of event.subscriberIds) {
|
|
150
157
|
if (typeof subscriberId !== 'string' || subscriberId.length === 0) continue
|
|
151
158
|
deliver(subscriberId, message, 1)
|
package/src/blackboard/remote.js
CHANGED
|
@@ -84,7 +84,7 @@ const BLACKBOARD_REMOTE_PACKAGE = 'weir-blackboard'
|
|
|
84
84
|
*/
|
|
85
85
|
export class BlackboardRemoteError extends Error {
|
|
86
86
|
/**
|
|
87
|
-
* @param {'bridge-absent' | 'unknown-session' | 'payload-failed'} code
|
|
87
|
+
* @param {'bridge-absent' | 'unknown-session' | 'payload-failed' | 'channel-unavailable'} code
|
|
88
88
|
* @param {string} message
|
|
89
89
|
* @param {{ cause?: unknown }} [options]
|
|
90
90
|
*/
|
|
@@ -96,29 +96,47 @@ export class BlackboardRemoteError extends Error {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* The bridge:
|
|
100
|
-
*
|
|
101
|
-
*
|
|
99
|
+
* The bridge: EVERY mounted blackboard plugin feeds this module-level slot,
|
|
100
|
+
* and every consumer is the same bundle module instance. The preset registry
|
|
101
|
+
* eagerly mounts BOTH declared presets (weir and weir-creative) at profile
|
|
102
|
+
* load, so two blackboard mounts feeding this bridge is the permanent steady
|
|
103
|
+
* state — ownership is a LIVE-FEED STACK: the latest live feed serves, a
|
|
104
|
+
* feed's unregister removes only itself and restores the previous live feed,
|
|
105
|
+
* and only an empty stack surfaces as bridge-absent. (The sibling capability
|
|
106
|
+
* read bridge keys its feeds by preset instead: its reads route by the
|
|
107
|
+
* session's exact projected preset, while blackboard calls carry no routing
|
|
108
|
+
* dimension and the two presets' faces are equivalent today.)
|
|
109
|
+
* Faces:
|
|
102
110
|
* kernel() — the shared arbitration kernel (list/read/apply/write/deleteKey)
|
|
103
111
|
* boardOf(agent) — conversation-root board id for an agent handle
|
|
104
112
|
* rootOf(sessionId) — conversation-root session id for a BARE session id
|
|
105
113
|
* ttlMs() — the settings-driven write-token TTL in milliseconds
|
|
106
114
|
*/
|
|
107
|
-
|
|
115
|
+
/** The faces every live feed provides. `run` is the mount's lifecycle gate (apply/write/delete through one admission), `requestPromotion` the slice-3 promotion requester, and `boardOf` may await that same gate. @typedef {{ kernel: any, boardOf: (agent: any) => string | undefined | Promise<string | undefined>, rootOf?: (sessionId: string) => Promise<string>, ttlMs: () => number, run?: (agent: any, action: (root: string) => any) => Promise<any>, requestPromotion?: (agent: any) => Promise<unknown> }} BridgeFaces */
|
|
116
|
+
/** @type {Array<{ faces: BridgeFaces }>} Live feeds in feed order; the stack top serves. */
|
|
117
|
+
const bridgeStack = []
|
|
108
118
|
|
|
109
119
|
/**
|
|
110
|
-
* Feed the bridge (blackboard plugin apply)
|
|
111
|
-
* disposes with its own
|
|
112
|
-
*
|
|
120
|
+
* Feed the bridge (blackboard plugin apply): push a unique token onto the
|
|
121
|
+
* live-feed stack and return the unregister the plugin disposes with its own
|
|
122
|
+
* lifecycle. The unregister removes exactly its own token — removing the
|
|
123
|
+
* serving token restores the previous live feed — and a repeated call is a
|
|
124
|
+
* no-op.
|
|
125
|
+
* @param {BridgeFaces} faces
|
|
113
126
|
*/
|
|
114
127
|
export function feedBlackboardRemoteBridge(faces) {
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
const token = { faces }
|
|
129
|
+
bridgeStack.push(token)
|
|
130
|
+
return () => {
|
|
131
|
+
const at = bridgeStack.indexOf(token)
|
|
132
|
+
if (at >= 0) bridgeStack.splice(at, 1)
|
|
133
|
+
}
|
|
117
134
|
}
|
|
118
135
|
|
|
119
|
-
/** Current bridge faces, or null when
|
|
136
|
+
/** Current bridge faces: the latest live feed's, or null when no live feed remains (the one true bridge-absent). */
|
|
120
137
|
export function blackboardRemoteBridge() {
|
|
121
|
-
|
|
138
|
+
const top = bridgeStack[bridgeStack.length - 1]
|
|
139
|
+
return top ? top.faces : null
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
/** Strip one kernel row to the pinned wire entry shape (summary is a free-text string, content optional, the promoted marker rides along when present). */
|
|
@@ -165,7 +183,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
165
183
|
/**
|
|
166
184
|
* Resolve the acting session and its board from the wire agent.
|
|
167
185
|
* @param {any} agent
|
|
168
|
-
* @returns {{ sessionId: string, boardId: string
|
|
186
|
+
* @returns {Promise<{ sessionId: string, boardId: string | undefined, faces: BridgeFaces }>}
|
|
169
187
|
*/
|
|
170
188
|
async function resolveActing(agent) {
|
|
171
189
|
if (!ensure()) throw new BlackboardRemoteError('channel-unavailable', 'blackboard remote is not registered with the gateway')
|
|
@@ -12,7 +12,9 @@ export const LOG = /^session\.v([1-9][0-9]*)\.jsonl(?:\.zstd)?$/
|
|
|
12
12
|
const SIDE = /^session\.v[1-9][0-9]*\.jsonl(?:\.zstd)?_bb$/
|
|
13
13
|
const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/
|
|
14
14
|
const LOCK = 'weir-blackboard.lock'
|
|
15
|
-
|
|
15
|
+
// The checked graph's global `process` contract (capabilities/store/checked-contract.d.ts)
|
|
16
|
+
// is narrow by design; getuid is optional here and asserted locally (kernel.js:99 precedent).
|
|
17
|
+
const uid = () => /** @type {any} */ (process).getuid?.()
|
|
16
18
|
const code = error => error?.code
|
|
17
19
|
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
18
20
|
|
|
@@ -55,11 +57,11 @@ export async function locateSession(home, id, expectedCwd) {
|
|
|
55
57
|
const logs = (await fs.readdir(dir)).filter(name => LOG.test(name))
|
|
56
58
|
// Highest generation is DSH's canonical selection. Same-version physical
|
|
57
59
|
// encodings are ambiguous: never guess which installation owns the log.
|
|
58
|
-
if (logs.some(name => !Number.isSafeInteger(Number(LOG.exec(name)[1])))) throw new Error('blackboard: unsupported canonical generation number')
|
|
59
|
-
logs.sort((a, b) => Number(LOG.exec(b)[1]) - Number(LOG.exec(a)[1]))
|
|
60
|
+
if (logs.some(name => !Number.isSafeInteger(Number(/** @type {RegExpExecArray} */ (LOG.exec(name))[1])))) throw new Error('blackboard: unsupported canonical generation number')
|
|
61
|
+
logs.sort((a, b) => Number(/** @type {RegExpExecArray} */ (LOG.exec(b))[1]) - Number(/** @type {RegExpExecArray} */ (LOG.exec(a))[1]))
|
|
60
62
|
if (!logs.length) continue
|
|
61
63
|
const name = logs[0]
|
|
62
|
-
if (logs[1] && LOG.exec(logs[1])[1] === LOG.exec(name)[1]) throw new Error('blackboard: ambiguous canonical log')
|
|
64
|
+
if (logs[1] && /** @type {RegExpExecArray} */ (LOG.exec(logs[1]))[1] === /** @type {RegExpExecArray} */ (LOG.exec(name))[1]) throw new Error('blackboard: ambiguous canonical log')
|
|
63
65
|
const path = join(dir, name)
|
|
64
66
|
await trusted(path)
|
|
65
67
|
const handle = await fs.open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0))
|
|
@@ -70,7 +72,7 @@ export async function locateSession(home, id, expectedCwd) {
|
|
|
70
72
|
const text = name.endsWith('.zstd') ? zstdDecompressSync(bytes).toString('utf8') : bytes.toString('utf8')
|
|
71
73
|
header = JSON.parse(text.split('\n')[0])
|
|
72
74
|
} catch { throw new Error('blackboard: unreadable Session header') }
|
|
73
|
-
if (header.type !== 'session' || header.id !== id || typeof header.cwd !== 'string' || !isAbsolute(header.cwd) || header.version !== Number(LOG.exec(name)[1]) || expectedCwd !== undefined && header.cwd !== expectedCwd) throw new Error('blackboard: Session header identity/cwd mismatch')
|
|
75
|
+
if (header.type !== 'session' || header.id !== id || typeof header.cwd !== 'string' || !isAbsolute(header.cwd) || header.version !== Number(/** @type {RegExpExecArray} */ (LOG.exec(name))[1]) || expectedCwd !== undefined && header.cwd !== expectedCwd) throw new Error('blackboard: Session header identity/cwd mismatch')
|
|
74
76
|
matches.push({ id, dir, log: path, file: join(dir, name + '_bb'), header })
|
|
75
77
|
}
|
|
76
78
|
if (matches.length !== 1) throw new Error(`blackboard: ${matches.length ? 'ambiguous Session directories' : 'durable Session log unavailable; sidecars preserved'}`)
|
|
@@ -102,6 +104,12 @@ function dead(owner) {
|
|
|
102
104
|
const ownerKey = Symbol.for('weir.blackboard.storage.owner.v1')
|
|
103
105
|
const writerToken = globalThis[ownerKey] ??= randomUUID()
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* @param {object} [dependencies] test seams
|
|
109
|
+
* @param {string} [dependencies.home] profile home; locateSession fail-closes on an unusable value
|
|
110
|
+
* @param {(op: string, path: string) => unknown} [dependencies.hook] maintenance fault-injection seam
|
|
111
|
+
* @param {number} [dependencies.timeoutMs] writer-lock acquisition budget
|
|
112
|
+
*/
|
|
105
113
|
export function createBlackboardStorage({ home, hook = async (_op, _path) => {}, timeoutMs = 5000 } = {}) {
|
|
106
114
|
const queue = new Map()
|
|
107
115
|
async function safeRead(path) {
|