Skip to content

#53: per-person memory — durable per-person record layer#86

Merged
BrettKinny merged 7 commits into
mainfrom
feat/53-per-person-memory
May 22, 2026
Merged

#53: per-person memory — durable per-person record layer#86
BrettKinny merged 7 commits into
mainfrom
feat/53-per-person-memory

Conversation

@BrettKinny
Copy link
Copy Markdown
Owner

#53 — per-person memory

A durable, queryable per-person memory layer for Dotty — facts about named
household members (preferences, relationships, recent context), distinct
from the static hand-authored household registry.

What it does

  • Write — the model attributes a fact to a named person
    (remember_person tool on the pi-runtime path; /api/voice/remember_person
    on the bridge path).
  • Read — facts are surfaced back as a [Person memory] prompt block
    (bridge path, injected by _voice_preparer) or via the recall_person
    tool (pi-runtime path — that runtime exposes no prompt-injection seam, so
    read is a tool).
  • Kid-safety gate — a fact about a child / unidentified / unclassifiable
    person is routed to a person_pending:<id> review queue that the read
    paths never touch. Only an affirmed adult (registry age >= 18, or an
    adult relation) auto-commits. The gate decision is single-source in
    Python (bridge _person_memory_needs_review; dotty-behaviour
    GET /api/voice/person_review_status) — never duplicated in TypeScript.
  • Review UI — a new /ui/memory dashboard card lists the pending queue
    with Approve / Redact, and stored records with Redact.

Storage is the existing FTS5 brain.db, namespace-partitioned
(person:<id> / person_pending:<id>) — no schema change.

Commits

  • 1e4d01c bridge.py — [Person memory] read injection + /api/voice/remember_person + gate
  • cfc2b8c dotty-pi-ext — recall_person read tool + fetchPersonMemories + oracle test
  • db284b3 dotty-behaviour classifier endpoint + dotty-pi-ext remember_person write tool
  • 1eeea0f bridge /ui/memory dashboard review surface

Tests

New: 8 classifier cases (dotty-behaviour test_routes_voice.py), a
recall_person oracle equivalence test, remember_person unit tests.
Verified locally with py_compile / node --check / Jinja tag balance;
the full pytest and node test runs need the deploy hosts (fastapi,
pytest, better-sqlite3 not installed in the dev env).

Not done — leave #53 open until

Refs #53.

🤖 Generated with Claude Code

BrettKinny and others added 4 commits May 22, 2026 19:04
Add a durable per-person memory layer keyed on the household registry's
person_id, stored as `memories` rows in a `person:<id>` namespace of the
existing brain.db (FTS-free direct fetch — no schema change).

Read path: _voice_memory_person_fetch_blocking + _build_person_memory_block
render a [Person memory] block, injected by _voice_preparer after
[Speaking with] and before [Current perception] on both /api/message and
/api/message/stream. Fetched off-loop via asyncio.to_thread.

Write path: POST /api/voice/remember_person with a conservative kid-safety
gate — facts auto-commit to readable person:<id> memory only when the
speaker is affirmatively an adult in the registry; known minors, unknown
people, and unclassifiable entries route to a person_pending:<id> review
queue that the read path never reads.

The write trigger (a tier1_slim tool calling the endpoint) and the
dotty-pi-ext parallel land in follow-ups. Refs #53.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pi runtime exposes no turn-prep / system-prompt injection seam (the
extension only registers tools and listens to agent_end), so per-person
memory is surfaced on the pi-runtime path as a tool — recall_person —
rather than the [Person memory] prompt-block injection bridge.py uses.

- brain_db.ts: fetchPersonMemories() — namespace-scoped SELECT against
  person:<id>, mirror of bridge.py:_voice_memory_person_fetch_blocking.
  Reads only the approved namespace; person_pending:<id> is never read.
- tools/recall_person.ts: recall_person(name) tool — facts pipe-joined,
  matching the memory_lookup result shape.
- tests/recall_person{_oracle.py,.test.ts}: equivalence test — seeded
  person:<id> rows, TS SELECT vs the bridge SELECT asserted byte-equal,
  plus ordering / namespace-isolation / case-insensitivity cases.

The remember_person write tool lands next. Refs #53.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sifier

