gate(engine): make a state_get with no producer a build error, not a silence #132

Open
tim.lingo wants to merge 1 commits from feat/gate-state-key-reads into main
Member

A state_get for a key nothing writes returns an empty string silently — no error, no warning, no log. That is exactly how #129 happened: history moved to a per-session key, one reader kept the old spelling, and the crisis-escalation half of the safety score received "" on every real conversation for two days with nothing failing. The same audit found three more of this shape. This makes the class a build error.

Why this is harder than grep, and why that matters

Keys are frequently computed. conv_hist_key(session_id) returns either "conv_history" or "session_hist_" + session_id. Other code does state_set("agent_workspace_root_" + sess, v). A naive literal matcher floods you with false positives and gets switched off within a day — a gate that cries wolf is worse than no gate.

So a key expression resolves to a set of patterns (EXACT "k" or PREFIX "head_"), never a bare string:

  • literal → EXACT; concatenation → fold left, keep the static head → PREFIX
  • helper call → union of its return expressions, params bound to that call site's actual arguments, guards constant-folded (so conv_hist_key("") yields only conv_history and does not falsely claim the session_hist_ namespace)
  • local variable → union of every let of that name in the enclosing function; parameter → union of the argument at that position across all call sites
  • unresolvable → listed, never fails the build

Coverage is directional: a write namespace same-or-broader than the read covers it; a narrower one does not. That direction is precisely what catches "the producer moved."

Two-leg proof

LEG A  pre-fix scratch copy                     LEG B  current tree
FINDINGS (1)                                    UNRESOLVED (0)
  chat.el:2536  state_get("conv_history")       FINDINGS (0)
    conv_hist_key() owns this key namespace     PASS
FAIL — exit 1                                   exit 0

Stronger than the synthetic leg — a blind test. Run read-only against feat/soul-openai-tools-v2, a branch it was never told about, the gate reported:

FINDINGS (1)
  chat.el:2937  state_get("conv_history")
      conv_hist_key() owns this key namespace (EXACT 'conv_history') — go through the
      helper, or a rename orphans this site silently
FAIL: 1 state-key finding(s).

That is the exact line a human had found by hand. Against fix/129-on-openai-tools (the fix): PASS. Re-run independently after the fact — same results, exit 1 and exit 0 respectively. Plus three producer-rename controls (EXACT key, PREFIX namespace, narrowed namespace), each correctly orphaning 3 readers across files.

Results on the current tree

278/278 sites resolve. UNRESOLVED: empty. False positives: zero.

Six findings are all TRUE positives, baselined in scripts/state-key-baseline.txt with causes and printed loudly on every run:

  • chat.el reads soul_identity five times — the writer was deleted 2026-05-13 in b163fa6 (a commit about awareness/ISE writes). The system prompt, vision handler, agentic prompt and two council handlers have been prefixing "" for roughly three months.
  • studio.el:57 emits "principal":"" — never wired.

Four further keys are host-set by design and declared in scripts/state-key-external.txt, each citing the source comment documenting its default.

One limitation I want stated, not papered over

The DEAD-READ check alone does not catch #129 — the dead handle_chat() still writes conv_history through the helper, so the key technically has a producer. It is the hand-rolled check (a literal spelled by hand inside a helper-owned namespace) that catches it. That limitation is written into the script header rather than glossed.

Rung: RUNS

Executes in 0.15 s; discriminates on four independent pairs; verdicts quoted above. Scripts-only commit — no .el changed, so nothing needed rebuilding.

CI wiring — safe, but deliberately not done here

Pure static read: no ports, no daemons, never touches ~/.neuron; needs only python3, which ubuntu-latest has; exits 0 on this branch. It would slot beside the existing soul-contract gate. Two reasons to hold: ci.yaml has someone else's changes in flight, and switching it on would immediately red feat/soul-openai-tools-v2 at chat.el:2937 — correctly, but that should be a deliberate, announced flip after that branch takes its fix. The workflow file was not edited.

Refs #129

🤖 Generated with Claude Code

