Skip to the content.

vToolcall — Tool Results as a Materialized View

Date: 2026-06-25 Status: design note over a substrate that is mostly already shipped. The serve-from-cache-as-if-the-tool-ran mechanism exists today as the vDSO FastPath (internal/vdso, consulted at internal/kernel/kernel.go:347 inside Kernel.Submit) and is reachable now through fak_syscall (internal/gateway/mcp.go:235,431) and the in-kernel agent loop. The closed-loop invalidation core — hierarchical epoch vector (scope.go), refutation/revoke (revoke.go), per-principal isolation (principal.go) — is implemented and tested. What is not built: witness-keying of the tier-2 key, a cross-process shared cache (only revocation crosses processes), a context-level ProvisionalSink for speculative un-admit, and any result-injection disposition in the PreToolUse hook protocol. Those are named honestly below and gate the speculative rungs.

Audit provenance (2026-06-25). Every fak symbol named below was grounded against HEAD before writing. Five corrections from that pass are folded in and load-bearing: (1) topologyinternal/gateway/gateway.go:1-27’s own package doc states /v1/chat/completions (and the Anthropic /v1/messages twin) “does NOT execute the client’s tools (the client does)”; the FastPath that synthesizes results is reached only via k.Syscall, never the proxy path. So “hand the harness a cached result as if the API returned it” is real for fak_syscall/in-kernel and structurally impossible on the provider-forward proxy. (2) The contextq verdict set (MaterializationKind: HIT/FAULT/RECOMPUTE/REFUSE/ABSTAIN, contextq.go:164-168, five) and the cachemeta verdict set (LookupKind: hit/miss/revalidate/transform/quarantine/fault, cachemeta.go:245-250, six) are not byte-identical and not aliases — they overlap on HIT/FAULT and diverge elsewhere; any unified verdict needs an explicit, lossy mapping, not a rename. (3) The witness is not bound into the tier-2 key — revoke.go:44-49 says so verbatim; two agents reading the same file under different git SHAs still share by (tool,args,worldVer). (4) vdso.Default is process-global, not cross-process: the FILL half of a shared cache is unbuilt; only Revoke is on the wire (POST /v1/fak/revoke). (5) The context-MMU (internal/ctxmmu) does not implement abi.ProvisionalSink; the only registrant is internal/spec.Sink, which retracts a KV-cache token span for speculative decode, not an admitted tool-result page from context. Trust-law claims (ProviderCacheVerdictLookupTransformCanServe()==false, cachemeta.go:303,320; materialization.go:189-191) reproduced exactly.


0. The one-sentence idea

Everything presented to the harness is a materialized view over a source of truth the kernel controls — and a tool result is just one more view. When the kernel already knows a tool’s result (it ran a Read last turn, or a peer did), it can serve that result as if the tool had just run, skipping the round-trip — provided the view is still fresh by a predicate cheaper than the call itself, and provided the kernel is the one dispatching the tool.

vToolcall is contextq’s materialization machine pointed at the tool-result wire instead of the inbound-context wire. The scarce thing it owns — and the reason it can be sound where a provider KV cache cannot — is closed-loop invalidation: the kernel adjudicates every mutating call and observes every completion (vdso.Emit), so it sees the write that invalidates a read and can strand the stale entry before it is ever served. The spine of this note is that invalidation, because the failure it prevents is not a wasted token (vCache’s worst case) but a corrupted answer (the model reasoning over a file that no longer exists on disk).


1. The polarity flip from vCache (why this note exists at all)

vCache (docs/notes/VCACHE-VIRTUAL-API-CACHE-2026-06-24.md) caches a provider KV prefix. Its Law A2 — correctness never depends on warmth — is true there because the model re-derives truth from a synthesized prefix: a stale prefix costs tokens, and any divergence self-corrects as the model reads the resent context. A wrong belief is expensive, never wrong.

A tool result is the opposite. The model consumes it as ground truth and cannot re-derive it — it has no other channel to the file’s bytes. So the lethal failure inverts:

  vCache (KV prefix) vToolcall (tool result)
What is cached a warm prefix the model re-derives from a result the model reasons over
Worst case of a false hit pay full price, book a phantom saving model reasons over stale bytes → corruption
Failure class cost / latency correctness
Invalidation loop open (provider is opaque; warmth is a belief) closed (kernel sees its own writes)
Safe posture “correct-and-cheaper-in-expectation, never dependent on a hit” sound-before-serve: a hit must equal a fresh call at serve time

This flip is the whole design. vCache may serve a stale prefix because the cost is bounded; vToolcall may not, ever — but it has the closed loop that lets it guarantee it won’t. Law T (the vToolcall soundness invariant, dual of A2): a served view must equal a fresh execution at the moment of serving. vdso.go:13-17 already states it: “a cache hit equals a fresh call.” Everything below is in service of that one sentence.


2. Topology — who runs the tool decides what is even possible

This is the question that makes or breaks the concept, and the answer is it depends on who dispatches the tool. There are three surfaces, with structurally different powers.

2a. REAL + SHIPPING TODAY — fak_syscall / in-kernel (the kernel IS the dispatcher)

When a caller routes a tool through fak — an MCP client calling fak_syscall, or fak running its own agent loop — the path is mcp.go:235 → k.Syscall → Kernel.Submit, and Submit consults abi.FastPaths() at kernel.go:347 before adjudication. A vDSO Lookup hit returns abi.Verdict{Kind: VerdictAllow, By: "vdso"} with the result pre-stored, carrying served_by=vdso and a tier tag. This is literally “serve from cache as if the tool ran,” and it is wired now. fak_syscall is already advertised in tools/list (mcp.go:431).

The first shippable rung therefore has nothing to add on the serve path — it exists. The rung is adoption: a Claude Code MCP client that calls its read-only tools via fak’s MCP server gets dedup + cache today. Optional polish: surface served_by/miss-reason (vdso.MissReasons) in the tool result so the client can distinguish a hit from a fresh run.

2b. STRUCTURALLY IMPOSSIBLE — the /v1/messages provider-forward proxy

On fak guard -- claude, fak is a proxy downstream of the harness. The wire cycle is strict: the model emits tool_use (id preserved) → Claude Code executes the tool locally → it sends back a tool_result keyed by tool_use_id on the next request → fak parses it via DecodeAnthropicMessagesRequest (tool_use_id → ToolCallID) and admits it via admitInboundResults (gateway.go:1316). The proxy path runs s.adjudicate → k.Decide only — no FastPath, no Lookup, no dispatch (confirmed: messages.go/messages_stream_planner.go call admitInboundResults, never abi.FastPaths()). The model API does not run the client’s local tools; there is nothing for fak to short-circuit. “Hand the harness a synthesized tool_result as if the API returned it” is the wrong direction for this surface, full stop.

What fak can do here is result-RECOGNITION, not interception: a tool_result whose (tool,args)→result the vDSO already holds is a signal the inbound block is redundant and can be compacted out of the forwarded prefix — the same byte-splice discipline as promptmmu.CompactInboundTools / agent.CompactAnthropicHistory. This is Law-A2-clean (cost/latency only; the harness’s real result is still the one forwarded), and it is the only legitimate cache use on the proxy wire.

2c. NET-NEW, BLOCKED ON A PROTOCOL — the PreToolUse hook

tools/repo_guard.py:402 (run_hook) is the existing PreToolUse shape: it reads the tool call on stdin and emits hookSpecificOutput.permissionDecision. It does intercept before the harness executes — the only place “serve before the harness runs it” is real for an external harness. But the protocol it speaks carries allow/deny only; it has no result-injection disposition. A vToolcall hook would canonicalize (tool,args), consult a fak cache, and emit a result — which requires the harness to accept a hook-supplied result. Until that disposition exists, this rung is blocked on the harness contract (a time-sensitive claim, dated 2026-06-25 — re-verify against the current Claude Code hook protocol before relying on the ordering). And because a hook is a separate process with no EvComplete feed, its cache runs open-loop and must bind an external witness to stay sound — the same machinery 2a gets for free.

The deciding asymmetry: 2a is not just the easy rung, it is the strong one, because invalidation is closed-loop there and open-loop everywhere else. Ship 2a first.


3. The safety taxonomy — pure / read-mutable / effectful

A tool call may be served from a view iff serving it is observationally indistinguishable from running it fresh. The admission floor is the conjunction of two independent predicates, both required:

  1. SOUND — no false hit is ever possible. Every served entry is bound, at serve time, to an invalidation key that has strictly advanced if reality changed in a way that could alter the result. Serving can only ever degrade to a MISS → engine; it can never invent a stale hit. (The revoke.go lemma generalizes: every gate only turns a would-be hit into a miss.)
  2. CHEAPER-THAN-THE-CALL — the invalidation predicate’s evaluation cost is strictly less than executing the tool. A predicate as expensive as “re-run and diff” is sound but pointless — it deletes the speed win. This is the tool-result form of vCache’s recall.go break-even gate (P·r < S·U): serving from cache is worthwhile only when the check is amortized-cheaper than the call.

Soundness alone is insufficient; that is the load-bearing addition over “just cache it.”

The three classes map onto the three vDSO tiers:

Class Definition Served from view? Invalidation predicate vDSO seam
pure result is a total function of args (no world state) YES, unconditionally none — args-sha256 is the key; re-derived not trusted tier-1 pure registry (vdso.go:351)
read-mutable reads world state other actors can write YES, behind a sound+cheap predicate (tool, args-hash, principal, epoch-chain) matches current tier-2 cache + keyLocked; epoch bumped by Emit on any write-shaped completion (vdso.go:468)
effectful writes / mutates / sends; or a read whose freshness is not nameable NEVER n/a — no sound cheap predicate exists, so the floor refuses destructive(c) veto (vdso.go:299) + resourceMisnamed gate (scope.go)

The third row’s hard NEVER does the safety work, and the vDSO encodes it two ways that cannot drift: IsWriteShaped (a name heuristic over writeShapeNeedles: write/edit/delete/patch/exec/run/send/book/update/cancel — vdso.go:282) OR c.Meta["destructive"]. The heuristic deliberately over-approximates (a read-only rundown_report is excluded because its name contains “run”). That is the correct error direction: a false “effectful” costs one cache miss; a false “pure/read” costs corruption.

The decision procedure a hook (or Lookup) runs, top-to-bottom, first match wins, every deny → fresh execution:

admit(call c):                                          # decision, miss-reason
  1. if destructive(c):            return DENY(DESTRUCTIVE)      # effectful → never served
  2. if not (readOnlyHint and idempotentHint):
                                   return DENY(MISSING_HINTS)    # cannot prove read-shaped
  3. if classify(result(c)) != Cacheable:
                                   return DENY(SECRET)           # Law D4, fail-closed (§6)
  4. if c.Tool in pure:           out,ok = pure(args)           # tier-1: RE-DERIVE, don't trust
        if ok: return SERVE(out, tier=1, taint=servedTaint(c))
  5. if c.Tool in static:         return SERVE(static, tier=3)  # args-independent canned
  6. if granularity==Resource and resourceMisnamed(c,args):
                                   return DENY(RESOURCE_MISNAMED)
     key = keyLocked(c,args)                                     # tool:hash(+principal):epoch-chain
     if e = cache[key]:
        if revokedLocked(e.witness): evict(e); return DENY(WITNESS_REVOKED)
        return SERVE(e.ref, tier=2)                              # epoch matched ⇒ sound hit
  7. return DENY(NOT_CACHED)

A misclassification escape hatch must be stated honestly: a custom mutating tool named apply or commit_changes (if neither hits a needle) whose author also set readOnlyHint=true would be admitted as read-mutable and could serve a stale view of a mutated world. The effectful floor is sound for tools whose names hit writeShapeNeedles or carry Meta["destructive"]; for everything else it is only as sound as the hints, and a mis-hinted mutator defeats it at runtime. A definition-time toollint can catch the obvious cases, but it is not a runtime guarantee.


4. Invalidation — the spine

