Skip to content

transform_decisions cannot explain tool-set-driven prefix busts: cached_m0_tool_set_hash is permanently NULL #349

Description

@iceteaSA

Observability gap, not a behavior change. I am explicitly not asking for the fold trigger back — see "What I'm not asking for" below, which quotes the existing decision and concedes it.

What happened

Six sessions on one box took full prefix busts within seconds of each other. Cause, established by byte-diffing the provider request bodies across the boundary: the tools array went 131 → 132 entries when a plugin registered a new tool at startup. Zero definitions changed, zero removed, order stable. Tools precede system in the cached prefix, so a single added tool invalidates from byte ~0.

MC's markers could not explain it. transform_decisions for the affected session showed 19 consecutive passes with materialized=0 straddling the bust — correct, and useless. system_hash covers output.system.join("\n") (system-prompt-hash.ts:443), and the array that changed is not in it.

The forensic cost was a wrong attribution: with the client-side signal negative, the bust was filed as provider-side eviction. That's a materially different conclusion — eviction is an infrastructure problem you can't fix, tool registration is a local one you can schedule. Only an out-of-band byte-diff of the request bodies corrected it.

The field already exists and is never written

session_meta.cached_m0_tool_set_hash          — column present, migration v34
populated: 0 / 4629 sessions                  — never written on any session

storage-meta-session.ts:44   "NULL AS cached_m0_tool_set_hash"
storage-meta-shared.ts:590   ["cached_m0_tool_set_hash", null]
inject-compartments.ts       0 references

It is plumbed through the whole storage layer — schema, ensureColumn, the row validator, the marker map — and hardcoded to null at every write site. Vestigial, left behind when the trigger was removed.

What I'm not asking for

ARCHITECTURE.md is explicit, and I agree with it:

Deliberately NOT triggers … and tool-set-hash change (process-global, false positives).

That reasoning is correct and this incident reinforces it. A tool set is process-global, so folding on tool-set change would fold every session in the process at once — MC manufacturing a fleet-wide bust of its own. In this incident the provider busted those six sessions regardless of what MC did; folding would have prevented nothing and added MC's own bust on top.

Not folding was the right call. Not recording is the gap. MC cannot explain after the fact what it correctly declined to act on.

Proposal

Populate the existing column and carry the value into transform_decisions as an observed-state field, alongside the pass's other markers. No new trigger, no change to mustMaterialize, no behavior change of any kind.

tools[].name is sufficient — definitions rarely move without names, and names alone caught this case. Hashing full definitions would be strictly more expensive for no demonstrated gain.

Why it's worth the column

With it, "MC's markers show no change" becomes a complete statement about the request rather than a statement about MC's slice of it. Right now three classes are invisible to transform_decisions:

  1. Tools array — this issue.
  2. Anything injected below the plugin — a transport layer adding per-request bytes after the transform.
  3. Midnight date rollover — deliberately normalized out (system-prompt-hash.ts:403-430), correctly, but it means a date-driven change cannot be distinguished from no change.

(1) is the one that's cheap to close and already has its column sitting in the schema.

Related: #348 (record the hash values behind a fold reason, not just the reason). Same class — record what was observed, change nothing about what is done — and the same table.

Counter-position

The honest case against: MC is a context-management plugin, not a cache-forensics tool, and request-body composition is the harness's business. If transform_decisions is meant strictly as a record of MC's own decisions rather than of the request state MC observed, then this belongs in a harness-level tap and the vestigial column should simply be dropped instead. That's a legitimate call and I'd rather have it made explicitly than leave a permanently-null column in session_meta implying a capability that isn't there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions