fix(engine): the agentic crisis screen reads the session's own history again
P0 SAFETY. Closes the regression we introduced inff421d3(2026-08-05).ff421d3correctly moved conversation history to a per-session key via conv_hist_key(session_id). One consumer did not move with it: the agentic path's L1 safety screen kept reading the anonymous "conv_history" bucket. The desktop app always mints a session id (DaemonClient.kt:706), so history was always written under session_hist_<id> and that read always returned "". The half of the crisis score that receives history is the escalation half — the one that exists for distress building across several turns, where no single message trips the bell on its own. It scored 0 on every real conversation for two days. Single-message hard bell was never affected. The bitter part: the comment that line carried documented this exact bug being fixed once already, under issue #9. The fix was right then. The rename re-broke it, and the comment went on describing a repair that no longer held. A comment is not a gate. The read now goes through conv_hist_key like every other consumer, including the plain path at soul.el:398 and the thread-anchoring read thirty lines below it in this same handler. It is one line. The rest of this commit is structure so it cannot happen quietly again: - agentic_safety_screen() owns the two decisions that were inline — which window the screen sees, and the screen call. Inline safety inputs are untestable safety inputs; that is what let a rename starve this one with nothing failing and nothing logging. - the comment above the call site now states the invariant (read window == written window) instead of naming a key that can be renamed out from under it. TWO-LEG PROOF, one variable — the single line state_get("conv_history") -> state_get(conv_hist_key(session_id)): before scripts/run-el-test.sh tests/test_history_amplification.el 3. REGRESSION #129 ... FAIL got: soft_bell expected: hard_bell 8 passed, 1 failed runner exit 1 after same command, same tree, that one line changed 9 passed, 0 failed runner exit 0 Full engine rebuild from these sources is clean: gen-soul-amalgam.sh -> 1,164,103 bytes / 1226 inlined bodies (gate wants >= 1200), cc-brain.sh -> 903,096 bytes, 0 errors. agentic_safety_screen and conv_hist_key both present in the built binary (nm: T _agentic_safety_screen, T _conv_hist_key). Rung reached: BUILT + RUNS (discriminating test). NOT yet in a DMG and not yet verified in the app a human opens — those are the next two rungs and neither is claimed here. Known and NOT fixed by this commit: - feat/soul-openai-tools-v2 carries the same defect independently at chat.el:2937 and needs the same change or a merge. - the defect CLASS (a read of a state key no producer writes) is still invisible to every gate we have. Issue #129 proposes making it a build error; that is the follow-on. Closes #129 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2519,6 +2519,24 @@ fn handle_chat_plan(body: String) -> String {
|
||||
return "{\"plan\":" + plan_json + ",\"model\":\"" + json_safe(model) + "\"}"
|
||||
}
|
||||
|
||||
// ── agentic_safety_screen — the agentic path's L1 input gate ──────────────────
|
||||
//
|
||||
// Extracted 2026-08-07 (issue #129) so the agentic path's safety INPUT is
|
||||
// reachable by a test. It owns exactly two decisions: which history window the
|
||||
// screen sees, and the screen call itself.
|
||||
//
|
||||
// Why it is a function and not two inline lines: those two lines sat in the
|
||||
// middle of a 300-line handler, and a key rename (ff421d3) moved the producer
|
||||
// without moving this consumer. Nothing failed, nothing logged — the
|
||||
// history-amplification half of the crisis score simply received "" on every
|
||||
// real session for a day. Inline safety inputs are untestable safety inputs.
|
||||
// See tests/test_history_amplification.el, which fails if this window and
|
||||
// conv_history_record ever stop agreeing.
|
||||
fn agentic_safety_screen(session_id: String, message: String) -> String {
|
||||
let history: String = state_get(conv_hist_key(session_id))
|
||||
return safety_screen(message, history)
|
||||
}
|
||||
|
||||
fn handle_chat_agentic(body: String) -> String {
|
||||
let message: String = json_get(body, "message")
|
||||
if str_eq(message, "") {
|
||||
@@ -2554,10 +2572,10 @@ fn handle_chat_agentic(body: String) -> String {
|
||||
|
||||
// L1 safety screen — agentic path must pass the same gate as layered_cycle.
|
||||
// Hard bell: return the crisis response immediately, do not enter the agentic loop.
|
||||
// Fix(issue #9): "conversation_history" key was never written; history lives under "conv_history".
|
||||
// Old key caused history-amplification in safety_screen to always receive "" on agentic path.
|
||||
let history: String = state_get("conv_history")
|
||||
let screen_result: String = safety_screen(message, history)
|
||||
// The history window this screen sees is owned by agentic_safety_screen (issue #129);
|
||||
// it must be the same window conv_history_record writes, or the escalation half of the
|
||||
// crisis score is silently starved. Do not inline this read back into the handler.
|
||||
let screen_result: String = agentic_safety_screen(sess_for_root, message)
|
||||
let screen_action: String = json_get(screen_result, "action")
|
||||
if str_eq(screen_action, "hard_bell") {
|
||||
safety_log_bell("hard", json_get(screen_result, "reason"), str_slice(message, 0, 80))
|
||||
|
||||
Reference in New Issue
Block a user