Version. 0.1
Date. 2026-08-05
Author. Claude Code (execution session). Operator: Marvin Percival.
Brief. inspection-briefs/loomworks-b11-step-0-inspection-brief-v0_1.md.
Charter. standing-notes/dunin7-standing-authorization-charter-v0_1. R-5 inspection run.
Status. Read-only. No fix, no change request, no recommendation. Establishes ground for one.
/Users/dunin7/loomworks-engine at main afe85070fb4c3cbccb008f5a20805408a50c222e — confirmed unmoved, working tree clean./Users/dunin7/loomworks at main c12a1ad5b32aa3be5ba284c6ee7428092f55a34d, tag surface-typecheck-gate-v0_1 — confirmed unmoved, working tree clean.playground_dev and playground_test — not touched. No throwaway database was needed.
3.1 Entry point. POST /operator/converse → operator_converse(), loomworks-engine/src/loomworks/orchestration/routers/converse.py:617-1446. Pipeline: classify → route → respond. This is the sole Companion/chat/Q&A entry point found; there is no separate "ask"/"query" endpoint.
3.2 What is retrieved, and how. For the intent this brief's behavior is most consistent with — ask_about_past_input — routing lands in _route_ask_about_past_input(), orchestration/router.py:1529-1563, which calls list_assertions(engagement_id=..., state="committed", limit=_PAST_INPUT_LIMIT, db=db), _PAST_INPUT_LIMIT = 50 (router.py:119). list_assertions (engagement/assertions.py:807-876) is a plain SQL query on current_memory_objects, ORDER BY created_at DESC (assertions.py:820-821) — not a similarity or vector search, not a query keyed to the question's content. The router's own docstring states the design directly: "Topic-based filtering is out of Phase 42 scope (the responder summarizes all assertions and the LLM naturally focuses on what the Operator asked about)" (router.py:1536-1539). Relevance to the specific question is decided by the model reading a fixed, recency-ordered dump — not by the retrieval mechanism. A separate, always-attached "engagement context" block also rides along regardless of intent: most-recent 20 committed assertions, MAX_ASSERTIONS_IN_CONTEXT = 20 (orchestration/prompt.py:56, 309-344).
3.3 What is assembled. assemble_prompt(), orchestration/prompt.py:839-1000. Real section order: persona → PROJECT CONTEXT → personal-memory block → cross-promotion instruction → OBSERVATION instruction → per-intent INTENT template with {operation_result} filled by format_operation_result(). The ask_about_past_input template, quoted in full (orchestration/prompts/intent_instructions/ask_about_past_input.md:1-4):
> The Operator wants to know what they've told you — their past notes and contributions. You have the relevant notes below. Present them conversationally, using display numbers so the Operator can reference specific items. If they asked about a specific topic, focus on relevant notes. > > NOTES: > {operation_result}
{operation_result} is filled by _format_past_input() (prompt.py:481-491) — a numbered #N: content listing of exactly the rows list_assertions returned, unsummarized, unreworded, before the model ever sees them.
3.4 What comes back, and what happens to it. generate_response() → _call_responder_llm(), orchestration/responder.py:64-125. The call is client.messages.create(model=..., max_tokens=1024, system=system_prompt, messages=messages) (responder.py:77-82); the return value is response.content[0].text, verbatim (responder.py:94). No parsing, no schema validation, no check against the NOTES it was given. The only handling is a blanket except Exception substituting a fixed fallback string on an outright API failure (responder.py:116-125) — that branch does not fire on a normal, well-formed but untrue completion. ConverseResponse.companion_message (orchestration/schemas.py:791-795) carries the text straight through; the surface renders it as plain chat text (loomworks/src/components/chat/ChatView.tsx:312,492, loomworks/src/app/operator/engagement/[engagement_address]/useConversation.ts:386). There is no check between the model and the reader.
3.5 Where citation happens — the pivot. ConverseResponse (schemas.py:779-838 and onward) carries no citation/source field. The system attaches retrieved material as programmatic fact only for intents that bypass the LLM entirely and are server-composed: recall_personal (router.py:1609-1630, with the explicit comment "the reply is a FAITHFUL readout: it lists exactly the retrieved memories, in order, with the real count — it cannot name a memory not in the set"), show_held (router.py:897-954), find_files (router.py:957-1075), orient (converse.py:1095-1122). ask_about_past_input is not one of these — it is LLM-routed. Any citation-like content in an ask_about_past_input answer (a #N reference, "you told me earlier that…") is free text the model composed, not a value the system extracted and attached. The model was handed real retrieved data and instructed to use display numbers, so a citation it produces is usually grounded — but nothing enforces that, and the schema has no channel through which the system could attach a citation even if it wanted to. The persona prompt addresses the same distinction directly, as an instruction rather than an enforced guarantee (orchestration/prompts/companion_persona.md:13-14): "Honest about uncertainty. When you do not know, say so." and "Never invent provenance. If you say '{operator_name} mentioned earlier that...' you must be drawing on actual project content. If you cannot, do not make the claim."
Verdict on 3.5. For ask_about_past_input, a citation is a model claim, not a system fact. The system-fact case exists in this codebase (recall_personal et al.) but is a different, non-LLM code path.
4.1 Model parameters — possible cause. The classifier call is pinned temperature=0 (orchestration/classifier.py:227-258), with a comment citing a prior incident (CR-2026-138 Part B) about non-deterministic routing. Several other LLM seams in the codebase are likewise pinned to temperature=0 (engagement/manifestation_organization.py:428,436, engagement/creation_conversation_memory.py:198, orchestration/correction_check.py:151, orchestration/find_files.py:133). The responder call that generates the answer text itself — _call_responder_llm, responder.py:76-82 — sets no temperature, top_p, or seed. It runs at whatever the Anthropic SDK/API defaults to, which is non-zero and non-deterministic. Every other LLM seam in this codebase was deliberately hardened for determinism except the one that produces the text B-11 is about. Classed: possible cause.
4.2 Retrieval determinism — impossible cause under unchanged data; conditional otherwise. list_assertions's ordering (created_at DESC) and fixed limit=50 are stable SQL — the same question against an unchanged database returns identical rows in identical order every time. Not a source of variation unless the committed-assertion set changed mid-sequence between the three asks, which was not checked (unread — see below). Classed: impossible cause, conditional on unread database state.
4.3 State between runs — possible cause, most concrete finding. get_recent_turns(limit=10) (orchestration/conversation_turns.py:307-340) is read fresh on every /converse call, scoped to (person_id, engagement_id), and every companion turn is persisted (converse.py:1276-1393, step 7) before the next call. So the three "identical" asks were not independent: run 2's classifier and responder inputs include run 1's question and run 1's answer (up to the last 10 turns), and run 3 includes both. This is a direct, code-confirmed mechanism connecting the sequence causally — the middle answer's own false confession, if it happened on run 2, was in the model's view for run 3. Classed: possible cause.
4.4 Time-dependence — not found, impossible cause for this path. The only timestamp-shaped prompt content is the personal-memory relative-time formatter (prompt.py:1016-1049, e.g. "noted 2 hours ago"), unrelated to ask_about_past_input's NOTES block, which carries no timestamps — only #N: content. No recency filter or moving window exists in this intent's path. Classed: impossible cause.
5.1 Is there a self-check? Not found. No code path calls the model a second time to verify, score, or audit its own ask_about_past_input answer. generate_response is one completion call with one return; there is no confidence field, no verifier pass. A misfiring self-check cannot be the cause, because no self-check exists — this rules out the class of explanation the brief flagged as most diagnostic.
5.2 Is there an instruction that invites it? Yes, confirmed and quoted above (companion_persona.md:13-14) — the persona instructs the model to flag uncertainty and to refuse invented provenance. This is a real, standing instruction always present in the system prompt (persona is assemble_prompt's first block, prompt.py:895) capable of producing an uncertainty-flag or a fabrication-disclaimer on a turn where the model's own (non-deterministic, §4.1) judgment of its grounding wavered, independent of whether the NOTES block it was actually given supported the answer.
5.3 What the record shows about retrieval on the relevant turn. The retrieval query runs unconditionally before the responder is called (router.py:1545-1550), and the rows it returns are spliced verbatim into the system prompt before the model generates anything. If the model's answer was correct, the code guarantees the supporting content was present in its context at generation time for that turn. A "the system fabricated that" claim on such a turn would be contradicted by the prompt the system actually built, not corroborated by it — consistent with the brief's discipline in §2: the model's self-report is data about what it emitted, not evidence about what happened.
5.4 Is the confession ever right? Yes — nothing in generate_response/_call_responder_llm constrains the model's output to the NOTES it was handed; it is unconstrained free-text generation. The model can go beyond retrieved material on some turn, and on such a turn a confession would be true. The finding is not that self-reported uncertainty should be suppressed — it is that the current design gives the reader no way to distinguish a true confession from a false one, because §3.4/3.5 establish there is no post-generation check and no system-attached citation to compare the confession against.
Verdict: the answering path does not do real retrieval today, for the intent this behavior traces to. "Retrieval" for ask_about_past_input is: fetch up to 50 most-recent committed assertions by recency, full stop — no query term, no relevance ranking, no search index (router.py:1529-1563, assertions.py:807-876, docstring at router.py:1536-1539 naming this as a deliberate, current scope limit). This matches the brief's §6 "fixed context, capped" case, not the "search already exists" case. By the brief's own framing, B-11 and B-12 are the same underlying work, not sequential ones — there is no existing search mechanism for B-12 to replace; B-12 would build the retrieval layer that does not yet exist. A B-11 fix aimed at making citations trustworthy would either (a) be scoped to today's fixed-dump-of-50 mechanism and need rebuilding once B-12 lands, or (b) wait for B-12. Whether to merge the two build-list items is the Operator's decision per the brief's §7; this section states the retrieval-mechanism fact the decision rests on.
None. No prior finding in this brief is being corrected — this is the first Step 0 pass on B-11.
ask_about_past_input vs. recall_personal vs. general_conversation) is unread; the findings above are conditioned on ask_about_past_input, inferred as the most likely path because it is the only LLM-routed intent whose prompt hands the model numbered, citable material — recall_personal/show_held are ruled out as the source because they are server-composed and, per §3.5/5.1, structurally cannot originate a self-initiated "I fabricated that" claim. This inference is stated as inference, not established fact.resolve_responder_model (credit/routing.py:108-154) can select different models by credit tier between calls (converse.py:1151-1161) — a real, code-confirmed mechanism that could put non-identical models behind "the same question" asked three times. Unread whether it did.claude-sonnet-4-6 per AnthropicLLMClient.__init__'s default in llm_client.py:120). Token cost is small (low thousands of tokens per attempt, Sonnet-tier) but is live spend against a real API key. Not made. Flagging for authorization rather than spending, per §1.DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — B-11 Step 0 findings — v0.1 — 2026-08-05 The retrieval was real and the confession wasn't — the code that proves it is the code that never checks.