LearnNewsExamplesServices
Frontmatter
id15296
titlePublish hook projections through the fenced single-writer lease
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtJul 16, 2026, 8:34 PM
updatedAt6:37 AM
githubUrlhttps://github.com/neomjs/neo/issues/15296
authorneo-opus-ada
commentsCount0
parentIssue15100
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 15267 Produce the lifecycle frontier — the response-required fact source
blocking[]
closedAt6:37 AM

Publish hook projections through the fenced single-writer lease

Closed Backlog/active-chunk-6 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jul 16, 2026, 8:34 PM

Context

Live Lane Awareness (Epic #15100, graduated from Discussion #15090) is producer-complete and consumer-empty. All four truth surfaces are built or in review — the typed route (#15087, merged), the two historical Bird Views (#14435, #15088, shipped), the current-state landscape (#15234, in review), and the lifecycle frontier (#15267, PR #15295) — and not one of them reaches a hook. ADR 0035 §2.6 specifies the writer that would carry them. It has no ticket.

This is the missing critical-path leaf of its own epic. Epic #15100 names it directly: "The only stateful adapter is an expiring projection writer with enforced single-writer lease semantics and atomic old-complete/new-complete transport", and OQ5/OQ6 record enforced single-writer atomic transport as a resolved open question.

Filed rather than assumed: a producer whose output nothing consumes is unwired substrate that decays silently — the surrounding release pain (peers hand-surveying lifecycle obligations every hook fire) is not relieved by a producer, only by a projection a hook can read.

The Problem

Hook projections today render fixture or policy-only rows (#15265), and the canonical route stays trapped behind rendered Markdown. Wave 1's whole thesis is that hooks become pure bounded renderers of a published projection. Without the writer there is nothing to render, so every consumer-side leaf (§2.11 Ph3–4 reader cutover) stays blocked behind this one.

The hard part is not writing a file — it is that multiple MCP/service processes can race one target, and a stale holder must not be able to mutate the resource after takeover. An atomic rename alone was explicitly rejected by the epic: it prevents torn reads but not multi-writer lost updates.

The Architectural Reality

ADR 0035 §2.6 decides ownership and the storage contract; this ticket implements a decided contract and does not reopen one. The exact class/file still runs structural pre-flight.

  • Owner: the Memory Core operational service boundary (§2.6). Every local resident already authenticates to it; its SQLite/WAL store is process-shared so multiple processes can arbitrate one target; it has an operational SummarizationJobs lease-table precedent; and keeping the writer next to the broker lets the resource validate the fencing token immediately before rename. A remote lease plus an unguarded local write would not actually fence stale writers.
  • Two operational tables in the shared SQLite store — explicitly not Native Edge Graph nodes, creating no ontology: HookProjectionChannels(target_id, channel, source_watermark, envelope_json, captured_at, expires_at, updated_at, PK(target_id, channel)) and HookProjectionLeases(target_id PK, fencing_epoch, holder_token_hash, holder_instance_digest, acquired_at, expires_at, state).
  • target_id is server-derived from the attested tuple {schemaVersion, capability, agentId, harnessType, instanceKeyDigest, workspaceKeyDigest, projectionKind}; session id and raw paths excluded. Output path is derived under the Memory-Core-owned runtime root (<runtime-root>/hook-projections/<target_id>/current.json). A producer can submit an envelope for an admitted target/channel; it cannot choose a filesystem path.
  • Producer submission (§2.6.1): each producer submits only its own typed channel + watermark + expiry; a transaction validates schema, target admission, producer identity, and monotonic watermark. Regressed watermark rejected; equal-watermark identical replay idempotent; equal-watermark different payload is a source conflict and degrades that channel. Producers never read/write current.json and never merge another producer's channel.
  • Resource-side fencing (§2.6.2) — the load-bearing part: immediately before touching current.json the winner opens a serialized SQLite write transaction, revalidates {target_id, token hash, fencing_epoch, not-expired} against the broker's clock, reads the latest committed channel rows, and holds that same transaction while creating a unique temp sibling, flushing, and atomically renaming. No takeover can commit between token revalidation and resource mutation. An epoch in JSON is diagnostic; the transaction around the rename is what enforces the property.
  • Wave 1 lease shape: a bounded single-publication lease with no renewal. If one bounded render/write cannot finish before expiry it aborts — adding renewal requires ADR revalidation.
  • leaseTtlMs is required from the Memory-Core configuration boundary; the primitive has no hidden default (ADR-0019 discipline).
  • Fail-closed: missing SQLite, busy timeout, unreadable schema, or lease-service failure ⇒ no projection update. Normal contention returns held/retry metadata and performs no write.
  • Release is conditional on token hash AND epoch — a stale holder can neither release nor overwrite a successor; success and failure paths both use token+epoch-checked release.
  • Producer input contract already landed: lifecycle-frontier.v1 (#15267) and computed-route.v1 (#15087) are typed, frozen, notAuthority envelopes carrying their own watermark/expiry.
  • Structure map: Memory Core operational service helpers; SummarizationJobs is the cited lease precedent.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
New HookProjectionChannels table ADR 0035 §2.6 Per-producer typed channel rows keyed (target_id, channel); monotonic watermark enforced in-transaction Regressed watermark rejected; equal-watermark different payload degrades that channel Module JSDoc watermark monotonicity + replay-idempotence + conflict specs
New HookProjectionLeases table ADR 0035 §2.6.2 Serialized acquisition; monotonic fencing_epoch; only the token HASH stored Unheld/expired → acquire; held → held + retry metadata, no write Module JSDoc contention + epoch-monotonicity specs
New projection writer primitive ADR 0035 §2.6.2 Token+epoch revalidated inside the SAME transaction that renames current.json Any lease/SQLite failure → fail-closed, no update Module JSDoc stale-holder-cannot-mutate-after-takeover spec
New leaseTtlMs config leaf ADR 0035 §2.6.2 + ADR-0019 Required from the Memory-Core config boundary No hidden default — absent leaf fails loud config.template.mjs JSDoc fail-loud-on-missing-leaf spec
Existing lifecycle-frontier.v1 / computed-route.v1 producers #15267 / #15087 Consumed as submitted channels; never merged or re-derived by a producer A channel absent/expired is honestly absent, never synthesized Producer JSDoc per-channel independence spec

Decision Record impact

aligned-with ADR 0035 §2.6 (writer ownership + storage contract) and §2.6.1–2.6.3 (submission, fencing, crash/stale behavior). No amendment — this leaf implements the decided contract. Any move to a renewable lease is an ADR revalidation, not an implementation choice.

Decision Record

Required authority: ADR 0035 (graduated from Discussion #15090, Cycle-7), §2.6.

Acceptance Criteria

  • HookProjectionChannels + HookProjectionLeases exist as operational tables in the shared Memory Core SQLite store — not Native Edge Graph nodes, creating no ontology.
  • Lease acquisition runs in a serialized write transaction; succeeds only when the target row is unheld or expired; increments fencing_epoch monotonically; returns a raw token while storing only its hash.
  • The fencing property, pinned by a concurrency test, not by inspection: a stale holder cannot mutate current.json after takeover. Token+epoch revalidation and the rename occur inside the SAME serialized transaction; no takeover commits between them.
  • Release/clear is conditional on token hash AND epoch — a stale holder can neither release nor overwrite a successor; success and failure paths both use token+epoch-checked release.
  • Producer submission validates schema, target admission, producer identity, and monotonic watermark: regressed watermark rejected; equal-watermark identical replay idempotent; equal-watermark different payload degrades that channel as a source conflict.
  • target_id is server-derived from the attested tuple; session id and raw paths excluded. A producer cannot supply a filesystem path — pinned by spec.
  • leaseTtlMs is required from the Memory-Core config boundary and fails loud when absent — no hidden default, not even a literal.
  • Wave-1 lease is bounded single-publication with no renewal: a render/write that cannot finish before expiry aborts rather than extending.
  • Fail-closed on missing SQLite, busy timeout, unreadable schema, or lease-service failure: no projection update, never a partial write.
  • Reader-observable atomicity: a concurrent reader sees old-complete or new-complete, never a torn payload.
  • Cross-family review challenges the fencing proof and the crash/takeover path specifically.

Out of Scope

  • Hook rendering / reader cutover (§2.11 Ph3–4) — hooks are pure bounded renderers of the published projection; this ticket publishes, it does not render.
  • Lease renewal / heartbeat. Wave 1 is bounded single-publication by ADR decision; renewal requires ADR revalidation, so proposing it here is out of scope by construction.
  • New producers. #15267 and #15087 are the Wave-1 inputs; this consumes them.
  • Ranking, scoring, or curation. The composition layer may concatenate produced envelopes preserving provenance/freshness/emptiness/degradation/citations; it may not read content to rank, weight, infer, or curate per turn.
  • Quarantine/migration of the legacy unprovenanced lifecycle-state file — a separate §2.11 concern.

Avoided Traps

  • Atomic rename without singleton enforcement — explicitly rejected by Epic #15100: it prevents torn reads but not multi-writer lost updates. The rename is necessary and not sufficient; the transaction around it is the actual property.
  • Fencing epoch in the JSON payload as the guard. It is diagnostic only. A reader-visible epoch cannot stop an old holder from mutating the resource — only the serialized transaction wrapping the rename can.
  • A remote lease + an unguarded local write. Looks correct, fences nothing. This is precisely why §2.6 puts the writer next to the broker.
  • A default leaseTtlMs. A primitive-local default (even a literal) silently converts a config gap into a plausible-but-wrong lease window (ADR-0019; and the live snapshot-drift trap in #15294 makes an absent leaf a realistic state, not a hypothetical).
  • Renewal "because a render might be slow." Wave 1 chose bounded-no-renewal deliberately; a slow render must abort loudly rather than quietly hold a target.

Related

  • Parent epic: #15100 (Live Lane Awareness — Wave-1 composition). This is its missing critical-path leaf.
  • Producer inputs: #15267 (lifecycle frontier, PR #15295), #15087 (typed route, merged).
  • Blocked consumers: §2.11 Ph3–4 reader cutover; #15265 (hook advisory serves fixture rows) is the live symptom of having no published projection.
  • Authority: ADR 0035 §2.6 (learn/agentos/decisions/0035-live-lane-awareness-composition.md), graduated from Discussion #15090.
  • Config-boundary trap to avoid at wiring time: #15294.

Live latest-open sweep: checked latest 20 open issues at 2026-07-16T18:32:51Z; no equivalent found (searched additionally for projection writer lease, single-writer, fenced lease projection, ADR 0035 writer across open+closed — only the epic and its producers match). A2A in-flight [lane-claim] sweep at 2026-07-16T18:32Z across the last 12 claims: no overlapping claim.

Retrieval Hint: hook projection writer fencing epoch lease serialized transaction rename current.json

Origin Session ID: ad475320-6bdc-4555-ba3f-b78d51de0b17