handle_chat_agentic now reads body image + image_media_type and, when present, sends the current
user turn as an Anthropic content-block array [{text},{image}] instead of a plain string — so the
model sees raw pixels alongside memory, history, and tools (parity with the CLI). Additive: no image
=> output byte-identical to before. elc-clean. Pairs with neuron-ui fix/chat-vision-attachments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All 8 session_preload node accesses (3 profile, 2 work, 2 project, 1
summary) now check id_in_seen(node_id, seen_ids) before including
content. seen_ids is populated by engram_compile via state and covers
all nodes already in the activation+search context block. Prevents
high-salience nodes from appearing twice in the system prompt.
Issue 1 (CRITICAL): Restore parse_float_x100 for correct single-decimal
float handling. "0.9" now correctly yields 90, not 9. Also restores
engram_numeric_valid guard that validates inputs before str_to_int.
Issue 2 (CRITICAL): Fix handle_chat_agentic safety screen history key
regression. state_get("conversation_history") -> state_get("conv_history")
so the safety screen receives actual history instead of always "".
Issue 3 (REAL BUG): Replace _sel_N JSON sentinel injection in
engram_compile_ranked with |N| index string tracking. Sentinels were
leaking into node JSON delivered to the LLM and cleanup only covered
indices 0-14, leaving indices 15+ uncleaned.
Issue 4 (REGRESSION): Restore rendered conversation history formatting.
Conversation history is now rendered as "User: .../Assistant: ..." with
400-char truncation per turn, not raw JSON array injection.
Issue 5 (SCOPE/SAFETY): Restore removed defensive code: engram_numeric_valid
and parse_float_x100 guards; conv_history_load label-based fetch + partial-
write guard + load-failure state flag; conv_history_persist partial-write
guard + failure logging; hist_warning in response envelope.
Issue 6 (UNDOCUMENTED): Restore bell event cutoff from 259200s (3 days)
back to 1209600s (14 days). Also restore PositiveEvent affective context
search that was removed alongside the cutoff change.
Issue 7 (LOGIC REGRESSION): Fix affective_prefix to run every turn
(not just hist_len == 0). The care/joy directives must persist throughout
the session, not vanish after turn 1.
Issue 8 (MINOR): session_summary_write_dated now uses el_from_float(0.85)
for salience and importance (two-decimal) to avoid any ambiguity in float
parsing, and the function is re-added with the session-end hook.
Two design bugs in the state_set placement caused the dedup seen-ID set
to be incomplete even with callsites wired up:
1. state_set("engram_compile_seen_ids") was called immediately after
merging the main node pools, before scan_part (persona fallback) and
affective_part (bell node) were computed. Nodes appearing only in
those segments were never added to the seen set.
2. affective_part is a bare JSON object (bn0 from json_array_get), not
a JSON array. Passing it to engram_extract_ids would have gotten
json_array_len == 0 and silently skipped the affective node's ID.
Fix: move state_set to after ctx is assembled from all three segments.
Extract ids_from_merged and ids_from_scan via engram_extract_ids (both
are JSON arrays), and extract ids_from_affective via json_get(affective_part, "id")
directly since it is a bare object. Merge all three via add_to_seen
before publishing to state.
Thread a seen-node-ID exclusion set from engram_compile() through to
session_preload in handle_chat, preventing the same high-salience nodes
(identity, recent memories) from appearing 2-3x in the system prompt.
Changes:
- Add id_in_seen(), add_to_seen(), engram_extract_ids() helpers that
maintain a comma-delimited seen-ID accumulator (EL has no Set type)
- In engram_compile(): after merging all topic/entity/recall pools, extract
node IDs from merged_nodes and publish via state_set(engram_compile_seen_ids)
- In handle_chat(): read seen_ids from state after engram_compile() returns,
then check id_in_seen() before emitting each session_preload bullet
(profile x3, work x2, project x2, summary x1 — all 8 candidate nodes guarded)
Nodes already present in the compiled engram context are skipped in preload,
eliminating 3000-3500 token repetition on first-message turns.
engram_compile() already published seen node IDs to state via engram_compile_seen_ids
but handle_chat never read or applied them. Wire up the consumption side:
- Read engram_compile_seen_ids from state after engram_compile() returns
- Check each session_preload candidate node (profile x3, work x2, project x2,
summary x3) against id_in_seen() before emitting its content bullet
- Nodes already present in the compiled engram context are skipped entirely,
preventing the same high-salience identity/memory nodes from appearing 2-3x
in the system prompt and burning 3000-3500 tokens on repetition
- Add engram_render_node/render_nodes/dedup_nodes helpers for human-readable
prose bullet output instead of raw JSON node objects reaching the LLM
- Fix engram_compile_ranked to use |N| index sentinel instead of _sel_N JSON
mutation which leaked sentinel fields into LLM-visible node data (Issue #11)
- Update build_system_prompt with chat_mode param; no_tools_rule only included
for chat path, not agentic paths (Issue #9)
- Move engram block to end of system prompt for strongest LLM attention (Issue #8)
- Label sections: STABLE IDENTITY vs RETRIEVED MEMORY (Issue #10)
- Render conversation history as User:/Assistant: dialogue instead of raw JSON
- Add RETRIEVED MEMORY labels to agentic and dharma room system prompt assembly
- Cache bell node in engram_compile state (engram_compile_bell_node)
so handle_chat reads cached value instead of duplicate bell query (Issue 2)
- Cache activation result (engram_compile_activation_json) for strengthen_chat_nodes
reuse — eliminates third activation query per turn (Issue 7)
- Fix context cap to truncate at clean JSON object boundary (Issue 6)
- Cache bell node result in engram_compile state (engram_compile_bell_node)
so handle_chat affective_prefix reads the cached value instead of firing
a duplicate engram query for distress signals (Issue 2)
- Cache primary activation result in engram_compile state
(engram_compile_activation_json) using nodes0 from engram_compile_multi
- Replace redundant engram_activate_json(message, 2) in strengthen_chat_nodes
with state_get(engram_compile_activation_json) - eliminates a third
activation query per turn (Issue 7)
- engram_compile already has object-boundary truncation and cross-set
dedup via engram_nodes_merge/engram_dedup_nodes (Issues 1, 6, 9)
- Issue 2: replace raw 50-char threshold with is_genuine_continuation() that
checks for explicit follow-up phrases and mid-sentence capitalization (proper
nouns signal a new topic, not a continuation)
- Issue 3/8: build_activation_seed() scans back to find the prior USER turn as
the topic anchor instead of using the last assistant reply (hist_len-1)
- Issue 4: engram_compile_multi() fans out across three seeds — enriched primary,
raw message (entity queries), and emotion query — merging non-redundant results
- Issue 5: agent workspace_root appended to ag_seed so agentic activation is
workspace-aware; previously ignored despite being available in state
- Issue 6: distill_transcript() extracts salient tail+question content from full
transcripts before passing to engram_compile in dharma room handlers
- Issue 7: dist/soul-with-nlg.el handle_chat and handle_chat_agentic now load
history and use build_activation_seed() — the raw message path is eliminated
- Issue 9: topic_snip_from_entry() takes the TAIL 200 chars of a long reply and
finds the last sentence boundary — captures end-of-reply named concepts
- Issue 10: multi_turn_topic() pulls up to 3 prior user turns into the non-
continuation seed so earlier thread context re-activates high-salience nodes
Fix critical float parsing bug: %g serializes 0.70 as '0.7', naive str_replace
dot-strip gives str_to_int('07')=7 not 70. New parse_salience_100() uses
str_index_of to detect single-decimal strings and multiplies by 10 to correct.
Affects conv nodes (0.6/0.7), default memories (0.5/0.5), utterance nodes (0.6)
— the majority of the graph was scoring near zero and filtered by threshold=25.
Fix recency to use max(created_at, updated_at, last_activated) so nodes
strengthened by engram_strengthen() after chat turns score as fresh, not by
original write time. A node referenced yesterday but created 25 days ago
was borderline-filtered; now correctly scores fresh.
Compress recency dynamic range from 10x (10-100) to 1.54x (65-100) via
formula (50 + recency/2). Old formula: sal*imp*recency/10000 let recency
dominate — a canonical high-importance node at 30 days scored identical to
a fresh noise node. New: high-importance nodes remain competitive when old.
Add tier-aware decay with softer floor (30 not 10): Canonical nodes decay
over 365 days, Episodic over 90 days, working/untiered over 35 days. Long-
term identity and persona nodes are no longer permanently filtered.
Lower threshold from 25 to 15 to admit moderately-relevant older nodes that
pass scoring with the corrected formula. Backfills recall coverage lost when
single-decimal nodes were being silently discarded.
Apply scoring to activation nodes: engram_compile_ranked(activate_json, 5)
replaces unconditional pass-through. Threshold 5 preserves recall while
excluding genuinely zero-quality stale nodes.
Extend sentinel cleanup in engram_compile_ranked from _sel_0-9 to _sel_0-19
so max_nodes can safely be increased past 10 without JSON corruption.
Fix critical float parsing bug in engram_score_node: str_replace('.','')
then str_to_int silently miscored single-decimal salience strings (0.9->9,
0.7->7, 1.0->1). Introduce parse_salience_100() which detects decimal
position and scales correctly (no decimal: *100; one decimal: *10;
two decimals: as-is).
Replace flat 30-day linear decay with tier-aware decay curves: Canonical
nodes use a 365-day window (foundational identity resists aging), Episodic
nodes use 90 days, Working/untiered keep the existing 30-day slope. Floor
stays at 10 for all tiers.
Use max(created_at, updated_at) as the recency reference so revised nodes
are not penalised for their original creation date.
Extend affective context windows from 72h/7d to 14 days across all three
paths (engram_compile, handle_chat, soul.el load_identity_context) so a
Friday crisis carries into Monday sessions and all paths present consistent
context. The 72h/7d split caused conflicting affective context between
soul.el (which loaded a 5-day-old crisis node) and chat.el (which excluded
it on subsequent turns).
Add salience evolution to mem_consolidate: strengthen top working-memory
nodes (recently recalled across sessions) and Canonical-tier nodes
(foundational identity must not decay to the floor). Previously consolidate
returned structural counts only with no salience changes.
Expand conversation window from 20 to 40 turns in both handle_chat and the
agentic history trim. Long technical sessions were losing early problem
framing at 10 user + 10 assistant pairs.
- Q1: engram_numeric_valid() guard against non-numeric timestamps in bell scoring
- Q2: soul-agnostic cold-start fallback in engram_compile (drops genesis-specific hardcoded node IDs)
- Q3: partial-write guard and failure logging in conv_history_persist/load
- Q4: document circuit-breaker limitation requiring C runtime support
- Q5: println warnings on empty activation/search paths
- Q6: load_identity_context warns when all identity fetches return empty
- Q7: recall_status state tracking (ok/empty/unavailable) surfaced to LLM via MEMORY STATUS block
- Q8: document shared-state race conditions in engram_recall_status and safety_system_addendum
- CRITICAL BUG: conv_node_id empty check moved outside is_bell block so silent Conversation node loss is always logged
Merge propose/agent-workspace-root-read (Tim's PR #28):
- path_within_root now appends '/' to root before prefix check (closes proj_evil bypass)
- edit_file in dispatch_tool now checks agent_workspace_root() and resolves path
- handle_chat_agentic reads agent_workspace_root from request body, only persists if non-empty
- Safety screen preserved after workspace root read (conflict resolved)
Merge improve/safety-crisis-detection (PR #31): reads layered_cycle_safety_system_addendum
from state and appends to system prompt on each turn (cleared after use to prevent bleed).
Safety ts extraction falls back to updated_at. Affective prefix now wires into system build.
Conflict with PR #33 resolved: capability_rules and session_preload both preserved.
- engram_compile: BellEvent nodes do not carry created_at in the engram
node JSON; extract the unix timestamp from the embedded ' | ts:NNNNN'
pattern in the content string instead. Fall back to created_at/updated_at
if the marker is absent. Guard str_to_int against empty string so the 72h
recency check never silently treats every node as epoch-0 stale.
- auto_persist: append the current unix timestamp to the BellEvent label
('bell:soft:1749876543') to make it unique per turn. The previous label
('bell:soft') was the same for every soft bell, causing engram to treat
all subsequent writes as updates to the same node.
- Remove dead soft_bell block in layered_cycle that wrote soul_safety_system_augment
to state but was never read; safety augmentation now goes through the correct
layered_cycle_safety_system_addendum state key read by build_system_prompt
- build_system_prompt now reads layered_cycle_safety_system_addendum and appends
it to the system prompt, clearing the key after consumption
- Timestamp extraction for distress nodes falls back to updated_at when created_at
is empty, preventing the 72h recency check from always treating nodes as stale
- Issue #3: err_404/err_405 now emit HTTP 404/405 via __status__ envelope instead of HTTP 200
- Issue #4: add auth_check() function to handle_request; enforces NEURON_TOKEN on all routes except /health and /lineage
- Issue #5: missing required params now return HTTP 400 (__status__ envelope) in /api/chat (GET+POST), /imprint/contextual, /imprint/user, and handle_chat
- Issue #6: LLM unavailable in handle_chat now returns HTTP 503 instead of HTTP 200
- Issue #7: add 32 KB message size guard on POST /api/chat before engram_compile and LLM
- Issue #8: add TODO comment to route_health documenting the live-engram-query problem and the /health/deep split plan
- Issue #9: add comment to hist_trim documenting fragile str_index_of parser and silent data corruption risk
- Issue #10: add TODO comment in handle_request documenting missing per-IP rate limiting
- Issue #11: fix connectd_post temp file collision — add monotonic sequence counter so concurrent requests get unique paths
- Issue #12: fix call_mcp_bridge fixed temp file race — add monotonic sequence counter for unique paths under concurrent load
- Issues #1/#2: add TODO comment in handle_request documenting EL no-exception limitation and SIGSEGV handler gap
Issue #5: detect empty string from llm_extract_text() as an error in handle_chat,
handle_chat_as_soul, and handle_dharma_room_turn. The C runtime silently returns ""
when the LLM response content array is missing or all blocks fail to parse; without
this guard the empty string passes through to callers as a silent empty reply.
Issue #9: make agentic_loop max_tokens configurable via NEURON_LLM_MAX_TOKENS env
var (default 4096). The hardcoded value is marginal for long tool chains (8 iterations
x 4096 tokens); operators can now set 8192+ for complex multi-step tasks without
rebuilding. Non-agentic path (llm_call_system) still uses the C runtime hardcode —
that fix lives in el_runtime.c (see TODO block added in this commit).
Issue #10: increase connector_tools_json and tool_auto_approved curl --max-time from
2s to 5s to reduce false-empty tool lists when neuron-connectd is under transient
load. Graceful degradation to [] on bridge down is unchanged.
Issues #1/#2/#3/#4/#6/#8: documented as TODO comments in chat.el. These require
targeted C runtime changes in el_runtime.c (llm_provider_request retry loop,
EL_LLM_TIMEOUT_MS separation, HTTP 429 backoff, 5xx retry, EL_HTTP_MAX_RESPONSE_BYTES
cap). Architectural decisions recorded so they are traceable to root causes.