The pi-runtime write path for declared per-person facts. The kid-safety
gate decision needs the household registry, so it stays single-source in
Python rather than being duplicated in TypeScript:

- dotty-behaviour: GET /api/voice/person_review_status — classifies a
  person_id against the household registry (adult -> auto-commit; minor,
  unknown, or unclassifiable -> review). Ports bridge.py's gate logic.
- dotty-pi-ext: remember_person tool — asks the classifier, then writes
  the fact direct to brain.db under person:<id> or person_pending:<id>
  via storeMemory (consistent with the `remember` tool). The HTTP client
  fails safe: an unreachable classifier routes to review.

Tests: 8 classifier cases in dotty-behaviour (adult/minor by age &
relation, unknown, sparse, none-household, endpoint); namespace-router +
edge cases in dotty-pi-ext.

Refs #53.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The human end of the kid-safety review queue, completing the #53
implementation.

- bridge.py: _voice_memory_person_records_blocking (list approved +
  pending rows), _voice_memory_approve_blocking (person_pending:<id> ->
  person:<id>), _voice_memory_delete_blocking (redact). Exposed to the
  dashboard via configure().
- dashboard.py: GET /ui/memory (rows grouped by person, the pending
  review queue surfaced first), POST /ui/actions/memory/{approve,redact}.
- templates: memory_list.html (card body — pending + stored sections,
  per-row Approve/Redact), memory_result.html. New card in dashboard.html
  between the perception card and the activity feed.

Refs #53.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 22, 2026 09:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements #53 by adding a durable per-person memory layer (stored in brain.db under person:<id> / person_pending:<id>), including write/read paths, a kid-safety review gate, and a bridge dashboard UI for approving/redacting pending items.

Changes:

  • Add per-person memory read/write tooling on the pi-extension side (remember_person, recall_person) and direct DB access helpers.
  • Add a single-source kid-safety “needs review” classifier endpoint in dotty-behaviour.
  • Add bridge prompt injection of [Person memory] plus a new /ui/memory dashboard card for review/management.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
