LearnNewsExamplesServices
Frontmatter
id15681
titleWriteGuard: TTL lease + scoped acquisition lifecycle for persistent writers
stateClosed
labels
enhancementaiarchitecture
assigneesneo-gpt
createdAtJul 22, 2026, 3:51 AM
updatedAtJul 22, 2026, 9:56 PM
githubUrlhttps://github.com/neomjs/neo/issues/15681
authorneo-kimi-iris
commentsCount2
parentIssue13056
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 22, 2026, 9:56 PM

WriteGuard: TTL lease + scoped acquisition lifecycle for persistent writers

Closed Backlog/active-chunk-8 enhancementaiarchitecture
neo-kimi-iris
neo-kimi-iris commented on Jul 22, 2026, 3:51 AM

Context

A live #15678 reproduction left the cockpit controller write-locked after failed call_method attempts from a persistent MCP connection. A second writer path from the same seat then received Write denied ... conflict until the App Worker was reloaded.

The root premise survives current-source inspection: WriteGuard.requestWrite() acquires and holds, admitWrite() returns no acquisition provenance, InstanceService.assertWritable() discards the admitted lock, and callMethod() may await arbitrary application code after admission. WriteGuard already names TTL expiry for silent holders as a follow-up.

The Problem

Held-until-release is intentional: it protects multi-step edits from cross-writer interleaving. The missing lifecycle facts are what make it unsafe for long-lived writers:

  1. an idle or silently dead holder has no expiry path;
  2. a caller cannot distinguish a newly created lock from same-writer re-entry;
  3. an operation owner has no fenced token with which to touch or conditionally release its acquisition;
  4. a naive wall-clock sweep could expire a lock while an async write is still in flight;
  5. a naive “release on error” could release a pre-existing hold or unlock partially mutated state.

Architectural Authority

ADR 0021 is binding:

  • the writer remains the exact (agentId, sessionId) pair;
  • two Bridge connections carrying the same AgentIdentity remain two writers;
  • held-until-release remains the default regime;
  • the App Worker heap remains the lock authority.

Therefore identity-only coalescing is explicitly rejected in this ticket. Changing it requires a separately evidenced ADR-0021 amendment.

The Fix

Implement one coherent lease + scoped acquisition lifecycle:

  1. Lease metadata: each held lock carries a generation/token, acquiredAt, lastTouchAt, and in-flight state. Public snapshots remain defensive copies.
  2. Acquisition receipt: LockRegistry.acquire() / WriteGuard.requestWrite() report whether the call created a new lock or re-entered an existing one, plus the current fenced token. admitWrite() and the write-path owner retain that receipt instead of discarding it.
  3. Deterministic lazy sweep: an injected clock drives request-time/inspection-time expiry; no owned timer is required. Only idle, expired locks are reclaimed. A request that observes reclamation emits an independently inspectable receipt.
  4. In-flight protection: begin/end (or an equivalent scoped handle) prevents expiry while an admitted synchronous or async operation is executing. Re-entry refreshes lastTouchAt without duplicating the lock.
  5. Fenced touch/release: an old operation or expired holder can never touch or release a successor generation. Disconnect release remains immediate and authoritative.
  6. Phase-aware error semantics: release automatically only when the current operation created the lock and failure is proven pre-mutation or rollback-complete. Re-entrant failures never release the pre-existing hold. Arbitrary call_method failures retain the hold because application code may have partially mutated; they end in-flight protection, emit a failure receipt, and become TTL-reclaimable.
  7. Incident coverage: reproduce persistent MCP writer versus a distinct in-process writer pair. The second path remains a distinct writer per ADR 0021; it succeeds only after disconnect release or observable idle expiry, never through identity coalescing.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Failure Docs Evidence
LockRegistry.acquire ADR 0021 writer/subtree model Preserve conflict math; return created vs reentrant acquisition provenance and a generation Invalid descriptors deny without mutation JSDoc Pure creation/re-entry specs
WriteGuard.requestWrite / held state Heap authority Stamp lease metadata, token, and in-flight state; defensive-copy every outward value Malformed lease input denies; existing valid hold remains JSDoc Clock-injected state-transition specs
Sweep owner ADR 0021 + Base lifecycle Lazy/request-time sweep with injected clock; reclaim only idle expired generations In-flight or unexpired locks remain JSDoc Boundary-time + async-in-flight specs
Touch/end/release Acquisition token Mutate/remove only the matching current generation Stale token is a no-op and cannot clobber a successor JSDoc Expired-A / successor-B fence spec
admitWrite / InstanceService.assertWritable Admission seam Return/retain the scoped acquisition receipt for the operation owner Legacy frames remain unguarded; enforced undecidable inputs deny JSDoc Integration specs
Error lifecycle Mutation disposition Auto-release only newly-created + pre-mutation/rollback-complete work; otherwise retain and emit failure receipt TTL/disconnect reclaims safely JSDoc New-vs-reentrant and partial-mutation specs
Writer identity ADR 0021 §2 Preserve (agentId, sessionId) exactly; no AgentIdentity join Different sessions conflict even if one seat/identity Ticket + JSDoc Spoof/coalescing rejection spec
Observability Verify-before-assert Bounded receipts name holder, subtree, reason, token/generation, and timestamps Receipt failure must not change lock decision JSDoc Sweep/error receipt specs

Acceptance Criteria

  • AC1: idle expired holders are reclaimed by an injected-clock lazy sweep; reclaim is observable.
  • AC2: an active in-flight async operation is never swept, even beyond TTL.
  • AC3: same-writer re-entry refreshes/touches without duplicating and reports reentrant; a new lock reports created.
  • AC4: disconnect release remains immediate; expiry is only the silent-holder fallback.
  • AC5: stale holder/token A cannot touch or release successor generation B.
  • AC6: error cleanup is acquisition- and mutation-disposition-aware; it never releases a pre-existing re-entrant hold or an unknown partially mutated hold.
  • AC7: (agentId, sessionId) remains the writer key. Same-identity coalescing is rejected under ADR 0021, including the ?id= spoofing falsifier.
  • AC8: the #15678 persistent-writer reproduction is covered: the second distinct writer is admitted after disconnect or idle expiry, with no App Worker reload.
  • AC9: existing overlap, defensive-copy, disconnect, undo/redo, create/destroy, set-property, and call-method behavior stays green.

Out of Scope

  • Per-operation locking as a replacement for held-until-release.
  • AgentIdentity-based writer coalescing.
  • Automatic rollback of arbitrary application methods.
  • Bridge authentication or #15678 argument marshalling.

Related

Origin Session ID: eb9be68e-9401-4ecd-9762-ef519b4091ed

Retrieval Hint: WriteGuard TTL lease scoped acquisition receipt token fence in-flight async persistent writer