CR identifier. CR-2026-057
Version. 0.1
Date. 2026-05-06
Status. Approved for execution.
Author. Claude.ai (CR drafting layer). Operator: Marvin Percival.
Executing agent. Claude Code (CC) on DUNIN7-M4.
Depends on. Phase 42 complete (tag phase-42-intent-classification-and-reactive-companion at b8a27c0). Phase 41 complete (personal engagement, companion naming, ActorRef(kind="companion")).
Informed by. Phase 43 scoping note v0.1 (eight decisions settled). Personal memory investigation v0.1 (accepted — §§2–5 personal memory, §5 privacy, §8 open questions). Companion as agent investigation v0.1 (accepted — §7 capability tiers). Companion expertise note v0.1 (four founding principles). Phase 42 implementation notes §4 (personal engagement / get_current_seed / ADMIN finding).
Phase 43 teaches the Companion to remember personal things. Phase 41 created the container (personal engagement, auto-created per person, Memory-only). Phase 42 gave the Companion a brain and voice but no awareness of personal memory. Phase 43 adds three capabilities:
Additionally, the Companion can cross-promote — during project conversation, it notices a fact is personal and offers to remember it across all projects. On confirmation, the fact enters personal memory with provenance linking to the source engagement.
Substrate-only phase. No frontend changes. No new migration.
All eight decisions from the scoping note v0.1 are adopted without modification.
| Decision | Summary |
|----------|---------|
| S1 | Personal memory loading: all committed assertions, most recent first, truncated to ~500 token budget. No relevance filtering. Loads only during engagement-scoped conversation (project_id provided). |
| S2 | New intent: remember_about_me. Person-scoped — creates assertion in personal engagement. Classifier distinguishes from add_knowledge (project-scoped) by content: personal facts vs. domain facts. |
| S3 | New intent: forget_about_me. Keyword/content matching against personal assertions. Retraction (reversible), not permanent deletion. Companion mentions permanent erasure option in settings. |
| S4 | Cross-promotion: response-time detection. Responder system prompt includes standing instruction to notice personal facts during project conversation and offer to remember them across all projects. Confirmation on the next turn routes via classifier as remember_about_me. |
| S5 | Conversational continuations: LLM-resolved via conversation history. No new continuation intents. Classifier prompt gains continuation guidance for "yes" / "no" / refinements after personal memory offers, cross-promotion offers, and retraction confirmations. |
| S6 | Held with conversational commit. Personal memory assertions created in held state. Companion offers interpretation → Operator confirms → commit. Refinement ("no, just shrimp") → amend. Rejection → retract the held assertion. |
| S7 | Personal engagement seed reconciliation. Fix get_current_seed / ADMIN mismatch. [CC determines: approach — short-circuit for personal engagements, fallback lookup, or known seed text return.] |
| S8 | Substrate-only phase. No frontend changes. |
phase-42-intent-classification-and-reactive-companion at b8a27c0.Step 0 confirms:
[CC verifies: exact file path, current intent taxonomy (seven live + four stubs), parameter extraction format.]src/loomworks/orchestration/classifier.py. [CC verifies: function signature, return type (ClassifiedIntent), how conversation history is passed, how extracted_parameters are structured.]src/loomworks/orchestration/router.py. [CC verifies: function signature (route_intent), RouteResult schema fields including operation_data, how add_knowledge handler creates a held assertion.]src/loomworks/orchestration/prompt.py. [CC verifies: function signature (assemble_prompt), component structure (persona + engagement context + intent instruction), how the system prompt string is built.]src/loomworks/orchestration/responder.py. [CC verifies: function signature, how system prompt is passed, conversation history formatting.][CC verifies: storage location, how templates are loaded, how {operation_result} is interpolated.]person.personal_engagement_id FK exists (Phase 41). [CC verifies: column type, FK target, that it reliably returns the personal engagement UUID.][CC verifies: visibility='personal', that the personal engagement exists for test persons.][CC verifies: the function used by Phase 42's add_knowledge handler — file, signature, required arguments for creating a held assertion with ActorRef(kind="companion").][CC verifies: the function for retracting an assertion with rationale — file, signature.][CC verifies: the function Phase 42's ask_about_past_input handler uses — file, signature, return type (display number, content, created_at).]conversation_turns table exists (Phase 42). [CC verifies: that companion turns carry classified_intent, that recent turns are retrievable per person+engagement.]get_current_seed or equivalent seed-loading function. [CC verifies: file, function name, how it reads the seed, whether personal engagement seeds are loadable through it. Confirm the ADMIN mismatch from Phase 42 implementation notes §4.][CC verifies: function signature, tiered loading, how it resolves context for an engagement.][CC verifies: that the pipeline in the converse endpoint passes through all three stages and that adding components to prompt assembly is a single insertion point.]Pre-flight divergence stops execution at Step 0 and drives a CR amendment.
Archive this CR to docs/phase-crs/phase-43-cr-personal-memory-contribution-v0_1.md at Step 0 before Step 1 begins.
A function that loads the personal engagement's committed assertions and formats them for prompt inclusion.
Location: extends src/loomworks/orchestration/prompt.py (or a submodule if CC judges the file too large).
Signature:
async def load_personal_memory(person_id: UUID, personal_engagement_id: UUID, db: Session) -> str:
"""Load committed assertions from the personal engagement, formatted for prompt inclusion.
Returns a formatted text block, ~500 tokens max, containing the person's
committed personal memory assertions, most recent first.
Returns empty string if no committed assertions exist.
"""
Loading strategy: query committed assertions on the personal engagement, ordered by created_at DESC. Format each assertion as a bullet. Truncate to ~500 token budget (approximate via character count — 1 token ≈ 4 characters, so ~2,000 characters). The truncation drops the oldest assertions first.
Format per assertion:
- {content} (noted {relative_time})
Assembled block:
PERSONAL MEMORY
Things you've told me about yourself:
- You're allergic to all shellfish (noted 3 days ago)
- Your birthday is March 15th (noted last week)
- You prefer metric units (noted 2 weeks ago)
- You're based in Miami (noted 3 weeks ago)
If no committed assertions exist, the function returns an empty string (no block inserted into the prompt).
Relative time formatting. [CC determines: the simplest approach — a utility function or inline calculation. "noted just now" / "noted 2 hours ago" / "noted yesterday" / "noted 3 days ago" / "noted last week" / "noted 2 weeks ago" / "noted last month" / "noted 3 months ago". Precision is cosmetic, not functional.]
Integration into prompt assembly. The personal memory block is inserted into the system prompt after the engagement context block and before the intent instruction block. The assemble_prompt function gains a new parameter:
def assemble_prompt(
companion_name: str,
operator_name: str,
engagement_context: EngagementContext | None,
classified_intent: ClassifiedIntent,
operation_result: RouteResult | None,
personal_memory: str = "" # NEW — from load_personal_memory
) -> str:
The personal memory block is appended to the system prompt only when non-empty.
When to load. The converse endpoint calls load_personal_memory only when project_id is provided (engagement-scoped conversation). Project-less conversation does not load personal memory. The Companion uses personal memory to enrich project conversations, not as a standalone feature.
Converse endpoint integration. The converse endpoint pipeline (Phase 42 D9) gains one new step between context loading and classification:
# After engagement context loading, before classification
personal_memory = ""
if request.project_id and person.personal_engagement_id:
personal_memory = await load_personal_memory(
person.id, person.personal_engagement_id, db
)
# ... later, in prompt assembly:
system_prompt = assemble_prompt(
...,
personal_memory=personal_memory
)
The classifier prompt (Phase 42 versioned text asset) is updated with four additions:
Addition 1 — Two new intents in the taxonomy:
remember_about_me:
personal_fact — the substantive personal fact, stripped of conversational framing.
forget_about_me:
forget_description — a description of what the Operator wants forgotten.
Addition 2 — Boundary guidance between add_knowledge and remember_about_me:
DISTINGUISHING PROJECT KNOWLEDGE FROM PERSONAL MEMORY:
- If the fact is about the project domain (soil pH, recipe ingredients, system requirements, design decisions), classify as `add_knowledge`.
- If the fact is about the Operator personally (preferences, identity, schedule, dietary restrictions, location, working style), classify as `remember_about_me`.
- "The soil pH should be 6.5" in a farming project → `add_knowledge`
- "I prefer working in the mornings" → `remember_about_me`
- "We use PostgreSQL for this project" → `add_knowledge`
- "I'm allergic to shellfish" → `remember_about_me`
Addition 3 — Continuation guidance:
CONVERSATIONAL CONTINUATIONS:
When the Operator's message is a short response (yes, no, a refinement) to a prior Companion offer, use the conversation history to determine what the response is about:
- If the prior Companion turn offered to remember a personal fact and the Operator confirms ("yes", "that's right", "correct"), classify as `remember_about_me` with the personal fact extracted from the prior companion turn.
- If the prior Companion turn offered to remember a personal fact and the Operator refines ("no, just shrimp", "actually it's March 16th"), classify as `remember_about_me` with the refined fact.
- If the prior Companion turn asked to confirm a retraction and the Operator confirms ("yes", "that's the one"), classify as `forget_about_me` with the forget_description from the prior exchange.
- If the prior Companion turn offered cross-promotion ("want me to remember that across all your projects?") and the Operator confirms, classify as `remember_about_me` with the fact from the prior exchange.
- If the prior Companion turn offered cross-promotion and the Operator declines ("no, just this project"), classify as `general_conversation`.
- If the Operator rejects entirely ("never mind", "forget it"), classify as `general_conversation`.
Addition 4 — Intent ordering update. The two new intents are inserted into the taxonomy list. Total intent count: nine live (seven existing + two new) and four stubs (unchanged).
The router (src/loomworks/orchestration/router.py) gains two new intent handlers.
remember_about_me handler:
async def handle_remember_about_me(
classified: ClassifiedIntent,
person: Person,
engagement: Engagement | None,
db: Session
) -> RouteResult:
Logic:
person.personal_engagement_id. If None, return an error RouteResult with operation_data={"error": "no_personal_engagement"}.conversation_turns). If the prior companion turn's classified_intent is remember_about_me and there is a held assertion on the personal engagement created by the Companion:personal_fact matching the held assertion's content, or the fact is the same as the held one): commit the held assertion. Return RouteResult with operation_data={"action": "committed", "personal_fact": content, "assertion_display_number": dn}.personal_fact that differs from the held assertion's content): amend the held assertion's content to the refined fact. Return RouteResult with operation_data={"action": "refined", "personal_fact": refined_content, "assertion_display_number": dn}. The held assertion remains held — the Companion will confirm the amended version on the next turn.general_conversation — this case doesn't reach the remember_about_me handler, so no action needed here).classified.extracted_parameters["personal_fact"]ActorRef(kind="companion")
Return RouteResult with operation_data={"action": "held", "personal_fact": content, "assertion_display_number": dn}.
Held assertion lookup. To find the most recent held assertion on the personal engagement created by the Companion, query assertions on the personal engagement where normative_force='held' and actor kind is companion, ordered by creation time DESC, limit 1. [CC verifies: the query pattern for this — existing assertion query functions may support this filter, or a targeted query is needed.]
Cross-promotion provenance. When the Companion offers cross-promotion and the Operator confirms, the personal_fact extracted by the classifier comes from the prior companion turn. The router creates the personal memory assertion with no special provenance linking in Phase 43 — the assertion content captures the fact. Provenance linking to the source engagement assertion is deferred to a future phase if needed.
Scoping note S4 note: the scoping note mentions provenance linking to the source engagement assertion. This CR defers explicit provenance linking (a FK or reference field) because it requires new schema. The personal memory assertion's content captures the fact; its creation timestamp and conversation history provide implicit provenance. If explicit provenance is needed later, it's an additive change.
forget_about_me handler:
async def handle_forget_about_me(
classified: ClassifiedIntent,
person: Person,
engagement: Engagement | None,
db: Session
) -> RouteResult:
Logic:
person.personal_engagement_id.classified_intent is forget_about_me and the prior operation_data identified a match:RouteResult with operation_data={"action": "retracted", "retracted_fact": content}.classified.extracted_parameters["forget_description"] against assertion content using keyword/substring matching.
Matching strategy: lowercase both the forget_description and each assertion's content. Split forget_description into keywords. Score each assertion by how many keywords appear in its content. Exact substring match gets highest priority.
RouteResult with operation_data={"action": "confirm_retraction", "matched_fact": content, "matched_assertion_id": id, "assertion_display_number": dn}.RouteResult with operation_data={"action": "ambiguous", "candidates": [{"content": c, "display_number": dn}, ...]}.RouteResult with operation_data={"action": "no_match"}.
Threading the assertion reference for confirmation. The forget_about_me handler's confirmation context requires knowing _which_ assertion was matched on the prior turn. Two approaches:
forget_description on the confirmation turn, and the handler re-runs the matching — since the Operator confirmed, the match should still be unique.
[CC determines: approach (a) or (b). Approach (a) is simpler (no schema change on conversation_turns). Approach (b) is more reliable (avoids re-matching). If CC chooses (b), add a nullable metadata JSONB column to conversation_turns or store the reference in the companion turn's content in a parseable way.]
Two new intent instruction templates, stored in the same location as Phase 42's templates.
remember_about_me instruction:
The Operator told you something personal — a fact about themselves, not about their project. You are noting it as personal knowledge that you'll remember across all their projects.
If this is a NEW personal fact (action is "held"):
Offer your interpretation as a confirmation: "I'll remember [your interpretation of the fact] — is that right?" Be specific in your restatement to let the Operator catch any misinterpretation.
If this is a CONFIRMATION (action is "committed"):
Confirm naturally: "Got it, I'll remember that." Keep it brief — one sentence. Don't repeat the fact in full unless it was refined.
If this is a REFINEMENT (action is "refined"):
Confirm the refined version: "Updated — I'll remember [refined fact] instead. Is that right?"
PERSONAL FACT: {operation_result}
forget_about_me instruction:
The Operator wants you to forget something personal.
If a match was found (action is "confirm_retraction"):
Confirm before retracting: "You want me to stop using [matched fact]?" Wait for confirmation before proceeding.
If this is a confirmed retraction (action is "retracted"):
Confirm the retraction: "Done — I've stopped using that." Then mention: "If you want it permanently erased, that option will be available in your settings."
If no match was found (action is "no_match"):
Let the Operator know: "I don't have anything matching that in my personal notes about you." If there are personal memory assertions, offer to list them.
If the match is ambiguous (action is "ambiguous"):
Ask for clarification: "I'm not sure which one you mean. Here's what I have: [list candidates with display numbers]."
MATCH RESULT: {operation_result}
The responder's system prompt for engagement-scoped conversation gains a cross-promotion instruction. This is a standing instruction added to the prompt assembly — not a per-intent instruction. It is included whenever the conversation is engagement-scoped (project_id is provided).
Cross-promotion instruction text:
CROSS-PROJECT MEMORY
If the Operator mentions a personal fact during this project conversation — something about them personally, not about the project domain — you may offer to remember it across all their projects. Be natural about it. Don't offer for every personal detail — only when the fact seems genuinely useful beyond this one project.
Example: "Your oven running hot might be useful for all your cooking projects — want me to remember that generally?"
If you do offer, keep it to one sentence at the end of your response. Don't make it the focus. The project conversation is primary.
Integration. The cross-promotion instruction is appended to the system prompt after the personal memory block and before the intent instruction block, only when project_id is provided. It is included regardless of the classified intent — the Companion should be aware of cross-promotion opportunity during any engagement-scoped turn.
Prompt assembly order after Phase 43:
Fix the seed-loading path so the Companion can load the personal engagement's seed content for context assembly.
Background. Phase 41 execution deviation: the personal engagement's Seed and SeedInductionLoopCycle were recorded on the personal engagement itself, not on ADMIN. Phase 42's get_current_seed reads from ADMIN — it cannot find the personal engagement's seed.
Requirement. When the engagement context loader processes the personal engagement, it must obtain the seed content. The personal engagement's seed content is minimal and known: "Personal knowledge — facts, preferences, and context that belong to the person and travel across all engagements."
[CC determines: the cleanest fix. Options from scoping note S7:
(a) Extend get_current_seed to fall back to reading from the engagement itself when ADMIN lookup fails.(b) Personal-engagement-specific seed loader (check visibility='personal', different code path).(c) Short-circuit: if the engagement is personal, return the known seed text directly.
The requirement is that after this fix, the engagement context loader can process a personal engagement without error. The context loader may already have a code path that handles missing seeds gracefully — if so, the fix may be minimal.]
Classification tests (LLM-dependent, skip if no key):
remember_about_me with personal_fact extracted.remember_about_me with personal_fact extracted.forget_about_me with forget_description extracted.forget_about_me with forget_description extracted.add_knowledge, not remember_about_me. "I prefer working in the mornings" → remember_about_me, not add_knowledge.remember_about_me with the fact from the prior turn).Router tests (deterministic):
remember_about_me with new personal fact → creates held assertion in personal engagement with ActorRef(kind="companion").remember_about_me with confirmation context (prior held assertion exists) → commits the held assertion.remember_about_me with refinement (different personal_fact from held assertion) → amends the held assertion content.forget_about_me with unique keyword match → returns confirm_retraction with matched assertion.forget_about_me with confirmed retraction context → retracts the assertion with rationale.forget_about_me with no matching assertions → returns no_match.forget_about_me with ambiguous match (multiple assertions match) → returns ambiguous with candidates.Prompt assembly tests (deterministic):
load_personal_memory returns formatted text block for a personal engagement with committed assertions.load_personal_memory returns empty string for a personal engagement with no committed assertions.load_personal_memory truncates to ~500 token budget (~2,000 characters) when assertions exceed budget. Oldest assertions dropped first.project_id is provided and personal memory is non-empty.project_id (project-less conversation).project_id is provided).Integration tests (LLM-dependent, skip if no key):
Seed reconciliation test:
Backward compatibility:
add_knowledge intent still works in project context — creates held assertion in project engagement, not personal engagement.Expected test count: ~25–30 new tests. LLM-dependent tests skip if no key available.
No new migration. Personal engagement already exists (Phase 41, migrations 0057/0058). Assertions use existing tables. Conversation turns table exists (Phase 42, migration 0059). No schema changes required for Phase 43.
If CC determines that a metadata JSONB column on conversation_turns is needed for threading assertion references (D3, forget_about_me confirmation context), that would be a new migration. [CC determines: whether this is needed based on the chosen approach.]
Phase 43 modifies existing files. No new files are expected, though CC may add utility functions or split modules if they grow too large.
| File | Change |
|------|--------|
| src/loomworks/orchestration/prompt.py | Add load_personal_memory function (D1). Extend assemble_prompt with personal_memory parameter (D1). Add cross-promotion instruction assembly (D5). |
| src/loomworks/orchestration/classifier.py | No code change — the classifier prompt text asset is updated (D2). |
| Classifier prompt text asset | Two new intents, boundary guidance, continuation guidance (D2). |
| src/loomworks/orchestration/router.py | Two new intent handlers: handle_remember_about_me, handle_forget_about_me (D3). Register in router dispatch. |
| Intent instruction templates | Two new templates: remember_about_me, forget_about_me (D4). |
| Converse endpoint router | Add load_personal_memory call before classification. Pass personal_memory to assemble_prompt (D1). |
| Seed-loading function | Fix for personal engagement seeds (D6). [CC determines: which file.] |
| Step | What | Auto/checkpoint |
|------|------|-----------------|
| 0 | Pre-flight: verify baseline (test count 1,580 + 20 skipped, Alembic 0059, classifier prompt, router dispatch, prompt assembly, personal engagement FK, assertion creation/retraction/query functions, conversation turns, seed loading, engagement context loader). Archive this CR. | Auto |
| 1 | Personal engagement seed reconciliation (D6). Verify personal engagement seed is loadable through the context assembly path. Test §D7 item 25. | Auto |
| 2 | Personal memory loader (D1). Extend prompt assembly with personal memory block. Tests §D7 items 14–19. | Auto |
| 3 | Classifier prompt update (D2). Two new intents + boundary guidance + continuation guidance. Classification tests §D7 items 1–6. | Auto |
| 4 | Router extensions (D3). remember_about_me and forget_about_me handlers with conversational commit/retraction. Tests §D7 items 7–13. | Auto |
| 5 | Intent instruction templates (D4) and cross-promotion instruction (D5). | Auto |
| 6 | Integration tests (D7 items 20–24) and backward compatibility tests (D7 items 26–27). Full round-trip tests. | Auto |
| A | Checkpoint A — Operator confirms. Tag substrate repo phase-43-personal-memory-contribution. | Checkpoint |
remember_about_me → held → committed on confirmation.forget_about_me → confirmed → retracted.add_knowledge still works (project-scoped, unaffected by personal memory).conversation_turns.metadata column is added, 0060.remember_about_me, forget_about_me), boundary guidance, and continuation guidance. Total: nine live intents, four stubs.phase-43-personal-memory-contribution on substrate repo.DUNIN7 — Done In Seven LLC — Miami, Florida Phase 43 CR — v0.1 — 2026-05-06