Expand description
The one execution path.
Every call of an executable capability — from the stdio MCP session, from /mcp, from
an HTTP route, from the command line, from the Cockpit or from a benchmark runner —
goes through CapabilityExecutor::execute: registry lookup, counters, the cache the
capability’s own policy asks for, then the handler. Transport adapters convert protocol
to JSON and back and own nothing else, so instrumentation and caching apply to every
transport at once and cannot drift apart. A second execution path is the defect this
module exists to make impossible.
§The cache key
Process memory, bounded per capability by its CachePolicy, keyed
by three things: the canonical id, the input normalised (object keys sorted at every
level, so {"a":1,"b":2} and {"b":2,"a":1} are one entry), and the registry
fingerprint — which hashes every descriptor and every declarative object’s content. Two
processes over different repository states therefore never share an entry, and a changed
layer never answers from an old one.
Errors are never cached. Commands are never cached, and the registry refuses a descriptor that asks for it, so that is a build-time refusal rather than a runtime one.
Two policies narrow the key further, for answers the registry fingerprint cannot see
move. CachePolicy::Generation adds the context’s Context::reading: an answer
read off the index, including the git state it was read at, is kept for as long as that
reading is the one asked. CachePolicy::Checkout adds that and the fingerprint of the
checkout, taken again on every call (capability::checkout): HEAD, the staging index,
the refs, and every changed or untracked path of the work tree with its size and
modification time. A checkout git cannot fingerprint is not cached.
§One handler per key at a time
Concurrent calls that miss on the same key do not each run the handler: the first runs it, the others wait for it and are answered from what it stored. A screen that asks the same source from two panels at once, or two browsers that open one page together, pay for one computation. A handler that fails stores nothing, so a caller that waited on it runs the handler itself.
§Switching it off
MAJORDOMUS_CAPABILITY_CACHE=off in a process’s environment makes every call run its
handler and store nothing, whatever the capability’s policy: the way to settle a doubt
about a cached answer by asking without the cache (project.cache-is-invisible).
§Invisibility
A cached answer is byte-identical to a computed one; nothing in a response says which it
was. project.cache-is-invisible requires it, and canonical_json is what makes the
key well-defined enough for it to be true.
use majordomus_cli::capability::executor::canonical_json;
// the same input written two ways is one cache entry, at every level of nesting
let one = serde_json::json!({ "b": 2, "a": { "y": 1, "x": 0 } });
let other = serde_json::json!({ "a": { "x": 0, "y": 1 }, "b": 2 });
assert_eq!(canonical_json(&one), canonical_json(&other));
// and a different input is a different entry
assert_ne!(canonical_json(&one), canonical_json(&serde_json::json!({ "b": 3 })));Structs§
- Capability
Executor - The executor of one registry.
Constants§
- SWITCH
- The environment switch that turns the capability cache off for a process.
Functions§
- bypassed
- Whether this process was told to run every capability without its cache:
SWITCHset tooff. - canonical_
json - The input as one canonical string: object keys sorted at every level, no whitespace, so that two inputs equal as data are equal as keys whatever order a client wrote them.