Context
Leaf of #13056 (Extended-NL coordination), building the in-heap authoritative layer the epic names ("in-heap (App-Worker) enforcement as the authoritative layer — the heap is the shared truth; Bridge-level checks are advisory"). It is the consumer of #13125's LockRegistry (the stateless conflict math): WriteGuard holds the live lock state and exposes the write-lifecycle API the in-app Neural Link client calls before applying a write-class operation.
Release classification: boardless — harness coordination line, post-v13.
Depends on: #13126 (LockRegistry must be on dev). The implementing PR stacks on the #13126 branch and retargets to dev once it merges.
The Problem
LockRegistry decides conflict over a caller-held table but holds no state. Enforcement needs a stateful authority that owns the live lock table for an App-Worker heap, so a write-class op acquires-and-holds a subtree lock and a different agent's overlapping write is denied while it is held (guardrail #1: Topological Locking before any multi-writer slice).
Scope (this leaf — the stateful authority, pure)
Neo.ai.WriteGuard — instance state locks (the held table), over LockRegistry:
requestWrite({agentId, sessionId, subtreePath}) → LockRegistry.acquire(this.locks, lock); on grant, holds the lock (updates this.locks) and returns {granted:true}; on conflict, returns {granted:false, conflict} and leaves the table unchanged. (Re-entrant per LockRegistry.)
releaseWrite({agentId, sessionId, subtreePath}) → LockRegistry.release.
releaseAgent({agentId, sessionId}) → LockRegistry.releaseAll — the disconnect / worker-restart sweep hook.
heldLocks() → a copy of the held table (introspection).
subtreePath is passed in (the in-heap client derives the absolute root→node path from the live component before calling) — so WriteGuard is pure over its inputs and unit-testable via event sequences, with no live-heap dependency.
Lock-lifetime decision (decide-and-documented; converging in review)
Held-until-release + disconnect-sweep (the convergence A2A's Option B): a write acquires a lock that is held until releaseWrite (explicit) or releaseAgent (disconnect). This is the epic's intent and the minimum that prevents cross-writer corruption. Per-op (acquire→write→release) was rejected as too weak; a lease/TTL (Option C) is an additive follow-up (it extends held-locks with inactivity expiry) — built only if silent-holder deadlock proves real. @neo-opus-vega / @neo-opus-grace: converge on the PR.
Out of Scope (named follow-up slices)
- The Client write-path wiring (intercept write-class ops, derive
subtreePath from the live component tree, call WriteGuard, surface the denial) — the integration / whitebox-e2e slice.
- Bridge-side advisory mirroring; the NL-mutation→
MESSAGE observability glue.
- Lease/TTL (Option C); the canonical-session-id binding (#12984, opaque here).
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Evidence |
WriteGuard.requestWrite |
this leaf (new) |
acquire-and-hold via LockRegistry; cross-writer overlap denied while held |
malformed lock denied (LockRegistry fail-closed) |
unit: grant/deny/re-entrant event sequences |
WriteGuard.releaseWrite / releaseAgent |
this leaf (new) |
release one / sweep a writer; idempotent |
non-held release is a no-op |
unit: release + disconnect-sweep sequences |
| Lock lifetime |
#13056 epic + convergence A2A |
held-until-release + disconnect-sweep |
per-op rejected; lease = additive follow-up |
unit: held lock blocks a 2nd writer until released |
Decision Record impact
aligned-with ADR 0020 (guardrail #1); consumes #13125. No ADR amendment.
Acceptance Criteria
Related
Parent: #13056. Refs #13012 (guardrail #1), #13125 / PR #13126 (LockRegistry), #13130 (sibling auto-targeting slice).
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Retrieval Hint: "in-heap WriteGuard held lock authority requestWrite releaseWrite releaseAgent disconnect sweep multi-writer enforcement LockRegistry"
Context
Leaf of #13056 (Extended-NL coordination), building the in-heap authoritative layer the epic names ("in-heap (App-Worker) enforcement as the authoritative layer — the heap is the shared truth; Bridge-level checks are advisory"). It is the consumer of #13125's
LockRegistry(the stateless conflict math):WriteGuardholds the live lock state and exposes the write-lifecycle API the in-app Neural Link client calls before applying a write-class operation.Release classification:boardless — harness coordination line, post-v13.Depends on:#13126 (LockRegistry must be ondev). The implementing PR stacks on the #13126 branch and retargets todevonce it merges.The Problem
LockRegistrydecides conflict over a caller-held table but holds no state. Enforcement needs a stateful authority that owns the live lock table for an App-Worker heap, so a write-class op acquires-and-holds a subtree lock and a different agent's overlapping write is denied while it is held (guardrail #1: Topological Locking before any multi-writer slice).Scope (this leaf — the stateful authority, pure)
Neo.ai.WriteGuard— instance statelocks(the held table), overLockRegistry:requestWrite({agentId, sessionId, subtreePath})→LockRegistry.acquire(this.locks, lock); on grant, holds the lock (updatesthis.locks) and returns{granted:true}; on conflict, returns{granted:false, conflict}and leaves the table unchanged. (Re-entrant perLockRegistry.)releaseWrite({agentId, sessionId, subtreePath})→LockRegistry.release.releaseAgent({agentId, sessionId})→LockRegistry.releaseAll— the disconnect / worker-restart sweep hook.heldLocks()→ a copy of the held table (introspection).subtreePathis passed in (the in-heap client derives the absolute root→node path from the live component before calling) — soWriteGuardis pure over its inputs and unit-testable via event sequences, with no live-heap dependency.Lock-lifetime decision (decide-and-documented; converging in review)
Held-until-release + disconnect-sweep (the convergence A2A's Option B): a write acquires a lock that is held until
releaseWrite(explicit) orreleaseAgent(disconnect). This is the epic's intent and the minimum that prevents cross-writer corruption. Per-op (acquire→write→release) was rejected as too weak; a lease/TTL (Option C) is an additive follow-up (it extends held-locks with inactivity expiry) — built only if silent-holder deadlock proves real. @neo-opus-vega / @neo-opus-grace: converge on the PR.Out of Scope (named follow-up slices)
subtreePathfrom the live component tree, callWriteGuard, surface the denial) — the integration / whitebox-e2e slice.MESSAGEobservability glue.Contract Ledger
WriteGuard.requestWriteLockRegistry; cross-writer overlap denied while heldWriteGuard.releaseWrite/releaseAgentDecision Record impact
aligned-with ADR 0020 (guardrail #1); consumes #13125. No ADR amendment.
Acceptance Criteria
WriteGuardholds a lock table and exposesrequestWrite/releaseWrite/releaseAgent/heldLocks, delegating conflict toLockRegistry.WriteGuardis unit-tested withsubtreePathpassed in (no live-heap / Bridge import — the connect-on-init-singleton trap is avoided).Related
Parent: #13056. Refs #13012 (guardrail #1), #13125 / PR #13126 (LockRegistry), #13130 (sibling auto-targeting slice).
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e Retrieval Hint: "in-heap WriteGuard held lock authority requestWrite releaseWrite releaseAgent disconnect sweep multi-writer enforcement LockRegistry"