Skip to content

feat(memory): person-memory writer — deterministic, evidence-minimal, firewalled - #297

Merged
adrianwedd merged 1 commit into
masterfrom
feat/person-memory-writer
Aug 25, 2026
Merged

feat(memory): person-memory writer — deterministic, evidence-minimal, firewalled#297
adrianwedd merged 1 commit into
masterfrom
feat/person-memory-writer

Conversation

@adrianwedd

Copy link
Copy Markdown
Owner

Stage 1 of the minimal person-memory plan: land the writer only. No retrieval, no seeding yet — nothing reads state/people-*.jsonl in this PR, and a structural test enforces that until the retrieval stage deliberately relaxes it.

What this adds

src/pxh/people.py — a deterministic, regex-only writer that records three narrow fact kinds (preference, relationship, commitment) from what Obi literally says, into state/people-{persona}.jsonl. No LLM anywhere in the write path; provenance kind is the hardcoded literal report (ceiling 0.9). Design doc: docs/specs/2026-08-25-person-memory-minimal-design.md.

Two call sites, both after the source message is durably stored:

  • api._append_obi_chat_api (role obi only) — threads the real obi_chat event id
  • voice_loop.record_conversation_turn (user text, SPARK persona only) — id-less channel, so a turn:sha1 content hash is the honest fallback

Privacy shape (the point of the review)

  • Firewall by filesystem: person facts live in a file mind.py never opens, so they structurally cannot reach reflection → thoughts-spark.jsonl/api/v1/public/thoughts → feed/blog/Bluesky. Pinned by source scans in tests/test_people_invariants.py, same pattern as test_policy_invariants.py.
  • Evidence minimisation: evidence is the exact matched clause + source id, never the whole utterance. The pinned case: "I'm sad about school today, but I really like dinosaurs." → stores likes dinosaurs with evidence "I really like dinosaurs"; "sad"/"school" appear nowhere in the store (test_evidence_is_the_matched_clause_never_the_whole_utterance).
  • Persona gate in the writer: GREMLIN/VIXEN are refused inside record_person_facts, not by filename.
  • Rejection-biased matcher: questions, hedges, conditionals, reported speech, hyperbole, deictic objects, negated intents all refused — a 30-case false-positive corpus in tests/test_people.py is the authority, and patterns may only be widened against it.
  • Expiry + supersession: commitments TTL 3d default / 10d cap (Hobart days), filtered at read time; corrections supersede, never delete.
  • Evolution-proof: people.py and test_people_invariants.py added to claude_session.BLACKLIST_FILES (mind.py/voice_loop.py are evolvable, so the module deciding whether a bridge exists must not be).
  • Test isolation: autouse conftest fixture redirects the store to tmp — same fix(tests): in-process tests write to production logs and bypass the supervisor guard — falsified brain-daemon forensics #221 hazard class as the health/mailbox fixtures.

Testing

84 tests in test_people.py + test_people_invariants.py, plus test_conversation_buffer.py (14), test_api.py -k obi (6), test_resident_only_invariant.py + test_policy_invariants.py (102) — all green locally; CI is the gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ThqC6Gq4mZyXWnnvGC2a57

… firewalled

Adds src/pxh/people.py: a regex-only writer that extracts three narrow fact
kinds (preference, relationship, commitment) from what Obi literally says,
into state/people-{persona}.jsonl. No LLM anywhere in the write path;
provenance kind is the hardcoded literal "report".

Privacy shape:
- Separate file mind.py never opens — person facts structurally cannot
  reach reflection, /api/v1/public/thoughts, the feed, blog or Bluesky.
  Pinned by source-scan tests in tests/test_people_invariants.py.
- Evidence is the exact matched clause plus the source message id, never
  the whole utterance: "I'm sad about school today, but I really like
  dinosaurs." stores only the dinosaur clause. Pinned by test.
- Real event-id threading on obi-chat (obi_chat:<id>); content-hash
  fallback only on the id-less voice buffer.
- Persona gate inside record_person_facts: GREMLIN/VIXEN refused at the
  writer, not by filename.
- Rejection-biased matcher: questions, hedges, conditionals, reported
  speech, hyperbole, deictic objects, negated intents all refused; 30-case
  false-positive corpus in tests/test_people.py is the authority.
- Commitments expire (3d default / 10d cap, Hobart days), filtered at read
  time; corrections supersede, never delete.

Call sites: voice_loop.record_conversation_turn (user text, spark persona)
and api._append_obi_chat_api (obi role, after durable store). people.py and
its invariant tests are blacklisted from px-evolve; conftest gains an
autouse fixture isolating the store from the live robot.

Design: docs/specs/2026-08-25-person-memory-minimal-design.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThqC6Gq4mZyXWnnvGC2a57
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying spark with  Cloudflare Pages  Cloudflare Pages

Latest commit: f6a1ff9
Status: ✅  Deploy successful!
Preview URL: https://d8a1055c.spark-e11.pages.dev
Branch Preview URL: https://feat-person-memory-writer.spark-e11.pages.dev

View logs

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