A pre_llm_call veto kept the typed text as the user row's content and put
the placeholder only in api_content, which only the main-loop wire replay
reads. Compaction (summary request and handoff), session_search, external
memory providers and the routed background-review digest all read content,
so the vetoed prompt reached a model at the next compaction or search.
- The placeholder is now the row's content and the persist override; the
typed text lives only in display_metadata.withheld_prompt, which the CLI
resume recap, TUI/Desktop session history and the dashboard render.
- A row the TUI/Desktop wrote at submit time is rewritten in place
(set_user_message_content gains an optional display_metadata, also on the
managed-worker store op).
- The hook now runs before turn-start compaction and a blocked turn skips
it: that summary request quotes recent user turns, this one included.
- Memory-provider transcript hand-offs drop the display-only key.
The turn prologue (agent/turn_context.py) updates a user row another writer
already materialized (in-place preflight compaction, an early/close flush, a
submit-time row) by its _row_id:
_stamp_api_content_sidecar -> set_message_api_content(sid, row_id, ...)
_append_multimodal_context -> set_user_message_content(sid, row_id, ...)
A managed worker runs the same prologue with RuntimeSessionStore as
_session_db, which had neither method. Both calls raised AttributeError,
were swallowed by the prologue's warning-only except, and the durable row
kept the pre-context bytes: a resumed session replays a view the model never
saw (prompt-cache divergence for the sidecar; the multimodal context part
missing from the stored row).
Two closed, fenced receipt ops mirror the SessionDB guards exactly:
session.message_api_content (row_id + content match, active user row of THIS
session) and session.user_content (row_id, active user row of THIS session).
Neither accepts arbitrary columns; a foreign or inactive row id updates 0 rows.
Probe (real SessionDB + worker receipt txn + real prologue functions,
r6-base): DIRECT sidecar ('question', 'question\n\nPLUGIN CONTEXT'),
multimodal 'look | [screenshot] | PLUGIN CONTEXT'; WORKER ('question', None)
and the raw parts JSON, with AttributeError logged for both. After: equal.
Test test_turn_prologue_row_backfills_land_through_the_worker_like_direct:
red on r6-base `('question', None) != ('question', 'question\n\nPLUGIN
CONTEXT')`, green after (also pins the foreign-row fence).
Review: https://github.com/NousResearch/hermes-agent/pull/106742#issuecomment-6067585232