Skip to main content

Module friction

Module friction 

Source
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 in closed_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 str drawn 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§

FrictionEntry
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.
FrictionLedger
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 code and its structured details, returns the matching engine-source literal when — and only when — the code has a declared vocabulary AND the details’ reason value 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.