Skip to content

Answer history questions from the records, and ask one question while the family is still there - #4

Draft
Laolex wants to merge 4 commits into
fix/week1-debtfrom
feat/history-agent
Draft

Answer history questions from the records, and ask one question while the family is still there#4
Laolex wants to merge 4 commits into
fix/week1-debtfrom
feat/history-agent

Conversation

@Laolex

@Laolex Laolex commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2 (fix/week1-debt) so the diff shows only the new work. Base moves to main once #2 merges.

Not for merge or deploy yet — judging is live. Production still runs docs/backups-erasure-and-hostnames and was not touched by any of this.

What this adds

An agent that answers from the records. agent.js runs a bounded tool-use loop (4 steps, 6 rows, 25s) over four read-only tools — search_sources, get_topic_thread, list_commitments, get_handover. Every query is parameterised and scoped by a circle_id the server supplies; the model never chooses which circle it reads. Voice notes and inbound email are untrusted text, so the isolation lives in SQL, not in prompt wording.

Three gates sit between the model and the family:

  • an answer produced without calling a single tool is discarded
  • an answer matching the advice pattern is discarded
  • an answer containing content words absent from the retrieved records is discarded

A discarded answer falls back to the deterministic path, and the mode is recorded (agentic / deterministic / deterministic-after-rejection) so a silently-degrading model is visible rather than invisible. Tool calls are persisted to history_tool_calls.

One grounding rule, shared. grounding.js extracts the check the eval already used, so the agent is held to the same rule the published numbers were measured against. extraAllowed lets the agent widen its vocabulary without moving the eval's baseline.

Guided recording. The app can now ask one short question while the family is still standing there, via the Realtime API. Ephemeral client secrets — the browser never sees the server key. The recorder starts first and never waits on the network, so if the realtime session fails to open, capture is unaffected and the endpoint returns fallback: 'record'. Sessions are capped (REALTIME_MAX_SECONDS, default 180) because a realtime session bills for as long as it stays open; the cap closes the listener, never the recording. The question reaches the handover as context, explicitly marked as not something the family said.

Three fail-loudly guards. The eval used to report 100.0% on an empty database — the exact confident-wrong number it exists to catch, and it caught us this week when .env pointed at a database the deployment does not use. It now exits 2 and names the path it read. spokenQuestion moved into capture-events.js, DOM-free and directly tested — including that a user-role transcript returns '', so the family's own speech can never be mistaken for the question the app asked.

Verification

37/37 passing. Each new guard was mutation-tested: disabling the advice gate, the grounding gate, the circle_id predicate, and the key redaction each fails a specific test, and every restore was verified byte-identical.

Known gaps

  • The browser half — getUserMedia, RTCPeerConnection, the SDP exchange — has never run against the live Realtime API. Not coverable without a real browser.
  • The eval corpus is 8 model-drafted cases, mostly demo recordings made by the author.
  • This branch is 6 ahead of main, and the deployed branch has 3 commits main does not. A three-way reconcile is needed before any deploy.

Laolex added 4 commits July 29, 2026 00:15
Lifts the grounding vocabulary and its checks out of the eval script into a
module both callers share, so the rule that scores a saved handover is the
same code that will gate an answer. ungroundedWords takes an extra allowlist
so a caller can widen its own vocabulary without moving what anyone else
measures: the eval passes none, and its published numbers are unchanged.
Replaces the canned keyword sentence with a bounded tool-use loop over four
read-only tools scoped to one circle. The model never sees the database and
never names a circle, so an instruction injected into a family email can
change what it says but not which family it can read.

An answer is served only if it cited a record, reads as no medical advice,
and every content word traces back to what the tools returned. Otherwise the
keyword answer stands: it is computed first every time, so a slow, down, or
wrong provider still leaves the family an answer with evidence attached.

Tool calls are persisted so any answer can be audited afterwards.
Capture opens a spoken realtime session alongside the recorder, so a missing
concrete detail - who agreed to do a thing, and when - can be asked for once,
aloud, while it is still remembered. The reply lands in the same recording.

The session deliberately does not produce the transcript. A transcript from
the browser cannot be checked against the audio, and the eval, the history
agent, and the promise that a handover holds only what the family said all
rest on the transcript being faithful to the recording. So the audio is still
recorded locally and still transcribed server side, and the question is
passed to the drafting model as context, marked as something the app said.

The browser never holds our API key: it is given a short-lived client secret,
restricted to circle members and rate limited, with the model and the safety
instructions pinned at mint time. The recorder starts first and never waits on
the network, so a refused mint, a browser without WebRTC, or a provider
outage costs the family nothing but the question.
Three small guards, one theme.

The eval reported 100.0% on a database with no handovers in it. That is the
confident wrong number the harness exists to catch, and it caught us: the
repo .env points DATABASE_PATH at a file the deployment does not use. It now
exits 2 and names the path it read.

A realtime session bills for as long as it stays open, and a carer may leave
a recording running. The listening window is capped server side, default
three minutes, and the recording is untouched by it.

Reading the spoken question out of a realtime event is the code most exposed
to the provider renaming a field. It moves to capture-events.js, apart from
app.js and free of any DOM, and is now tested against shapes it should read
and shapes it should ignore - including the family own speech, which must
never be mistaken for the question.
@Laolex
Laolex marked this pull request as draft July 29, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant