Expand description
Friction ledger — the engine’s record of its own surface’s learnability (agent-trust plan 08).
Every typed refusal a surface returns is appended as one JSONL line
to a workspace-local, gitignored, size-bounded ledger under
.memstead/state/friction/. The ledger answers “which refusal
codes do agents actually hit, on which verbs, how often” as a query
instead of an anecdote — the evidence substrate for surface-design
changes.
§Hard lines (recorded contract)
- Privacy: every recorded field’s value space is a closed,
engine-defined vocabulary — values that exist as literals in
engine source (
"cli"/"mcp", the tool/subcommand name, the UPPER_SNAKE_CASE refusal code, the per-code reason discriminators inclosed_reason) plus the epoch-seconds timestamp. Never parameters, entity ids, message text, free-form strings, or any payload content — a candidate field whose values a caller can influence is out of bounds no matter how useful. The write path enforces this by type: reasons enter only as&'static strdrawn from the vocabulary table. - Local only, forever: the ledger never leaves the machine —
no transmission, no registry involvement. The read surface is
memstead health --include friction(and the MCP counterpart). - Refusals only: successful operations are never recorded — this is a friction ledger, not telemetry.
- Best-effort, never perturbing: recording must not affect the refusal path. Every ledger I/O error is swallowed; the refusal returns unchanged whether or not the append landed (an unwritable state dir degrades to not-recording).
§Concurrency and bound
Appends are one write syscall of one complete line on an
O_APPEND handle — concurrent writers (a CLI invocation beside a
running MCP server, the normal state of a live workspace) interleave
whole lines, never tear them. The bound is rotation: when the
current file reaches the cap it is renamed to <name>.1 (replacing
the previous generation), so at most ~2× cap bytes exist on disk. A
concurrent rotation race loses at worst the rename (swallowed) —
entries keep landing in whichever generation the writer’s handle
points at, every line still complete.
Structs§
- Friction
Entry - One ledger line. Every field’s value space follows the module’s closed-vocabulary rule (see the privacy hard line above) — that rule, not this struct’s current shape, is the contract.
- Friction
Ledger - Append-side handle. Cheap to construct per refusal — no state beyond the target path and the cap.
Constants§
- DEFAULT_
CAP_ BYTES - Rotation threshold for the current generation. At ~80 bytes per entry this holds >6k refusals per generation — months of normal friction — while bounding the ledger to ~1 MiB across both generations.
Functions§
- closed_
reason - The per-code closed reason vocabularies, and the only gate through
which a reason reaches the ledger. Given a refusal’s
codeand its structureddetails, returns the matching engine-source literal when — and only when — the code has a declared vocabulary AND the details’reasonvalue is a member. The return is the vocabulary’s own&'static str, never the input string, so a caller-influenced value can only ever select from (not extend) the closed set; any other value records nothing. - friction_
ledger_ path - The current-generation ledger file path for a workspace.