**A `state_get` for a key nothing writes returns an empty string silently — no error, no warning, no log.** That is exactly how #129 happened: history moved to a per-session key, one reader kept the old spelling, and the crisis-escalation half of the safety score received `""` on every real conversation for two days with nothing failing. The same audit found three more of this shape. This makes the class a build error. ## Why this is harder than grep, and why that matters Keys are frequently computed. `conv_hist_key(session_id)` returns either `"conv_history"` or `"session_hist_" + session_id`. Other code does `state_set("agent_workspace_root_" + sess, v)`. A naive literal matcher floods you with false positives and gets switched off within a day — **a gate that cries wolf is worse than no gate.** So a key expression resolves to a *set of patterns* (`EXACT "k"` or `PREFIX "head_"`), never a bare string: - literal → EXACT; concatenation → fold left, keep the static head → PREFIX - helper call → union of its return expressions, params bound to **that call site's** actual arguments, guards constant-folded (so `conv_hist_key("")` yields only `conv_history` and does not falsely claim the `session_hist_` namespace) - local variable → union of every `let` of that name in the enclosing function; parameter → union of the argument at that position across all call sites - unresolvable → **listed, never fails the build** Coverage is directional: a write namespace same-or-broader than the read covers it; a *narrower* one does not. That direction is precisely what catches "the producer moved." ## Two-leg proof ``` LEG A pre-fix scratch copy LEG B current tree FINDINGS (1) UNRESOLVED (0) chat.el:2536 state_get("conv_history") FINDINGS (0) conv_hist_key() owns this key namespace PASS FAIL — exit 1 exit 0 ``` **Stronger than the synthetic leg — a blind test.** Run read-only against `feat/soul-openai-tools-v2`, a branch it was never told about, the gate reported: ``` FINDINGS (1) chat.el:2937 state_get("conv_history") conv_hist_key() owns this key namespace (EXACT 'conv_history') — go through the helper, or a rename orphans this site silently FAIL: 1 state-key finding(s). ``` That is the exact line a human had found by hand. Against `fix/129-on-openai-tools` (the fix): PASS. Re-run independently after the fact — same results, exit 1 and exit 0 respectively. Plus three producer-rename controls (EXACT key, PREFIX namespace, narrowed namespace), each correctly orphaning 3 readers across files. ## Results on the current tree **278/278 sites resolve. UNRESOLVED: empty. False positives: zero.** Six findings are all TRUE positives, baselined in `scripts/state-key-baseline.txt` with causes and printed loudly on every run: - `chat.el` reads **`soul_identity` five times** — the writer was deleted 2026-05-13 in `b163fa6` (a commit about awareness/ISE writes). The system prompt, vision handler, agentic prompt and two council handlers have been prefixing `""` for roughly three months. - `studio.el:57` emits `"principal":""` — never wired. Four further keys are host-set by design and declared in `scripts/state-key-external.txt`, each citing the source comment documenting its default. ## One limitation I want stated, not papered over The DEAD-READ check alone does **not** catch #129 — the dead `handle_chat()` still writes `conv_history` through the helper, so the key technically has a producer. It is the **hand-rolled check** (a literal spelled by hand inside a helper-owned namespace) that catches it. That limitation is written into the script header rather than glossed. ## Rung: RUNS Executes in 0.15 s; discriminates on four independent pairs; verdicts quoted above. Scripts-only commit — no `.el` changed, so nothing needed rebuilding. ## CI wiring — safe, but deliberately not done here Pure static read: no ports, no daemons, never touches `~/.neuron`; needs only python3, which `ubuntu-latest` has; exits 0 on this branch. It would slot beside the existing soul-contract gate. **Two reasons to hold:** `ci.yaml` has someone else's changes in flight, and switching it on would immediately red `feat/soul-openai-tools-v2` at `chat.el:2937` — correctly, but that should be a deliberate, announced flip *after* that branch takes its fix. The workflow file was not edited. Refs #129 🤖 Generated with [Claude Code](https://claude.com/claude-code)
tim.lingo changed target branch from fix/129-history-amplification to main 2026-08-07 15:55:42 +00:00
tim.lingo added 1 commit 2026-08-07 15:55:42 +00:00
Retires the defect class behind #129. The engine's state store returns "" for a
key nothing writes — no error, no warning, no log. That is how the agentic
path's crisis-escalation input scored 0 on every real conversation for two days
after ff421d3 moved conversation history behind conv_hist_key(session_id) and
left one consumer reading the old "conv_history" bucket by hand.

scripts/verify-state-keys.sh is the gate; scripts/state-key-audit.py is the El
reader behind it. Two checks:

  DEAD-READ    a state_get whose key resolves to something no state_set in the
               tree produces.
  HAND-ROLLED  a literal that belongs to a namespace a helper owns, accessed
               without the helper. This is #129's actual shape, and DEAD-READ
               alone does NOT catch it: the dead handle_chat() still writes
               "conv_history" through conv_hist_key(""). Stating that plainly
               because a gate that only appears to work is worse than none.

WHY IT DOES NOT CRY WOLF. Keys are usually computed, so a literal-matching
script would flood and be switched off in a day. The resolver handles
concatenation (matched on the static prefix), helper functions (resolved to
their possible returns, with guard conditions folded so conv_hist_key("") does
not falsely claim to produce the session_hist_ namespace), keys built into a
local, and keys arriving as a parameter (resolved through the call sites).
278 of 278 sites on this tree resolve: UNRESOLVED 0, FINDINGS 0. Unresolvable
keys would be listed and would NOT fail the build.