The user flagged cache invalidation as load-bearing. It is, and the killer insight is closed-loop: vCache cannot see a provider evict; the kernel can see its own write. fak ships the closed-loop core; the work is not a new cache but making the existing eraser explicitly layered and resolving the one place the loop is genuinely open.

Four layers, ordered by trust-strength of the signal each consumes

All four key into the same tier-2 entry (entry{key, ref, witness}):

LAYER 1 — GENERATION EPOCH (closed-loop, kernel-mediated writes). The ground truth and the killer advantage. Already shipped. Emit (vdso.go:468) sees every EvComplete; destructive(c) bumps worldVer (root) or a nodes[tag] epoch via the hierarchical vector in scope.go ("*" → namespace → namespace:entity). A read binds the epoch of every node on its root→leaf chain in keyLocked; the key changes iff any node is bumped. Because the kernel saw the write, a read of a file the kernel just wrote cannot hit a stale entry — the epoch it was filed under no longer matches. A write that can name its entity strands only that subtree; an un-nameable mutation falls to root (*) = full flush — the sound over-approximation. This is the file-path generation counter, generalized to a hierarchical vector.

LAYER 2 — STAT PROXY (the half-open loop: writes fak did NOT mediate). This is the one place the closed loop breaks — a peer session, an external editor, the compiler. The kernel never saw the write, so no epoch bumped. The fix binds a cheap world-state witness = sha(path, mtime_ns, size) captured at fill time into entry.witness (the field exists; the FS witness function does not yet). Two sub-mechanisms: (a) a cheap re-stat on the hit path before serving — far cheaper than re-reading and re-feeding — treating a witness mismatch as a forced miss; (b) for cross-process writes, a filesystem watcher (or the peer’s kernel) calls Revoke(witness) (revoke.go), which evicts every entry under that witness and broadcasts a Revocation so other agents are causally evicted. Honest status: layer 2a is the gate the sound-before-serve rule needs on a shared tree, and it is unbuilt — metaFor (gateway.go:1517) threads only readOnlyHint/idempotentHint, never a witness. Stat is a proxy, not proof (mtime granularity + same-size edits alias), which is why layer 3 exists.

LAYER 3 — CONTENT-HASH, VERIFY-THEN-TRUST (ground truth, on the BACKFILL path, never as a serve gate’s only basis). The CAS digest is the content hash. Its role is to adjudicate the stat proxy’s may-aliases asynchronously: when layer 2 says “unchanged” but certainty is wanted (a security-sensitive tool, a same-size edit), an async re-execute hashes the result and compares to the stored digest. Match → the served hit was correct (the common case; FaultsAvoided). Differ → the entry is poisoned: Revoke(witness) it for the next turn and raise the integrity high-water mark. This is verify-then-trust on re-admission, not a license to retroactively fix a serve that already happened.

LAYER 4 — TTL BACKSTOP (the unwatchable). For results the kernel can neither adjudicate (L1), stat (L2), nor cheaply re-hash (L3) — a network feed, a clock-dependent read, a non-deterministic Bash. A per-tool max-age stamped at fill; past it the entry is a forced miss regardless of epoch/witness. The only probabilistic layer; default short or zero (fail-closed, matching resourceMisnamed’s refusal to cache a read no write could invalidate). Never cache the genuinely-unwatchable as if it were watchable.

The un-ring problem, resolved

You cannot un-ring the bell. Once the model consumes a stale tool_result it is in the KV/history — on the Anthropic wire the harness already POSTed it back as a content block. Therefore the predicate must be SOUND BEFORE SERVING, and “serve fast, then backfill-verify, correct-if-wrong” must be rejected as the default for tool results. The user’s framing of “serve then backfill the cache/request” splits into three mechanisms with very different danger:

