Skip to main content

Module executor

Module executor 

Source
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§

CapabilityExecutor
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: SWITCH set to off.
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.