TWO-LEG PROOF, one variable — agentic_safety_screen's single line:
  pre-fix  scripts/verify-state-keys.sh --root <scratch>
           chat.el:2536 state_get("conv_history")
             conv_hist_key() owns this key namespace (EXACT 'conv_history')
           FAIL: 1 state-key finding(s)                        exit 1
  as-is    scripts/verify-state-keys.sh
           FINDINGS (0) ... PASS                               exit 0

INDEPENDENT CONFIRMATION: run read-only against
origin/feat/soul-openai-tools-v2, which carries the same defect on its own, the
gate reported chat.el:2937 — the exact line 43d0449's message had named by
hand, with no prior knowledge. Against origin/fix/129-on-openai-tools: PASS.

PRODUCER-MOVED CONTROLS: renaming the sole writer of an EXACT key (soul_model)
orphans 3 readers across 3 files; renaming the sole writer of a PREFIX
namespace (agent_workspace_root_*) orphans 3 readers, including when the
producer moves to a NARROWER namespace — a case an earlier, more permissive
prefix rule let through. That rule is now directional, with the reason written
next to it.

FOUND ON ITS FIRST RUN, unprompted: soul.el's state_set("soul_identity", ...)
was deleted 2026-05-13 in b163fa6 (a commit about awareness/ISE writes) and five
readers in chat.el were left behind — build_system_prompt, the vision handler,
the agentic system prompt and two council handlers have prefixed "" for ~3
months. studio.el:57 emits "principal":"" and never had a producer. Both are
recorded in state-key-baseline.txt with dates and causes so the gate can be
turned on today; they are DEBT, not false positives, and every run prints them.

Baseline signatures carry no line number (an unrelated edit must not un-mute an
accepted finding) but do carry a count, so a GROWTH in a baselined finding still
fails the build.

Engine behaviour unchanged: this commit adds scripts only, no .el is touched.
CI is deliberately NOT wired here — .gitea/workflows/ci.yaml has changes in
flight from someone else, and turning the gate on would immediately red
feat/soul-openai-tools-v2 (correctly). That flip should be deliberate.

Rung reached: RUNS — the gate executes (0.15s), discriminates on four
independent test pairs, and its verdicts are quoted above. Not wired to CI, and
no engine binary was built from this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

Rebased onto main (post-f1f52bc) as rebase/gate-state-key-reads-onto-main. All gates pass: committed input matches sources, builds (920,808 bytes), interface superset with nothing removed.

The gate runs, and it is already earning its place. Its first output on rebased main:

BASELINED (6) — pre-existing debt accepted. NOT clean; fix these.
  chat.el   DEAD-READ exact:soul_identity   (lines 737, 1745, 2620, 3425, 3480)
  studio.el DEAD-READ exact:soul_principal  (line 57)

FINDINGS (0)
PASS: every resolvable state_get key has a producer.

Five sites in chat.el read soul_identity and nothing produces it — they get an empty string. That is #137 measured rather than argued, and it is the identity-empty-on-the-chat-path defect still live today.

It passes only because those six are baselined as accepted debt. Worth deciding whether that baseline should shrink rather than persist — a gate whose baseline never moves is a record of debt, not a lever against it.

Rung: E2E-VERIFIED in an isolated lab (gate executed, output above). Not deployed.

Rebased onto `main` (post-`f1f52bc`) as `rebase/gate-state-key-reads-onto-main`. All gates pass: committed input matches sources, builds (920,808 bytes), interface superset with nothing removed. **The gate runs, and it is already earning its place.** Its first output on rebased main: ``` BASELINED (6) — pre-existing debt accepted. NOT clean; fix these. chat.el DEAD-READ exact:soul_identity (lines 737, 1745, 2620, 3425, 3480) studio.el DEAD-READ exact:soul_principal (line 57) FINDINGS (0) PASS: every resolvable state_get key has a producer. ``` Five sites in `chat.el` read `soul_identity` and **nothing produces it** — they get an empty string. That is #137 measured rather than argued, and it is the identity-empty-on-the-chat-path defect still live today. It passes only because those six are baselined as accepted debt. Worth deciding whether that baseline should shrink rather than persist — a gate whose baseline never moves is a record of debt, not a lever against it. Rung: E2E-VERIFIED in an isolated lab (gate executed, output above). Not deployed.
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/gate-state-key-reads:feat/gate-state-key-reads
git checkout feat/gate-state-key-reads
Sign in to join this conversation.