Why speculative-serve is the weakest rung, stated bluntly. The mechanism that would un-admit a synthetic context page is not wired: internal/ctxmmu does not implement abi.ProvisionalSink; the only registrant is internal/spec.Sink, whose Rollback evicts a KV-cache token span for speculative decode (EAGLE/Medusa-style), not an admitted tool-result page. Worse, even with that wiring, “recoverable” overclaims: once the model emits its next turn conditioned on the fake result, those tokens are causally downstream — a correction turn appends a contradiction, it does not un-condition the model. And worst of all, the reconciliation trigger (WitnessTracker.Observe re-observing the resource) requires the real tool to run anyway — so the speculative serve pays the very cost it tried to avoid (the trigger and the cost are the same event). The economic gate (P·r < S·U) independently refuses a lone speculative serve: it pays only on a sibling fan-out sharing one hot prefix. Conclusion: speculative-serve is net-negative for almost every real tool call. Ship backfill-the-cache; treat speculative-serve as research, gated behind the unbuilt sink.

“Correct-if-wrong” precisely defined: (a) Revoke the poisoned entry, (b) bump trustEpoch + publish a Revocation, (c) raise the integrity high-water mark, (d) only if the call was idempotent-to-redo and the stale result has not yet entered committed context — substitute the fresh result. It never means editing a tool_result the model already read.

The shared-tree reality (the repo’s own git model) — and where it does not pay off yet

A multi-session trunk where peers edit the same files is the read-mutable hazard at fleet scale, and it needs all three orthogonal axes:

  1. CONSISTENCY (worldVer / node epochs). Safe default on a shared tree is Global granularity: any peer write the kernel observes does worldVer++, full-flush — coarse but trivially sound. Per-file Resource precision is opt-in and requires the fine-writes-need-fine-reads invariant (scope.go:38-46).
  2. INTEGRITY (trustEpoch / witness). The git commit/blob SHA is the natural witness; Revoke(old_sha) evicts on a peer commit.
  3. ISOLATION (principal, principal.go). A per-session principal folded into the args-hash closes the cross-agent timing oracle. Empty principal = single-tenant sharing (byte-identical to v0.1); a named principal engages isolation.

The honest verdict for this repo’s workload. Three facts compound against shared-tree caching today: (i) the witness is not in the tier-2 key (revoke.go:44-49), so the only sound posture is Global = full flush per peer write; (ii) the cross-agent break-even cliff is ~1% writes (scope.go:8-13) and the repo’s own trajectory audit shows a write-heavy mix (Edit ≈ 99, Bash ≈ 94 vs Read ≈ 50 — far above 1%); (iii) the FS namespace is not tagged in nsKeywords, so the finer-eraser escape that would rescue the economics does not exist for Read/Edit/Glob. vToolcall over the shared git tree is net-negative until witness-keying and FS namespace tagging ship. The note refuses to claim otherwise. The single-session, own-writes-only case (Layer 1) is the sound win available now.

A second cross-process honesty note: vdso.Default is process-global. Two fak processes have independent tier-2 maps — session B’s process has an empty LRU, so “peer A read it, B is served from the pool” does not happen. Only the revoke half crosses processes (POST /v1/fak/revoke); the FILL half (a shared/persistent pool) is unbuilt. And the revoked-witness ledger fails closed on overflow (>8192, DefaultRevokedWitnessLimit): under high commit churn the integrity axis self-disables to “refuse all witness-bearing serves” — sound, but it turns that axis off as a speed win.


5. The materialized-view architecture (the unifying frame)

fak has, four times independently, built the same machine: take a backing source of truth, decide materialize or reuse a projection, gate the decision on freshness + trust, and emit a render layout.

View Backing source Existing type / file
tool-DEFS declared tool schemas promptmmu.CompactInboundTools — byte-splice rewrite, anchored on last cache_control breakpoint, fail-safe identity
HISTORY prior messages agent.CompactAnthropicHistory — prefix-preserving memcpy through last breakpoint, fail-safe identity (dogfooded 6203→3486B)
CONTEXT pages CDB image pages contextq — the reference implementation; MemoryViewRecord, ViewCache keyed (step, view_type, producer) excluding PolicyVersion so drift surfaces as RECOMPUTE not a silent miss
tool RESULTS tool executions vdso.VDSO — tier-2 entry keyed (tool, argHash, epoch-stamp); Emit = closed-loop invalidation; revoked[] = integrity epoch. This is vToolcall, already shipped as the vDSO.
KV prefix provider KV cache vcachechain + vcachegov + cachemeta.FromKVPrefix — and the two trust laws (A2, D4)