dotty-pi-ext/tests/remember_person.test.ts Adds unit tests for remember_person pure logic edge cases.
dotty-pi-ext/tests/recall_person.test.ts Adds equivalence + formatter tests for TS person recall vs bridge SELECT oracle.
dotty-pi-ext/tests/recall_person_oracle.py Implements a Python oracle mirroring bridge per-person SELECT for test equivalence.
dotty-pi-ext/src/tools/remember_person.ts Adds the pi tool for per-person memory writes with review gating.
dotty-pi-ext/src/tools/recall_person.ts Adds the pi tool for per-person memory reads (approved namespace only).
dotty-pi-ext/src/lib/dotty_behaviour.ts Adds client helper for the /api/voice/person_review_status classifier.
dotty-pi-ext/src/lib/brain_db.ts Adds fetchPersonMemories + constants/types for per-person DB reads.
dotty-pi-ext/src/index.ts Registers the new per-person tools with the pi extension runtime.
dotty-pi-ext/package.json Wires new recall/remember_person tests into the test script.
dotty-behaviour/tests/test_routes_voice.py Adds unit tests for the kid-safety classifier logic and endpoint.
dotty-behaviour/routes/voice.py Adds /api/voice/person_review_status endpoint and classifier implementation.
bridge/templates/memory_result.html Adds HTMX action result partial for approve/redact actions.
bridge/templates/memory_list.html Adds the per-person memory dashboard list UI (pending + stored).
bridge/templates/dashboard.html Adds a new dashboard card that polls /ui/memory.
bridge/dashboard.py Adds /ui/memory partial + approve/redact action routes and wiring hooks.
bridge.py Adds per-person memory fetch/injection into prompts, write endpoint, and UI backing operations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +53 to +80
export async function runRememberPerson(
name: string,
fact: string,
opts: RememberPersonOptions = {},
): Promise<string> {
const n = (name ?? "").trim();
if (!n) return "(no person specified)";
const trimmedFact = (fact ?? "").trim();
if (!trimmedFact) return "(empty fact)";
// Codepoint-aware truncation — matches Python str[:N] (see remember.ts).
const cp = Array.from(trimmedFact);
const capped =
cp.length > FACT_MAX_CHARS
? cp.slice(0, FACT_MAX_CHARS).join("")
: trimmedFact;

const needsReview = await fetchPersonReviewStatus(n, {
baseUrl: opts.baseUrl,
timeoutMs: opts.timeoutMs,
});
const ok = storeMemory({
content: capped,
category: "core",
namespace: personNamespace(n, needsReview),
importance: 0.7,
sessionId: opts.sessionId ?? null,
dbPath: opts.dbPath,
});
Comment on lines +55 to +65
export function runRecallPerson(name: string, dbPath?: string): string {
const n = (name ?? "").trim();
if (!n) return "(no person specified)";
// The household-registry id is the person's name lowercased — that is
// the `person:<id>` namespace convention bridge.py writes against.
// fetchPersonMemories applies the lowercasing.
const rows = fetchPersonMemories(n, {
limit: PERSON_MEMORY_MAX_FACTS,
dbPath,
});
return formatPersonRecall(n, rows);
Comment on lines +109 to +124
@router.get("/api/voice/person_review_status")
async def voice_person_review_status(
person_id: str,
household=Depends(get_household),
) -> dict:
"""Kid-safety classifier for dotty-pi-ext's remember_person tool.

Returns whether a declared fact about `person_id` must be routed to
the `person_pending:<id>` review queue (minor / unknown /
unclassifiable) rather than committed straight to readable
`person:<id>` memory."""
pid = (person_id or "").strip().lower()
return {
"person_id": pid,
"needs_review": person_needs_review(household, pid),
}
Comment on lines +130 to +150
def test_person_review_status_endpoint() -> None:
with TestClient(app) as client:
client.app.state.household = _FakeHousehold({
"dad": Person(id="dad", display_name="Dad", relation="parent"),
"kiddo": Person(id="kiddo", display_name="Kiddo", age=6),
})
r = client.get(
"/api/voice/person_review_status", params={"person_id": "Dad"}
)
assert r.status_code == 200
assert r.json() == {"person_id": "dad", "needs_review": False}

r2 = client.get(
"/api/voice/person_review_status", params={"person_id": "kiddo"}
)
assert r2.json() == {"person_id": "kiddo", "needs_review": True}

r3 = client.get(
"/api/voice/person_review_status", params={"person_id": "stranger"}
)
assert r3.json() == {"person_id": "stranger", "needs_review": True}
BrettKinny and others added 3 commits May 22, 2026 20:38
The bridge-side per-person write path landed unreachable: nothing in
the voice stack called POST /api/voice/remember_person. Only the
pi-runtime path (dotty-pi-ext) had a fully wired remember_person write.

Wire it into the tier1slim path:

- tier1_slim.py — add remember_person to TOOLS (name + fact) so the 4B
  can emit the call; description steers it away from general facts
  (those still use the [REMEMBER: ...] marker). Silent filler.
- bridge.py — add the _voice_tool_remember_person escalate handler and
  register it in _VOICE_TOOLS, so POST /api/voice/escalate dispatches
  it. Extract the gate+store core into a shared _person_memory_store()
  so the escalate handler and the /api/voice/remember_person endpoint
  apply one identical kid-safety gate decision.
- The handler returns the same confirmation strings as the pi-runtime
  remember_person tool, so the spoken reply is consistent across both
  voice runtimes.

The #53 kid-safety gate is unchanged — a tier1slim remember_person call
for a minor / unknown person still routes to the person_pending: queue.

Refs #53.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mory

# Conflicts:
#	bridge/dashboard.py
#	bridge/templates/dashboard.html
Review follow-up. The per-person-memory kid-safety gate (`_ADULT_RELATIONS`
+ the needs-review function) is implemented twice — bridge.py and
dotty-behaviour/routes/voice.py — because the two run as separate Docker
images on separate hosts and cannot share an import.

Rather than a network round-trip on the voice write path (a new failure
mode) or a shared module that neither deployment tree includes, mark the
dotty-behaviour copy CANONICAL and the bridge.py copy a TRANSITIONAL
MIRROR. The bridge.py copy is retired entirely when the #36 rehoming
removes bridge.py + bridge/*. Turns a silent-divergence trap into a
documented one with a clear source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@BrettKinny BrettKinny merged commit 539003d into main May 22, 2026
9 checks passed
@BrettKinny BrettKinny deleted the feat/53-per-person-memory branch May 22, 2026 10:47
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.

2 participants