ff421d39f6
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>
87 lines
5.3 KiB
Plaintext
87 lines
5.3 KiB
Plaintext
// auto-generated by elc --emit-header - do not edit
|
|
extern fn chat_default_model() -> String
|
|
extern fn engram_numeric_valid(s: String) -> Bool
|
|
extern fn parse_float_x100(s: String) -> Int
|
|
extern fn engram_score_node(node_json: String) -> Int
|
|
extern fn engram_render_node(node_json: String) -> String
|
|
extern fn engram_render_nodes(nodes_json: String) -> String
|
|
extern fn engram_dedup_nodes(nodes_json: String) -> String
|
|
extern fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String
|
|
extern fn engram_split_topics(message: String) -> String
|
|
extern fn engram_extract_entities(message: String) -> String
|
|
extern fn engram_detect_recall_intent(message: String) -> Bool
|
|
extern fn engram_is_continuation(message: String, hist_len: Int) -> Bool
|
|
extern fn engram_compile_multi(topic: String) -> String
|
|
extern fn engram_nodes_merge(a: String, b: String) -> String
|
|
extern fn id_in_seen(node_id: String, seen: String) -> Bool
|
|
extern fn add_to_seen(seen: String, node_id: String) -> String
|
|
extern fn engram_extract_ids(nodes_json: String) -> String
|
|
extern fn affective_node_ts(node_json: String) -> Int
|
|
extern fn engram_compile(intent: String) -> String
|
|
extern fn distill_transcript(transcript: String) -> String
|
|
extern fn json_safe(s: String) -> String
|
|
extern fn current_engine_note(model: String) -> String
|
|
extern fn bounded_persona_floor() -> String
|
|
extern fn operator_identity_block() -> String
|
|
extern fn build_system_prompt(ctx: String, chat_mode: Bool) -> String
|
|
extern fn hist_append(hist: String, role: String, content: String) -> String
|
|
extern fn conv_hist_key(session_id: String) -> String
|
|
extern fn conv_hist_label(session_id: String) -> String
|
|
extern fn is_utility_request(body: String, session_id: String) -> Bool
|
|
extern fn provenance_scan_urls(arr: String, acc: String) -> String
|
|
extern fn provenance_add_sources(block: String, btype: String, has_cit: Bool, cit_raw: String, acc: String) -> String
|
|
extern fn provenance_names(tools_used: String) -> String
|
|
extern fn text_join_sep(accumulated: String, incoming: String, after_interruption: Bool) -> String
|
|
extern fn tool_receipt(tools_used: String, sources: String) -> String
|
|
extern fn hist_trim(hist: String) -> String
|
|
extern fn hist_trim_with_bell_guard(hist: String) -> String
|
|
extern fn clean_llm_response(s: String) -> String
|
|
extern fn conv_history_persist(session_id: String, hist: String) -> Void
|
|
extern fn conv_history_load(session_id: String) -> String
|
|
extern fn conv_history_record(session_id: String, user_msg: String, assistant_msg: String, receipt: String) -> Void
|
|
extern fn conv_history_block(session_id: String) -> String
|
|
extern fn layered_generate(prompt: String, imprint_id: String, session_id: String) -> String
|
|
extern fn session_preload_bullets(nodes: String, max_bullets: Int, snip_len: Int) -> String
|
|
extern fn affective_context_prefix() -> String
|
|
extern fn handle_chat(body: String) -> String
|
|
extern fn handle_see(body: String) -> String
|
|
extern fn studio_tools_json() -> String
|
|
extern fn agentic_api_key() -> String
|
|
extern fn llm_base_url() -> String
|
|
extern fn llm_wire_format() -> String
|
|
extern fn json_escape(s: String) -> String
|
|
extern fn openai_chat_complete(model: String, base_url: String, api_key: String, safe_sys: String, messages_json: String) -> String
|
|
extern fn agentic_tools_literal() -> String
|
|
extern fn web_search_tool_json() -> String
|
|
extern fn strip_client_web_search(tools_inner: String) -> String
|
|
extern fn agentic_tools_with_web() -> String
|
|
extern fn connector_tools_json() -> String
|
|
extern fn agentic_tools_all() -> String
|
|
extern fn call_mcp_bridge(tool_name: String, tool_input: String) -> String
|
|
extern fn tool_auto_approved(tool_name: String) -> Bool
|
|
extern fn call_neuron_mcp(tool_name: String, args: String) -> String
|
|
extern fn agent_workspace_root() -> String
|
|
extern fn path_within_root(path: String, root: String) -> Bool
|
|
extern fn resolve_in_root(path: String, root: String) -> String
|
|
extern fn run_command_is_readonly(cmd: String) -> Bool
|
|
extern fn cmd_abs_escape_at(cmd: String, root: String, needle: String) -> Bool
|
|
extern fn run_command_guard(cmd: String, root: String) -> String
|
|
extern fn classify_tool_risk(tool_name: String, tool_input: String) -> String
|
|
extern fn dispatch_tool(tool_name: String, tool_input: String) -> String
|
|
extern fn is_builtin_tool(tool_name: String) -> Bool
|
|
extern fn next_bridge_id() -> String
|
|
extern fn handle_chat_plan(body: String) -> String
|
|
extern fn handle_chat_agentic(body: String) -> String
|
|
extern fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: String, messages_in: String, h: Map, tools_log_in: String) -> String
|
|
extern fn bridge_save(session_id: String, model: String, safe_sys: String, tools_json: String, messages: String, tools_log: String, tool_use_id: String) -> Bool
|
|
extern fn agentic_resume(session_id: String, tool_use_id: String, content: String) -> String
|
|
extern fn handle_tool_result(session_id: String, body: String) -> String
|
|
extern fn handle_chat_as_soul(body: String) -> String
|
|
extern fn handle_dharma_room_turn(body: String) -> String
|
|
extern fn handle_dharma_room_turn_agentic(body: String) -> String
|
|
extern fn session_summary_write(summary_text: String) -> String
|
|
extern fn session_summary_write_dated(summary_text: String, label: String) -> String
|
|
extern fn session_summary_autogenerate(hist: String) -> String
|
|
extern fn auto_persist(req: String, resp: String) -> Void
|
|
extern fn strengthen_chat_nodes(activation_nodes: String) -> Void
|