The unifying type names what is already there:

type View struct {
    Backing    SourceOfTruth        // SourceRefs + Producer + identity axes
    Policy     MaterializationPolicy // EAGER | LAZY | SYNTHETIC | SPECULATIVE
    Invalidate InvalidationPredicate // epoch-chain | witness | PolicyVersion | TTL
    Trust      TrustFloor            // Taint, Scope, Coverage, Faithfulness, CorrectnessBearing
}

Two claims to make carefully, because the naive version is false. First, the kernel-as-context-MMU framing is literal, not metaphor: contextq is demand-paging through a trust gate, vdso is the vDSO serving a result with no syscall, promptmmu is read-only-page sharing via byte-splice, and the render plan’s stable_prefix/volatile_tail split is the page-residency layout. Second — and this is where the adversary was right — the verdict vocabularies do not unify by renaming. contextq’s five (HIT/FAULT/RECOMPUTE/REFUSE/ABSTAIN) and cachemeta’s six (hit/miss/revalidate/transform/quarantine/fault) overlap only on HIT/FAULT; REFUSE has no single cachemeta twin and transform/quarantine have no contextq twin. A single Resolve() would need a tested, explicit mapping table, not the assertion that they are “byte-identical.” Treat the unification as a facade over the existing cachemeta.Entry (every machine already has a From* adapter into it) plus one Resolve() and one RenderPlan assembler — additive, not a rewrite of four hot paths. Ship that much; defer the unified verdict until the mapping is written and tested.

New views the frame makes obvious — labeled honestly as unvalidated design consequences, not capabilities:

  1. Redaction viewPolicy=SYNTHETIC, trust floor lowered (Faithfulness < 1.0); a new redaction rule bumps PolicyVersion → RECOMPUTE. The admitInboundResults quarantine-in-place path is already a degenerate redaction view.
  2. Counterfactual / what-if viewPolicy=SPECULATIVE, invalidation = never commits (a scratch epoch discarded), CorrectnessBearing=false. Lets a planner resolve a turn against a hypothetical set without mutating worldVer.
  3. Deterministic-replay viewPolicy=EAGER but frozen: pin every view to a fixed (epoch, policy_version, witness) so Resolve always returns the same verdict. Emit is the single record point; suppress write-bumps and recorded reads stay served.
  4. Multi-model shared-result view — the model-agnostic dual of KVView. A tool-result or summary view carries no ModelID in its trust-relevant axes, so the same view serves Opus and a local model — a cross-model win vCache structurally cannot give (KV is model-bound). vdso.shareable already does the principal-axis version of this drop.

The trap these four must respect. The two shipped, safe views (promptmmu, CompactAnthropicHistory) are byte-faithful splices with fail-safe identity — they never put synthetic content in front of the model. Redaction and counterfactual are SYNTHETIC content on the model-read wire; they cross the exact trust line those two were built never to cross, and putting them under the same View type with CorrectnessBearing=false would be a lie (a synthetic view the model reads is correctness-bearing by construction). They need their own threat model, not inheritance from the safe splices. State them as design consequences of the frame; do not present them as falling out “for free” — that is the same modeled-vs-measured honesty trap the project corrected on its webbench number.


6. Law D4 — never warm secrets (the orthogonal refusal that runs first)

Before any epoch or economics check, the content class gates admission. vcachegov.Warmable permits only Cacheable; Secret (credentials, tokens, PII) and SecretRegulated are refused — Secret unconditionally, SecretRegulated only through a deletion-capable surface. For vToolcall: a tool whose result carries secrets (a credential read, a PII fetch) is never cached, independent of read-shape. ClassifyPrefix is fail-closed: an unknown/unestablished class → Secret → refused. The rationale is the membership-oracle leak — cache-hit latency reveals “has anyone recently read string X.” The classifications must be closed-vocabulary, not runtime-determined, to avoid the timing side-channel.


7. Economics — when faking beats running

The gate from §3 restated as arithmetic: serve from a view iff the invalidation predicate is cheaper than the tool call, amortized over reuse. vCache’s recall.go form is P·r < S·U; the vToolcall form is per-tier.

Break-even, stated as the cliff it is: the predicate must beat the call. For a network read (tens of ms + token cost) the epoch compare wins by orders of magnitude. For a local Read (no network), fak’s cost to serve is small but so is the cost to run — and on the proxy wire fak cannot avoid the model re-feed anyway because it isn’t serving the result. The re-feed cost is the LLM’s, not fak’s; “cheaper than re-read + re-feed” overstates fak’s win on the proxy surface. The honest single-process win is execution-side latency + the cross-agent read-once-serve-N multiplier, not the single-session token bill (which the harness’s own prompt cache already largely harvests — measure with tools/session_audit.py, do not assume).

The killer use cases, ranked by how sound they are today:

  1. Pure / static / idempotent lookups (tier-1/tier-3: a content hash, a canned table, calculate) — zero coherence concern, sound everywhere. Lead with these.
  2. Deterministic record/replay for tests + debugging — a frozen corpus, zero live latency, witness-pinned to the recorded world-state; the one regime where you want the cache not to invalidate.
  3. Re-read of an unchanged file within one process where all reads AND writes route through one kernel — Layer 1 is exact and free; the second Read is served from tier-2 with no engine round-trip. Sound only under the kernel-mediated-writes precondition (or a content witness).
  4. Fan-out of parallel sub-agents sharing one read (read-once-serve-N via the agent-blind key or RegisterShareable) — the highest leverage, sound only when the sub-agents are not separate OS processes (one in-process kernel sees every write). Across separate processes, the FILL half is unbuilt (§4).
  5. Cross-session shared cache on the trunk — the largest blast radius and currently unbuilt (process-local cache; witness not keyed). A named follow-on, not a shipping capability.

8. Failure modes & design rules (adversarial)

Five named laws, each the distilled output of an attempt to break one mechanism.

Law T — a served view must equal a fresh execution at the moment of serving

The dual of vCache’s A2, with polarity swapped: a tool result is consumed as ground truth and cannot be re-derived, so a false hit is unrecoverable corruption, not a recoverable cost. Rule T1 — sound-before-serve. Serve only on epoch-match and (on a shared tree) a cheap witness re-check; every gate may only turn a hit into a miss. Rule T2 — backfill is telemetry, never a correctness license. “Serve fast, reconcile later” is legitimate only as backfill-the-cache (real bytes, refreshed for next time); serve-then-correct of a synthetic result is banned as a default because you cannot un-feed a consumed result.

Law A2′ — correctness never depends on a hit (inherited mechanically)

A vToolcall hit is served_by=vdso speed evidence, never proof. The trust floor is enforced in code, not prose: ProviderCacheVerdict returns LookupTransform and CanServe()==false (cachemeta.go:303,320; materialization.go:189-191). The tension to surface, not paper over: a speculative serve wants to put unverified bytes in front of the model now (that is the speed win), which is the opposite of CanServe()==false. You cannot both serve-to-the-model and mark-non-serveable. A2′ blesses backfill-the-cache (real bytes) and the labeling of synthetics; it forbids the speculative serve. Any [SHIPPED] claim resting on a verified==pending result is a fatal violation.

Law D4 — never warm secrets (runs first, §6)

vcachegov.Warmable permits only Cacheable; fail-closed on unknown; closed-vocabulary to deny the timing oracle.

Law C (closed-loop) — invalidation is closed-loop only within one kernel process

The differentiator over open-loop vCache holds per process: Emit observes only completions through this kernel. A harness-executed Edit, a peer’s git write, a compiler — none reach Emit, so worldVer never bumps and a subsequent tier-2 read would serve stale unless a content witness is bound. Rule C1 — on any non-kernel-mediated surface, the witness is mandatory, not a tightening. Rule C2 — cross-process coherence is revoke-only today; a shared FILL pool is unbuilt. Scope every “closed-loop” claim to one process and say so.

