ff421d39f66501bc8c75e896c534fd0783269a01
DESIGN FIT: three of round 7's five defects share ONE root — the conversation-history
layer persists only {role, content}, discarding tool provenance, session scoping, and the
distinction between a real user turn and an internal utility call. Fixes A and B RESTORE
Will's design rather than extend it: his agentic path already scopes history per session,
the plain path never got it, and his own source carries the TODO admitting the resulting
race (chat.el, handle_chat: "process-global key; concurrent /api/chat requests without
session_id race on this read-append-write"). Fix C repairs one join Will wrote that was
correct for a year and one we added last week. E1/E2 are ours.
FIX A — tool provenance in history (kills the FALSE CONFESSION)
Root cause, EXECUTED-verified: handle_chat_agentic recorded turns via hist_append, which
emits {"role","content"} only. server_tool_use blocks, web_search_tool_result blocks and
every citation were discarded, then replayed as text. On the next turn the model saw a
data-rich answer with zero evidence a search had happened, and its own permanent rule
("never describe a search you did not perform") left one conclusion available: that it
had fabricated the data. It apologised for a search it HAD run — four independent lines
of evidence confirm the search was real. The defect is not the model's honesty. It is
that we deleted the evidence and then asked it to account for itself.
Change: agentic_loop accumulates the source URLs it already walks past (citations and
web_search_tool_result content) and returns them as "sources"; handle_chat_agentic folds
tools_used + sources into a receipt line stored WITH the assistant turn. Receipts are
unconditional — a negative receipt ("no tools ran") is the other half of the guarantee,
because "no evidence of a tool" and "evidence of no tool" were previously identical in
the transcript. conv_history_block splits the receipt off before snipping so a long
answer cannot truncate away the evidence. The user never sees it: it is appended to the
history copy, not the reply.
FIX B — one history key for both paths (kills the BLANK STARE)
Root cause, EXECUTED-verified: the agentic path keyed history on session_hist_<id>; the
plain path was hard-wired to the process-global conv_history and never read session_id.
One conversation, two buckets. Proven in the guest engram: the scoped node held exactly
two turns starting at "Try again" while the earlier exchanges sat unscoped.
Change: conv_hist_key/conv_hist_label are now the single definition, used by BOTH paths;
session_id is threaded route -> layered_cycle -> layered_generate / conv_history_record.
The 2-line fallback (plain path reads the agentic key) was REJECTED: it keeps the
process-global bucket as a live write target, which is the bleed the TODO describes.
Also found and closed while threading: layered_cycle read session_id from the state key
"current_session_id", which is read here and WRITTEN NOWHERE in the entire source. It
was unconditionally "", so TODO(reliability #4) — per-session steward continuity — was
dead code that could never fire. It fires now.
LAZY SESSION, decided explicitly: we create the session EAGERLY at the door (app half,
ui#223) rather than migrating orphaned turns. Migration would copy the CONTENTS of a
process-global bucket, possibly another conversation's, into a named session — the bleed,
performed deliberately. Eager creation makes the situation impossible instead. Migration
is deliberately not implemented and must not be added without solving provenance first.
FIX C — the two text-join seams ("to.Good", byte-verified 0x77 0x2e 0x47)
Two bare `+` joins, written a year apart, had drifted into two answers to one question:
within-response block joins (Will's, 2026-05-03, latent until server-side web_search
began interleaving non-text blocks) and across-round joins (ours, 62af564).
Change: one named rule, text_join_sep, at both sites. NOT a blanket separator — a cited
answer splits MID-SENTENCE ("The current temperature is " + "86°F" + ", with "), so a
blanket separator shatters every sourced sentence. The rule takes the one bit that
distinguishes the cases: whether a NON-TEXT block intervened. Hoisting it also makes the
fix verifiable in the shipped binary, which an inline `+` is not.
FIX E1 — utility generations stay out of the transcript
Title generation ("Write a 3-6 word title...") and insight passes ran down the same plain
door as a real message and were recorded as if the user had typed them; the same calls are
the "model":"unknown" rows in usage.jsonl. is_utility_request reads an explicit utility
flag from the app, with the __title__/__insight__ id prefixes as a fallback for older
clients. Answered normally, never recorded.
FIX E2 — OPERATOR IDENTITY is scoped to tool-capable turns
The block (env USER/HOME, closing "This is a hard rule") was prepended to EVERY system
prompt including chat mode. On a Tools:Off turn there is no filesystem in reach, so it
governed nothing and merely supplied the loudest fact in the prompt — which is why the
model opened a fresh conversation with "You're test, on your machine at /Users/test".
Hoisted to operator_identity_block() and gated on !chat_mode. Unchanged wherever a file
or command tool can actually be reached.
ALSO: agentic_loop's per-session history persist had a second hand-rolled copy of
conv_history_persist with a different label expression, different salience scores and
different tags for the same node. Since both now derive the label from conv_hist_label and
engram_node_full upserts by label, two score policies were writing one node. Collapsed to
one writer.
BUILD NOTE: dist/elp-c-decls.h is force-included by the documented link recipe and carried
the OLD C arities, so it is updated here. This is the build-support header, NOT the stale
generated dist/soul.c — no dist/*.c was read or edited; all engine changes are .el source.
chat.elh/soul.elh are committed because a first-pass build against the old signatures FAILS
(measured); the other regenerated headers are reverted as unrelated churn.
BUILT: 887,000 bytes, sha256 d632b061ad75269d6adeb52578d030eaf49e895d91289d7f946b19c08450d728
Zero el_str_concat(<int>, str_len(...)) sites (the BUG-PLAINCHAT-1 miscompile guard).
web_search_20250305 and disable_parallel_tool_use both still present — PR #108's web search
and the ADR-0005 stopgap are intact.
Refs neuron#109 (builds on it), neuron#78 (Receipt Contract — the real fix A is a stopgap for)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(engine): history keeps its provenance and its session — the false confession and the blank stare
fix(engine): history keeps its provenance and its session — the false confession and the blank stare
fix(engine): history keeps its provenance and its session — the false confession and the blank stare
fix(engine): history keeps its provenance and its session — the false confession and the blank stare
fix(engine): history keeps its provenance and its session — the false confession and the blank stare
fix(engine): history keeps its provenance and its session — the false confession and the blank stare
Description
Neuron - the canonical CGI substrate. Real soul.el lives here.
34 MiB
Languages
Emacs Lisp
80.2%
Shell
13.4%
Python
4.2%
HTML
1%
Go Template
0.7%
Other
0.5%