Skip to the content.

Multiple-sink transmission for the cache lifecycle (2026-06-23)

A working note on one question: is multiple-sink transmission — one kernel op fanning its resolution across every registered ProvisionalSink — actually useful for the lifecycle of the KV / context cache? Short answer: yes, and it is the right shape, but its value is latent today (one registrant, never exercised with more than one, and the ABI doc describes a second registrant that does not exist). This note records the mechanism, the gap, the honest semantics, and what to wire next. It ships with an executable witness so the conclusion is a test, not a paragraph.

What “multiple-sink transmission” is here

abi.ProvisionalSink is a frozen seam (internal/abi/types.go):

type ProvisionalSink interface {
	Promote(ctx context.Context, txn TxnID, epoch uint64) error
	Rollback(ctx context.Context, txn TxnID, epoch uint64) error
}

A driver that produces retractable effects registers one with abi.RegisterProvisionalSink. The registry keeps a []ProvisionalSink. The reserved OpsSpec ops OpSpecCommit / OpSpecSquash (internal/spec/spec.go, op.Invoke) fan the chosen resolution across every registered sink for one (Txn, Spec.Epoch):

for _, s := range abi.ProvisionalSinks() {
	if o.commit { err = s.Promote(ctx, c.Txn, c.Spec.Epoch) } else { err = s.Rollback(...) }
	...
}

That fan-out is the transmission. The idea: a speculative or transactional turn touches several cache tiers at once; one kernel commit-or-squash should resolve all of them together, instead of each cache exposing a private rollback verb the caller has to remember to call.

Why it is useful for the cache lifecycle

The gap: built, dormant, doc-misrepresented

The honest semantics: best-effort, not atomic

The fan-out is not two-phase. op.Invoke iterates sinks sequentially, keeps only the first error, continues past a failing sink, performs no compensation of sinks already resolved, and still returns the success Outcome (only Status flips to StatusError). So a mid-fan retract failure leaves a torn cross-cache state. Today that is harmless only vacuously: the single shipped sink’s Promote/Rollback always return nil, so the error path can never fire.

This is a real property, not a bug to hide. Two things make best-effort defensible for now, and one names the upgrade:

What to wire next (ranked)

Cache tier Priority Why
radixkv (prefix/radix KV cache) high Retract (EvictNodeKVCache.Evict) is already bit-exact; the only gap is registration + (txn,epoch) node metadata. A squashed turn currently leaves phantom prefix nodes that skew hit rate in the exact speculative workload the seam serves.
ctxplan.Index (candidate index) high Append-only Add with no Retract, so a squashed turn’s spans persist and poison the candidate set. Needs one Retract(spanID) removing from spans/byID/posting/durable, then register. Conceptually the cleanest second sink.
ctxmmu (context-MMU) medium The intended canonical sink, but its current retract is post-admission FIFO aging, not a pre-admission draft-then-promote/rollback. Becoming a real sink is a redesign, not a wiring change.
vdso (tier-2 result cache) no Category mismatch: invalidation is consistency-driven (external-witness refutation on the coherence bus), not transaction-driven. Entries carry no (txn,epoch).
audit / decision journal no Must never be a sink. It is append-only, hash-chained, tamper-evident; an auditor should see decisions about calls that were later squashed. Keeping it out of the fan is a correctness property.

radixkv and ctxplan were peer-hot at the time of writing — wire them when their trees go quiescent, and only after the fan-out contract is itself a witness (it now is).

The witness shipped with this note

internal/spec/spec_test.go:

Follow-ups

  1. Correct the internal/abi/types.go ProvisionalSink doc comment (and spec/doc.go, ARCHITECTURE.md) — drop the claim that ctxmmu registers a sink, or make it true.
  2. Wire radixkv as the first real second sink (node (txn,epoch) metadata + RegisterProvisionalSink) once the lane is quiescent.
  3. Add ctxplan.Index.Retract + register it, so a squashed turn leaves neither stale KV nor a phantom index entry.
  4. Decide explicitly: keep best-effort + idempotent re-drive, or upgrade the fan to 2PC / compensation. The witness makes either choice a visible contract.