Document: cr-reply-source-legibility-v0_1
Version: v0.1
Date: 2026-06-28
Type: Phase change request — engine + Operator Layer (loomworks-engine + loomworks). Small, additive: one new response field + its population + its render.
Consumer: Claude Code (executor).
Branch base: engine main @ 5ccc3d7; OL feat-companion-surface @ 9667d60.
Origin: Decided this session — the source model, legibility-first. The Companion answers from several bodies (engagement Memory, personal Memory, the model's own knowledge) but does not tell the Operator where an answer came from — and when it does ("you mentioned it's sunny, I don't have live data"), that is the LLM choosing to narrate in prose, free to omit. This makes the source structural.
Grounding: loomworks-seed-model-grounding-inspection-v0_1's sibling source-model strand + the CR-drafting grounding pass (the recall reply path, ConverseResponse, the general_conversation dispatch branch, the persona prose, CompanionBubble — engine @ 5ccc3d7, OL @ 9667d60).
When the Companion answers you, it draws from a few different places — your engagement's record, your personal memory, or the model's own general knowledge — and blends them into one reply. Right now you can't tell which place an answer came from. Sometimes the Companion says so in words ("you mentioned it's sunny"), but that's the model choosing to mention it — it's free to leave it out, and sometimes does. This change makes the source a real, visible part of every reply: a small marker that says from your record, or from your personal memory, or the model's own knowledge — shown by the surface, not left to the model's prose. It starts with the two clearest cases (recall from your notes, and the model answering from itself) and lays the foundation for marking web answers later.
Three gaps make up the full source model: (1) structural source-marking on replies (this CR), (2) a content-origin provenance type (lets a non-Memory answer become a remembered fact — later), (3) web egress (a new capability — last, built onto this foundation). This CR is gap 1 — the cheapest and highest-value, because the source data already exists and is thrown away, and because it relieves the real pain (the Operator couldn't tell a recalled note from a live-data claim) without building any new capability. It is the same instinct as the add-side truthfulness fix: don't trust the LLM's prose for something that should be a guarantee — structure it in code.
The source data exists, structured, and is discarded before the surface:
_route_ask_about_past_input (router.py:642-676) packs operation_data["assertions"] = [{"display_number", "content"}]; _route_recall_personal (:679-729) packs operation_data["personal_memories"] = [{"content"}]. Both return RouteResult(operation_data=…). Then format_operation_result (prompt.py:630-677) flattens them to a prose list into the system prompt, and translate_converse_response (translators.py:288-295) builds the response only from companion_message — operation_data is never passed through. The structured source is retrieved and dead-ends in the prompt string.general_conversation (router.py:2707-2708) returns None — no RouteResult, no engine call; the responder answers from the system prompt + history alone. This branch is exactly "the model answers from its own knowledge" — the attach point for the LLM-knowledge marker.ConverseResponse has no source field. (schemas.py:677-812) — companion_message, side_effects, suggested_actions, project_id, draft_specification, readiness, classified_intent, token_usage, request_action, setting_changes, completeness_flag. No source/grounded_in/citations. Constructed in ~11 sites; translate_converse_response is the main recall/converse path.recall_personal.md: "do not narrate that you are 'checking personal memory'." companion_persona.md:6-7: "your primary source is what {operator} has told you … your secondary source is your general domain knowledge." So source is prose-only today AND the model is told not to narrate the lookup — which is exactly why a structural marker (out-of-band, not in the reply text) is the right complement, not a contradiction: the marker makes source legible without the model having to say it.The surface has a place for it but no field:
ConverseResponse (types.ts:142-160) mirrors the engine — no source field.ConversationTurn (types.ts:70-88) carries actor_kind/actor_display_name/contributor_designation/is_domain_expert (CR-2026-094, "honest source labeling") — but that labels who authored an inbound turn (Operator/Contributor/Domain Expert), explicitly null on Companion turns. This CR's marker is a different thing: the Companion reply's answer-source (where the Companion's answer came from). Same render slot, distinct concept — the CR reuses the surface treatment, not the field. Do not conflate them.CompanionBubble (ConversationTranscript.tsx:60-112 — the shared component both desktop ConversationPane and mobile MobileSurface use, from the mobile-Companion work) has the label line at :73-75. A source marker renders here, so desktop and mobile get it together for free.Directly on the seed's spine. The seed's trust axis is tie-to-source — a contribution's trust is its tie to where it came from, marked non-suppressibly (AI-origin in particular). Today the Companion's answer-source is suppressible (the model can omit it). Making it structural is the seed's own principle applied to the reply stream: source is shown, not narrated-if-the-model-feels-like-it. It also extends the add-side truthfulness standing note ("the Companion must not claim what the system did not do") — here, the Companion must show what it actually drew on, structurally.
ConverseResponse, populated from the dispatch outcome, mirrored to the client, rendered in CompanionBubble. Not LLM prose.ask_about_past_input) → "from your record" / "from this engagement's record."recall_personal) → "from your personal memory."general_conversation → None branch) → "the model's own knowledge."Other intents (tune_setting, create, delegation, etc.) are not answer-from-a-source in the same way — they get no source marker (null), which is correct (nothing to attribute). Confirm the null-for-non-recall default at Step 0.
actor_kind labeling. This is the reply's answer-source, not inbound authorship. New field, reuses the render slot's treatment.Per-step commits, suite green at each, halt-before-push. Engine + OL.
Confirm against engine 5ccc3d7 / OL 9667d60:
assemble_prompt → generate_response → translate_converse_response. Where is the dispatched intent (or the RouteResult/None outcome) known at the point translate_converse_response builds the response? The source value must be derivable there. (CC offered to trace this — do it now; it's the load-bearing seam.)ConverseResponse construction sites and which need the new field populated vs. which default to null (lean: only the recall + general_conversation paths set a non-null source; all others null).CompanionBubble's label line (ConversationTranscript.tsx:73-75) as the render seat and the actor_kind precedent's treatment (so the marker reads consistent with existing source labeling without reusing the field).ConverseResponse → display-turn flow: how a ConverseResponse becomes a DisplayTurn/Companion turn the bubble renders, so the new field threads to the bubble.Report findings, then build.
source field to ConverseResponse (schemas.py:677) — an optional enum/string: engagement_record / personal_memory / model_knowledge / null. (Enum lean — a closed set the surface maps to labels; confirm enum-vs-string at Step 0.)translate_converse_response (translators.py:288) from the dispatch outcome: recall-engagement intent → engagement_record; recall-personal → personal_memory; general_conversation/None → model_knowledge; everything else → null. (The threading seam from Step 0 determines how the dispatch outcome reaches the translator.)
Suite expectation: add tests — a recall reply carries source=engagement_record; a personal recall carries personal_memory; a general-conversation reply carries model_knowledge; a tune_setting/other reply carries null. Existing tests unaffected (additive optional field).
source field on the client ConverseResponse (types.ts:142) and thread it onto the DisplayTurn (or the Companion turn shape the bubble consumes).CompanionBubble (ConversationTranscript.tsx:73-75), render the source marker at/under the Companion label line — a small, muted element ("from your record" / "from your personal memory" / "the model's own knowledge"), styled consistent with the existing label treatment, shown only when source is non-null.SURFACE keys for the three labels).
Suite expectation: component test — a Companion turn with source=engagement_record renders "from your record"; model_knowledge renders "the model's own knowledge"; null renders no marker. Both desktop and mobile inherit it (shared component). Existing transcript tests unaffected.
Note: because the marker is out-of-band and additive, the existing prose behavior is unchanged — the Companion still speaks naturally; the marker is structural. The persona prose ("do not narrate checking personal memory") is now correct alongside the marker — the model doesn't narrate, the surface shows.
CompanionBubble).TagChipArea.test.tsx tsc errors confirmed at 9667d60 baseline are not this CR's.)source field is the display-side seed of that origin concept.source value (web) with a URL/fetch-time, rendered the same way. This CR's enum is designed to extend.
DUNIN7 — Done In Seven LLC — Miami, Florida
Change Request — Reply source legibility (structural source-marking) — v0.1 — 2026-06-28
Adds a structural source field to ConverseResponse (engagement_record / personal_memory / model_knowledge / null), populated server-side from the dispatch outcome (recall already holds the structured source and throws it away; LLM-knowledge is the general_conversation→None branch), rendered in the shared CompanionBubble for desktop + mobile. The surface shows the source; the model doesn't narrate it. Legibility-first — gap 1 of the source model. Engine 5ccc3d7 + OL 9667d60.