Law S (shared-tree) — the finer eraser is load-bearing and partly unbuilt

Rule S1 — Global granularity (full flush per observed write) is the only sound default until witness-keying lands; it net-loses past ~1% writes. Rule S2 — fine writes need fine reads (scope.go:38-46); a host wiring a new tool whose writes are entity-fine but whose reads cannot name the entity violates soundness — resourceMisnamed guards the tested shape, a misconfigured tagger is a foot-gun. Rule S3 — the FS namespace is untagged in nsKeywords; until it is, Read/Edit/Glob get root-only invalidation = full flush, so default this workload’s tier-2 reads off rather than claim a finer-eraser win that does not exist.

Residual risk (honest floor)

Even with every rule: on any surface where fak does not dispatch the tool, vToolcall cannot serve at all — it can only recognize a redundant result for compaction (§2b). Where it does dispatch, the witness is not yet in the key, the cross-process pool is unbuilt, and the speculative-serve sink is unwired. The genuinely sound, shipping wins are tier-1/tier-3 (always), deterministic replay (frozen), and single-process kernel-mediated reads (Layer 1). Everything else is gated on named follow-ons. Design posture, dual to vCache’s: be sound-before-serve and cheaper-when-you-serve — the moment a serve outruns its invalidation predicate, one un-mediated write turns a speed win into a corrupted answer.


9. What’s new vs what exists (the build boundary)

Reuse unchanged (shipped, grounded at HEAD):

Net-new (small, mostly specializations):

  1. FS-tool wiring into nsKeywords + entityOf (file path as entity) + a stat WitnessFunc — turns Layer 1/2 on for Read/Edit/Glob/Grep. Prerequisite for any shared-tree win.
  2. Witness-keying of the tier-2 key — the named follow-on in revoke.go:44-49; until it lands, shared-tree caching is Global-flush only.
  3. A cross-process shared/persistent tier-2 pool — the FILL half (only Revoke crosses today).
  4. Result-recognition compaction step in/near admitInboundResults — consult vdso.Lookup for a matching (tool,args) and mark a redundant block compactible (never replace a non-matching result). The only legitimate proxy-wire use.
  5. A verified=pending Meta convention + {"_synthetic":true} transcript wrapper + a CacheProvisional eventiff speculative serve is ever pursued (gated on #6).
  6. A context-level ProvisionalSink (internal/ctxmmu does not implement one) — the prerequisite for speculative-serve; until built, that rung does not exist.
  7. A PreToolUse result-injection disposition in the harness protocol — the prerequisite for rung 2c; out of fak’s boundary.

10. Phased plan

  1. R0 — Adopt the shipped serve path. Route a Claude Code client’s read-only tools through fak_syscall; surface served_by/miss-reason in the result. Nothing new on the serve path — the rung is adoption + observability. Sound for tier-1/tier-3 and single-process kernel-mediated reads today.
  2. R1 — Measure the addressable fraction. Extend tools/session_audit.py to key each Read by argHash(path) and count second-and-later occurrences with no intervening write — the exact vToolcall-addressable fraction, per session and machine-wide. No headline number until measured (the project’s modeled-vs-measured law).
  3. R2 — FS-tool wiring (net-new #1). nsKeywords/entityOf for file paths + a stat WitnessFunc (Layer 1 + Layer 2a). Single-process win first; default shared-tree tier-2 reads off until R3.
  4. R3 — Witness-keying (net-new #2). Bind the git SHA / stat witness into the tier-2 key so different-witness readers stop sharing — the precondition that makes shared-tree caching net-positive. Re-measure against the ~1% cliff before claiming a fleet win.
  5. R4 — Result-recognition compaction (net-new #4). On the /v1/messages proxy, compact a redundant tool_result the vDSO recognizes — cost/latency only, Law-A2′-clean.
  6. R5 — Cross-process pool (net-new #3). The FILL half of a shared cache; gated on R3 (witness-keying) for soundness.
  7. Deferred — speculative serve. Blocked on a context-level ProvisionalSink (#6) and an honest economic case that does not collapse to “the trigger is the cost.” Research, not roadmap.

11. Open questions