Files
esh-pfi-infrastructure/persistent-memory.d/2026-09-15-silent-wrong-answer-pattern.md
T

250 lines
16 KiB
Markdown

# ⭐⭐ The fleet's characteristic failure: a confident answer from a broken instrument
Named by svos-dev 2026-09-15 after three instances turned up between two agents in one
night. Collecting them here because the *class* is more useful than any instance, and
because every one of them **passed a check**.
## The shape
> **A check that reads the INPUT to a transformation, reported as if it read the OUTPUT.**
>
> Or, more generally: the instrument answers instead of the system, and its answer is
> shaped exactly like a real one — no error, no timeout, usually exit 0.
What makes this class expensive is not that things break. It is that **the broken state
is indistinguishable from a legitimate one**, so it survives review, passes CI, and is
found later by accident.
## The instances, 2026-09-15 alone
| # | instrument said | reality | why it passed |
|---|---|---|---|
| 1 | `provider=cuda` in the log | ORT had silently fallen back to **CPU** | the line echoes the *configured* env var, never the running EP |
| 2 | `node --check` green, `import app` green | the served page's **entire inline script was dead** | a JS `'didn\'t'` inside a Python string arrives as `'didn't'`; the FILE still holds the backslash |
| 3 | `secret get` → `""`, **exit 0** | a failed vault read | callers read an empty *optional* secret as "not configured" |
| 4 | `find()` → **"not found: <name>"** | a failed listing (`json.loads(stdout or "[]")`) | an empty stdout became a confident, authoritative negative |
| 5 | `/v1/toolsets` → **0 toolsets** | my credential lookup returned empty → 401 | an auth failure renders identically to an empty roster |
| 6 | `hermes plugins compat <typo'd path>` → **✓ exit 0** | nothing was scanned | "no hits" and "no files" are the same result |
| 7 | `hermes plugins doctor` → **exit 0** | it had printed `ERROR` | needs `--ci` to exit non-zero |
| 8 | `ss -ltnp \| grep python` → nothing | the listener was there, named **`hermes`** | the filter narrowed the window without announcing it |
| 9 | SIGTERM → **port free** | process alive another **35 s** | a script waiting on the port starts a second copy |
Prior art already in memory, same class: `pct snapshot` exiting 0 while refusing;
"an unreachable post office is an OUTAGE, never an empty inbox"; `docker logs --since`
returning 0 for a line that exists.
## The tell
⚠ **Whenever "broken" and "legitimately empty / absent / off" produce the same output,
you have one of these** — and the cheap check will not tell them apart, by construction.
## What actually works
1. **Measure the OUTPUT, not the input.** Not `provider=cuda` in a log — a process
holding memory on the pinned card. Not `node --check` on the file — parse the page
**as served**.
2. **Positive control, every time.** Run something the method *must* detect. #6 was
caught by scanning a plugin with a known-deprecated import; the clean result only
became meaningful once the instrument had proven it could fail.
3. **Negative control too** — ⚠ but check the negative is a *true* negative. Two
"failures" in the secrets-broker test were **names I had invented**; without checking,
I would have read two true negatives as a partial fix and kept digging at a bug that
was already gone.
4. **Refuse to emit the ambiguous value.** The real fix for #3 and #4 was not the lock —
it was making an empty result a loud non-zero instead of a plausible answer.
5. ⭐ **Don't declare victory on a plausible fix.** A lock is such an obvious answer to a
race that "I added a lock" reads as done. The first lock was in the wrong place and
still failed; the root cause (concurrent `bw unlock` at *session establishment*) only
surfaced because the plausible fix was tested and did not work.
## ⚠ And the instrument itself can be stale
`~/.local/bin/secret` was a **plain copy** of the repo file, in sync by luck. Every repo
edit silently left the live tool behind, so the first "fixed" test ran the OLD code.
Caught it; the next person could read stale output as proof a correct fix failed and
revert it. Now a symlink. **Check what you are running, not what you edited.**
## ⚠ The sibling failure: a claim nobody ever measured
The nine above are broken instruments. This one is *no instrument at all*, and it cost
more than any of them on 2026-09-15.
**The talk-deploy "permission problem" never existed.** tts-dev's `docs/infrastructure.md`
and a stale `persistent-memory.md` row said `/opt/docker/compose` on nh3-dev was not
project-writable. It is `root:docker 2775`, agent sessions run as `lkraven`, and
`lkraven` is in the `docker` group — a `mkdir` proves it in one second. **Nobody ran one
for nine days.** There is no `tts-dev` OS account either, so "add tts-dev to the docker
group" had no referent at all.
How it held together:
1. A **stale memory row** (`root:root`) supplied a plausible mechanism.
2. The operator's **routing instruction** ("give it to infra") was read as
*corroboration of a capability limit*. ⭐ **Those are different claims and only one
was ever stated** — a routing preference explains where work went, never whether it
could have gone elsewhere.
3. ⚠ A **contradicting `ls -la` was on screen in the same session** and was noted, then
dropped.
4. **I repeated it to the operator as fact** in a deploy report ("the durable fix is a
group rather than a relay"), which put a second agent's name behind it.
⚠⚠ **And then I did it again, one layer up.** Told to fix the harness issue, I found no
OS problem and no deny rule, inferred the **auto-mode classifier** must be refusing it
(the shape fit — I had been refused twice that night on the same box), and **committed a
`.claude/settings.json` to someone else's repo on that inference.** tts-dev's `mkdir`
then showed their session writes the path with no refusal at all. Reverted. I had spent
the night writing up this exact failure class and still built a fix for a layer nobody
had shown me failing.
⚠ The commit that carried it also **overclaimed a doc correction that never happened**:
I chained the edit and the commit in one invocation, the edit's anchor assertion failed
because the target text was already gone, and the commit ran anyway. **Never chain an
edit and its commit in one invocation** — a failed edit still produces a commit message
asserting it.
⭐ **The rule: "I can't do X" from any source — a doc, a peer, a memory row — is a
hypothesis until someone runs the command and pastes the error.** Ask for the error text
before designing around it. "There is no error text, because there was no error" is a
possible answer, and it was the right one here.
⭐ **Distinguish the layer before fixing it.** A shell `Permission denied` is a Unix
problem; a refusal naming permission rules or auto-mode is a harness one. Different
fixes, and neither applies when nothing failed.
## ⚠ CHARACTERIZED DEFECT: `/snapshot`'s handoff generator turns deferred items into orders
**n=2, same session, reproducible.** `snapshot_handoff.py` (gen-small) reliably converts
"open, operator-deferred, not blocking" into an imperative **Next steps** list, and twice
invited the next session to commit files explicitly marked as predating the session.
run 1: "Execute deferred operator tasks: AI-tab Dormant regrouping, nconnect=8,
fused MoE (park id 47)" + "Commit graphify-out/… if they are ready"
run 2: six next-steps, FIVE of them deferred/parked items presented as actions,
+ the same commit invitation
⚠ **It fails silently in the skill's blind spot.** The documented failure posture is
fail-loud-fall-back — unreachable gateway, timeout, truncation, missing section → write
nothing, exit non-zero. **A structurally valid handoff whose content inverts the
operator's intent passes every one of those checks** and exits 0.
⚠ **And this is the one artifact a fresh context inherits as instruction.** It is read
immediately after `/clear`, before any other framing, and its Next steps read as a
mandate. A wrong one here is not a bad summary; it is a fresh session going and doing
belayed work.
### Mechanism — it is `SYSTEM_PROMPT`, not the model
`snapshot_handoff.py:75-107`. Three things compose:
1. **`## Next steps` has no empty case.** `## Watch out for` gets an explicit escape
("OMIT THIS WHOLE SECTION if the input carries no gotchas"); `## Resume here` gets one
("If the input says nothing is in flight, say so plainly"). **`## Next steps` gets
neither**, while being told it is "A numbered list. Ordered, concrete". With nothing
in flight, the only action-shaped nouns left are the deferred items.
2. **The nothing-in-flight rule points straight at them** — "point at the most recent
open pointer it names" directs attention to the parked entries, which then get
promoted into Next steps.
3. **Nothing protects MODALITY.** "Invent nothing; every claim must trace to the input"
is satisfied — the items *are* in the input. Their *deferred-ness* is what got
dropped, and only identifiers are protected against restructuring.
⭐ **The general lesson: the verbatim-identifier rule shows some input attributes must
survive restructuring untouched. Modality is one of them and nobody guarded it.**
**Mitigation until fixed: read the generated handoff before accepting it**, and invert
any deferred item into an explicit *do NOT*. Both runs this session were corrected
in-session. **Reported to `galdrabok-dev` 2026-09-15** with both specimens, the mechanism
above and two proposed prompt changes (an empty-case escape for `## Next steps`; a rule
making deferred/parked/belayed items constraints rather than steps).
⚠ `galdrabok-dev` is `mode: pull` — no herald poke, so they see it on their next check.
✅ **FIXED 2026-09-15 10:24 PT — `galdrabok b0882a4`, "protect item modality in the handoff
generator".** Both proposed changes shipped near-verbatim and are live here already (my
`~/.claude/skills/snapshot` is a **symlink** into `~/development/galdrabok/skills/snapshot`,
so it needs no push). galdrabok reproduced the defect mechanically at **10/10 baseline runs,
9 of 9 deferred tokens every run, zero within-condition variance** — and their 4-variant
ablation shows **both** changes are load-bearing for *different* surfaces: the empty case
stops the promotion, the modality rule keeps the deferred items *present* as constraints
(the cheaper fix alone produced a clean handoff that had silently **dropped all four
deferred items**). ⚠ **`modality rule only` still leaked 5/5 via the commit invitation** —
a commit invitation is not a deferred *item*, so an item-modality rule never reaches it.
Their positive control (a fixture with genuinely pending work) held 5/5 real next-steps
under every variant, so the fix is not over-suppression. `Exit 0 is not acceptance` is now
permanent spec text (§4.12), not an interim note.
⭐ **My two real runs are the field corroboration, and they are why the artifact looked
clean:** `b0882a4` is stamped 10:24:35 and this repo's handoff was written 10:07:50 — 17
minutes earlier, by the UNFIXED generator, on the adversarial input (nothing in flight,
four deferred items, two do-not-commit files). It read correctly only because it was
corrected in-session, per the mitigation above. **2 of 2 real runs inverted.** The next
`/snapshot` taken here is the first real post-fix run; report the handoff verbatim, leak
or clean — one run, a datapoint against their n=5 fixtures, not a replacement for them.
⭐⭐ **SHARPENED 2026-09-15 (`galdrabok 206f6ad`, on origin) — the two leak surfaces have
DIFFERENT trigger conditions, and the dangerous one fires on ORDINARY input.** galdrabok
re-split the ablation by surface after I pointed out that a commit invitation is not a
deferred *item*, so an item-modality rule structurally cannot reach it:
| variant / fixture | deferred-ITEM leak | commit-invitation leak |
|---|---|---|
| baseline, all-deferred | 5/5 | 5/5 |
| modality rule only, all-deferred | **1/5** | **5/5** |
| empty case only / both, all-deferred | 0/5 | 0/5 |
| baseline, **mixed** (real work present) | **0/5** | **2/5** |
**Surface 1 (deferred items) needs the adversarial all-deferred shape to fire. Surface 2
(the commit invitation) fires on ordinary input** — on the mixed fixture it is the ONLY
leak. ⚠ **It is also the one a fresh session is least likely to question: committing
pending work reads as diligence.** Shipped as a **non-removal constraint** (SKILL.md
§Generation + contract §4.12): "A generator carrying just one of the two rules leaks on
the other surface. Neither may be removed as the other's duplicate" — so a future
tidy-up that reads them as one idea gets stopped.
📌 **OWED BY ME, logged on both sides:** the next `/snapshot` run in this repo is the
first real post-fix run. Report to galdrabok **verbatim**, no in-session correction —
and they want the **`## Watch out for` section quoted in full**, not just a leak/clean
verdict: whether the four real deferred items arrive *do-not-phrased* is a **soft failure
nothing checks**, held 3/5 (all-deferred) and 5/5 (mixed) on fixtures, and real prose
around each item is where they expect the phrasing to degrade first.
⚠ **Do NOT run `/snapshot` to satisfy this** — it is operator-invoked by standing rule;
the datapoint arrives when he next calls it, not on a peer's schedule.
⚠⚠ **RE-SCOPED 2026-09-15 (`galdrabok 1273a49`) — the owed run is a TRIPWIRE, not a
validation, because this repo is now the MIXED shape and mixed has almost no confirming
power.** Once BabyYarros became live in-flight work here, my next snapshot stopped being
their `all-deferred` fixture. Against their baseline table that costs the datapoint most
of its value, and they said so rather than waiting for the artifact:
- **Surface 1 (deferred items) cannot discriminate on mixed input at all** — the UNFIXED
generator already scored 0/5 there. A clean `## Next steps` is exactly what broken
produces on this shape. Reading it as evidence would be reading noise.
- **Surface 2 (commit invitation) can only falsify** — baseline mixed leak is 2/5, a 40%
event rate, so **one clean run is ~60% likely even if the fix did nothing**. One leaked
run refutes the shipped 0/5 outright.
⛔ **If it comes back clean that is NOT validation, and it must not be written down as
one.** It is a tripwire that did not trip. This sentence exists because it is precisely
the one a later session quietly upgrades into "confirmed in the field".
📌 **What still carries information: the verbatim `## Watch out for`.** Mixed is the
*better* fixture for it (do-not phrasing held 5/5 there vs 3/5 on all-deferred), and it is
the failure **nothing validates** — a leak gets caught by the step-7 read, but a deferred
item arriving as a flat description instead of a do-not passes every check and merely
reads as less binding. **Their predictions, on record for predict-then-check:** `## Next
steps` clean of all six identifiers; all four items present under `## Watch out for`; both
dirty files present and do-not-phrased. ⭐ **Least confident: `nconnect=8` — "declined in
scope" is a modality their rule does not enumerate** (it lists deferred / parked / belayed
/ blocked / deliberately-not-done). If one item comes through flat, that is the predicted
one, and it would mean the rule matches VOCABULARY rather than the concept — a fixable
miss. Thread closed from their side; no reply owed until the artifact lands.
⭐ Same family as everything above — the instrument produced a plausible artifact and
the plausibility is exactly what makes it dangerous.
## Related
`2026-09-15-talk-v10-deploy.md` (#2, and the gate built for it),
`2026-09-15-parakeet-stt-fv-ml1.md` (#1),
`2026-09-15-svos-miranda-plugin-validation.md` (#6, #7, #8),
`2026-09-15-irv-ml1-address-sweep-done.md` (the ana-docker/litellm neighbour trap).