Version. 0.1
Date. 2026-05-05
Author. Marvin Percival (DUNIN7), prepared via Claude.
Target. /Users/dunin7/loomworks (substrate) on DUNIN7-M4 (MacMini).
Priority. Standard.
Confidential. Internal.
Supersedes. Nothing for Phase 41 specifically.
Companion to. Phase 41 scoping note v0.2, personal memory and proactive Companion investigation v0.1, Companion as agent investigation v0.1, engine implementation strategy v0.2, Phase 14 CR v0.1 (person layer), Phase 15 CR v0.1 (universal commons).
Status. First draft. Consumes settled scoping decisions from scoping note v0.2 without relitigating them.
actor_from_companion helperPhase 41 is the first Arc 2 phase. It introduces the Companion as a named, identifiable entity and the personal engagement as the Companion's person-level memory surface.
Four deliverables:
1 — Companion naming. A companion_name column on the person table, defaulting to "Companion". A PATCH /me/companion-name endpoint to change it. The name is available for the system prompt and message author labels (wired in Phase 42).
2 — ActorRef(kind="companion"). A new actor kind for operations the Companion performs on behalf of a person. The actor_from_companion helper in the orchestration module constructs it from a person record.
3 — Personal engagement auto-creation. Every person gets an invisible, Memory-only engagement that belongs only to them. Migration creates one for each existing person; the signup transaction creates one atomically for new persons. A personal_engagement_id FK on the person table provides fast-path lookup.
4 — Dashboard exclusion. Phase 39's three cross-engagement endpoints and Phase 40's Orchestration API dashboard wrapper exclude engagements with visibility = 'personal'.
Substrate-only phase. One checkpoint.
Phase 41 is grounded in three layers, consulted in order.
Layer 1 — Methodology document v0.20. Memory-as-sole-write-target extends to the personal engagement: the person has a Memory too, governed by the same discipline (add, commit, retract) but with person-scoping and a permanent-deletion right (built in Phase 43, not Phase 41). Recognition is load-bearing: the Companion's name is a recognition commitment — the entity the person interacts with has a name they chose.
Layer 2 — Operator Layer discovery v0.4 and personal memory investigation v0.1. The discovery names the Companion as the primary surface (§8.1) and the dashboard as the home view (§8.2). The investigation (accepted) recommends personal memory stored as a personal engagement (option c), auto-created on signup, invisible in the dashboard, Memory-only. The investigation's §4.1 establishes companion naming as a person-level preference. The investigation's §7.2 establishes the implementation sequence: naming first, personal engagement creation second.
Layer 3 — Playground Spec v0.10 and prior phase CRs. Specific requirements:
actor_kind enum widening precedent.GET /operator/dashboard wrapper, actor_from_person helper precedent.phase-40-operator-vocabulary-schemas at commit 39e0a87.CC verifies at Step 0:
[CC verifies: model file, column names including display_name.]actor_kind enum location and current values (agent, human-contributor, person). [CC verifies: enum definition or CHECK constraint location.]private, discoverable, automatic). [CC verifies: enum definition or CHECK constraint location.][CC verifies: src/loomworks/api/routers/dashboard.py or equivalent, the subquery that selects engagement_ids for the person.][CC verifies: src/loomworks/orchestration/routers/dashboard.py or equivalent.][CC verifies: the function/endpoint that creates person + credentials + Loomworks membership atomically.][CC verifies: migration 0034 or 0035 for the _append_event_sync helper pattern.]actor_from_person helper location. [CC verifies: src/loomworks/orchestration/helpers.py.][CC verifies: Seed class in types module.][CC verifies: SeedInductionLoopCycle class in types module.]Pre-flight surprises (ground-truth divergence from this CR's assumptions) stop execution at Step 0 and drive a CR v0.2 revision; do not proceed through divergence.
| ID | Decision | Summary |
|----|----------|---------|
| S1 | Companion name on person table | companion_name TEXT NOT NULL DEFAULT 'Companion'. Column, not assertion. |
| S2 | ActorRef(kind="companion") | New enum value. Companion is distinct from agent and person. |
| S3 | Personal engagement + FK | Migration for existing persons + signup extension. personal_engagement_id FK on person table for fast-path lookup. |
| S4 | Visibility tier 'personal' | Fourth visibility value. Dashboard exclusion, engagement list exclusion, single-person ownership. |
| S5 | Memory-only by absence | Seed declares zero render-types. Other three rooms structurally dormant. |
| S6 | Dashboard exclusion in shared subquery | One filter change in the membership subquery, three endpoints covered. |
| S7 | Migration-based induction | Same pattern as Phase 15. Pre-approved single-cycle induction history. |
Phase 41 adds three Alembic migrations. CC may consolidate schema changes into fewer migrations if it simplifies the upgrade path, but the logical units are:
Migration file: 0057_phase_41_schema_additions.py (or next available number).
Changes:
'personal' to the engagement visibility column's accepted values. [CC verifies at pre-flight whether this is a CHECK constraint or a PostgreSQL ENUM type, and applies the appropriate ALTER.]companion_name TEXT NOT NULL DEFAULT 'Companion' to the person table. Existing rows receive the default.personal_engagement_id UUID NULL to the person table with a FK constraint referencing the engagements table. Nullable initially — the data migration (5.2) populates it for existing persons. The signup flow (Section 8) sets it atomically for new persons.'companion' to the actor_kind enum. [CC verifies at pre-flight whether this is a CHECK constraint or a PostgreSQL ENUM type. Follows the Phase 16 precedent (migration 0039).]Downgrade: Reverse all four changes.
Migration file: 0058_phase_41_personal_engagement_induction.py (or next available number).
For each person in the person table:
visibility = 'personal', engagement_name = f"Personal — {person.display_name}", random UUID._append_event_sync helper (Phase 15 migration pattern).convergence_reached=True, zero findings, single cycle. The actor is the ADMIN or the person as appropriate — [CC determines the correct actor pattern by reading Phase 15's migration 0034.]'operator' designation for the person.person.personal_engagement_id to the new engagement's UUID.
The migration uses the synchronous raw-SQL pattern established in Phase 15 (entry 30, manifest v0.13): inline _append_event_sync within connection.run_sync(), build JSON payloads in Python.
Engagement name. Uses f"Personal — {person.display_name}" so the engagement is identifiable in the database. The user never sees this name — the personal engagement is invisible in the dashboard and engagement lists.
Downgrade: Delete the personal engagement rows, their membership rows, their memory events, and null out person.personal_engagement_id. [CC verifies: the downgrade should identify personal engagements by visibility='personal'.]
The seed satisfies R-A5 through R-A11 with a minimal, Memory-only declaration:
PERSONAL_ENGAGEMENT_SEED = {
"what_the_work_is": (
"Personal knowledge — facts, preferences, and context that belong "
"to the person and travel across all engagements."
),
"who_consumes_the_work": "The Companion, on the person's behalf.",
"voice_of_the_work": "Not applicable — personal memory is facts, not authored content.",
"constraints": [
"Person-scoped: never visible to other persons.",
"Permanent deletion right available alongside retraction.",
],
"success_conditions": (
"The Companion can draw on personal knowledge when relevant "
"to any engagement conversation."
),
"initial_contributors": [], # populated per-person at creation
"initial_agents": [],
"additional_assertions": {
"engagement_name": "", # set per-person at creation
},
}
No render-types. No render_type_declarations key, or an empty list. This is what makes the other three rooms structurally dormant (S5).
No shape-type declarations. Same reasoning — no shapes means no Manifestation or Shaping activity.
[CC determines: the exact Seed model fields by inspecting the Seed class at pre-flight. The seed content above maps to the model; field names may differ (e.g., constraints may be a list or a string). CC adapts.]
This section elaborates on Migration B (Section 5.2) for the migration author.
For each person, the migration records the following memory objects in the event log, in order:
object_type="engagement", state="active", with a seed_ref pointing to the Seed object. Follows the Phase 2 pattern.object_type="seed", content from Section 6 with initial_contributors=[person.id] and additional_assertions.engagement_name set to f"Personal — {person.display_name}". Seed version 1.object_type="seed_induction_loop_cycle", cycle_number=1, convergence_reached=True, open_finding_count_after_cycle=0, findings_produced_refs=[], findings_addressed_refs=[]. The actor is a sentinel or the ADMIN agent per Phase 15's precedent.
After recording the memory objects, the migration must update the materialized view (current_memory_objects) so that queries reflect the new engagement and seed. [CC verifies: whether the projector runs automatically on event append, or whether the migration must call it explicitly. Phase 15's _append_event_sync inlines the projector call.]
The signup flow (Phase 14 + Phase 15 modifications) currently creates:
Phase 41 inserts between steps 5 and 6:
5a. Create personal engagement row (visibility='personal', engagement_name=f"Personal — {person.display_name}", random UUID).
5b. Record Engagement, Seed, and SeedInductionLoopCycle memory objects in the event log. Uses the async append_event function (not the sync migration helper — the signup flow runs in async context).
5c. Create membership on the personal engagement with 'operator' designation.
5d. Set person.personal_engagement_id to the new engagement's UUID.
All within the existing transaction. If any step fails, the entire signup rolls back.
[CC verifies: whether the signup flow uses a single transaction or multiple. Phase 15's implementation notes (F9) describe the membership insert ordering within the signup transaction. CC adapts the personal engagement creation to fit the same transaction.]
The engagement_name is set from the person's display_name at signup time. If the person later changes their display name, the personal engagement's name is not updated — the name is for database identification, not user-facing display (the personal engagement is invisible).
Endpoint: PATCH /me/companion-name
Request body:
class CompanionNameRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
Response:
class CompanionNameResponse(BaseModel):
companion_name: str
Behavior: Updates person.companion_name to the provided name. Returns the updated name.
Auth: Person session (same as other /me/... endpoints).
Router location: src/loomworks/api/routers/person.py or alongside other person-settings endpoints. [CC determines the appropriate router file at pre-flight.]
Name validation: Non-empty, max 50 characters. No profanity filter — the person names their own Companion.
Vocabulary wall note: This endpoint is engine-level (/me/...), not Orchestration API (/operator/...). The Orchestration API will expose companion naming through the converse endpoint or a settings endpoint in Phase 42. This endpoint provides the substrate-level write path.
actor_from_companion helper
Location: src/loomworks/orchestration/helpers.py, alongside the existing actor_from_person helper.
def actor_from_companion(person) -> ActorRef:
"""Construct an ActorRef for the Companion acting on behalf of a person.
kind='companion', id=person.id, display_name=person.companion_name.
The id is the person's UUID — the Companion's identity is bound to
the person. The display_name is the person's companion_name preference.
"""
[CC verifies: the ActorRef constructor fields (kind, id, display_name or equivalent). Follow the pattern in actor_from_person but with kind="companion" and display_name from person.companion_name.]
Phase 41 declares the helper but no Phase 41 code calls it. Phase 42 (intent classification) and Phase 43 (personal memory contribution) are the first consumers.
The three dashboard endpoints share a membership subquery that selects the person's engagement IDs. Phase 41 extends this subquery to exclude personal-visibility engagements.
Before (Phase 39):
person_engagements = (
select(EngagementMembership.engagement_id)
.where(EngagementMembership.person_id == current_person.id)
)
After (Phase 41):
person_engagements = (
select(EngagementMembership.engagement_id)
.join(Engagement, EngagementMembership.engagement_id == Engagement.id)
.where(
EngagementMembership.person_id == current_person.id,
Engagement.visibility != 'personal',
)
)
[CC verifies: the exact location of this subquery. It may be a shared function, a repeated pattern in each endpoint, or a dependency. CC applies the exclusion in the shared location if one exists; otherwise applies it in each endpoint's query.]
The GET /operator/dashboard endpoint wraps the Phase 39 endpoints. If it calls the Phase 39 endpoints directly (same process, function call), the exclusion flows through automatically. If it re-implements the queries, the same exclusion applies.
[CC verifies: whether the Orchestration API dashboard endpoint calls Phase 39 functions or re-queries. CC confirms the exclusion is inherited or applies it.]
test_companion_name.pytest_companion_name_default — after migration, existing persons have companion_name = "Companion".test_companion_name_set — PATCH /me/companion-name with {"name": "Max"} updates the person's companion name.test_companion_name_get — the companion name is returned in person-info responses or via a GET endpoint.test_companion_name_empty_rejected — empty string rejected with 422.test_companion_name_too_long_rejected — string > 50 characters rejected with 422.test_companion_name_unauthenticated — unauthenticated request returns 401.test_personal_engagement.pytest_personal_engagement_exists_after_migration — existing persons have a personal engagement (FK populated, engagement row exists).test_personal_engagement_visibility — the personal engagement has visibility = 'personal'.test_personal_engagement_seed_recorded — the engagement has a Seed memory object in the event log.test_personal_engagement_seed_no_render_types — the seed declares zero render-types.test_personal_engagement_induction_history — a SeedInductionLoopCycle with convergence_reached=True exists for the engagement.test_personal_engagement_membership — the person has a membership on the personal engagement with 'operator' designation.test_personal_engagement_fk_correct — person.personal_engagement_id points to the engagement with visibility = 'personal' that the person has a membership on.test_signup_personal_engagement.pytest_signup_creates_personal_engagement — after signup, the new person has a personal engagement (FK populated).test_signup_personal_engagement_visibility — the created engagement has visibility = 'personal'.test_signup_personal_engagement_membership — the new person has 'operator' designation on the personal engagement.test_signup_personal_engagement_seed — the personal engagement has a seed with the expected content.test_signup_personal_engagement_name — the engagement name includes the person's display name.test_signup_atomicity — if the personal engagement creation fails (e.g., simulated DB error after person creation), the entire signup rolls back (no orphaned person record). Follows the Phase 15 atomicity test pattern.test_signup_still_creates_loomworks_membership — regression: the Loomworks membership is still created.test_signup_still_creates_credentials — regression: passkey and TOTP created correctly.test_actor_from_companion.pytest_actor_from_companion_kind — actor_from_companion(person).kind == "companion".test_actor_from_companion_id — actor_from_companion(person).id == person.id.test_actor_from_companion_display_name — uses person.companion_name.test_actor_from_companion_custom_name — after renaming, the actor uses the new name.test_dashboard_exclusion.pytest_dashboard_active_excludes_personal — create an active job in the personal engagement; verify it does not appear in GET /me/dashboard/active.test_dashboard_needs_you_excludes_personal — create a held assertion in the personal engagement; verify it does not appear in GET /me/dashboard/needs_you.test_dashboard_recent_excludes_personal — create a completed render in the personal engagement (if possible without render-types — or test with a mock); verify it does not appear in GET /me/dashboard/recent.test_dashboard_includes_regular_engagements — regular engagements still appear after the exclusion filter is added.test_orchestration_dashboard_excludes_personal — the GET /operator/dashboard endpoint also excludes personal engagement items.test_visibility_personal.pytest_personal_visibility_accepted — an engagement with visibility = 'personal' can be created.test_existing_visibilities_unchanged — existing engagements (private, automatic) still have their original visibility values.Auto-mode posture: Steps 0–6 auto-mode-proceed. Checkpoint A halts until Operator confirms.
Archive this CR to docs/phase-crs/phase-41-cr-companion-identity-and-personal-engagement-v0_1.md. Run pre-flight checks (Section 3.2). Confirm baseline. Create branch phase-41-companion-identity-and-personal-engagement.
Verification: all pre-flight items confirmed. Test count matches baseline. CR archived.
Commit: Phase 41 step 0: CR archival and branch creation.
Write migration A (Section 5.1): visibility enum widening, companion_name column, personal_engagement_id FK column, actor_kind enum widening.
Verification: uv run alembic upgrade head succeeds. uv run pytest -v green (1,485 passed — no behavioral change, schema only). New columns exist on person table. New enum values accepted.
Commit: Phase 41 step 1: schema additions.
Write the personal engagement seed content as a constant (Section 6). Write migration B (Section 5.2): create personal engagements for all existing persons. Record Engagement, Seed, SeedInductionLoopCycle memory objects. Create memberships. Set FK.
Write tests: test_personal_engagement.py (Section 12.2) and test_visibility_personal.py (Section 12.6).
Verification: uv run alembic upgrade head succeeds. uv run pytest -v green. Existing persons have personal engagements with correct visibility, seed, induction history, membership, and FK.
Commit: Phase 41 step 2: personal engagement induction migration.
Extend the signup flow (Section 8) to create the personal engagement atomically for new persons.
Write tests: test_signup_personal_engagement.py (Section 12.3).
Verification: uv run pytest -v green. New signups produce a personal engagement with correct properties. Atomicity test passes. Regression tests pass.
Commit: Phase 41 step 3: signup personal engagement creation.
Write the PATCH /me/companion-name endpoint (Section 9). Write the actor_from_companion helper (Section 10).
Write tests: test_companion_name.py (Section 12.1) and test_actor_from_companion.py (Section 12.4).
Verification: uv run pytest -v green. Companion name is settable and retrievable. Actor helper constructs correctly.
Commit: Phase 41 step 4: companion name endpoint and actor helper.
Update the Phase 39 dashboard queries and Phase 40 Orchestration API dashboard wrapper (Section 11).
Write tests: test_dashboard_exclusion.py (Section 12.5).
Verification: uv run pytest -v green. Dashboard endpoints exclude personal engagement items. Regular engagement items still appear. Orchestration API wrapper also excludes.
Commit: Phase 41 step 5: dashboard exclusion for personal engagements.
Run the full test suite. Verify no regressions. Check that existing Phase 14, 15, 39, and 40 tests still pass.
Verification: uv run pytest -v green. Total test count = baseline + new tests.
Commit: Phase 41 step 6: final test sweep (only if any cleanup was needed).
Operator confirms. Tag substrate repo.
All Phase 41 work complete. Companion naming operational. Personal engagements exist for all persons. Dashboard exclusion working. Actor helper ready. All tests green.
Tag: phase-41-companion-identity-and-personal-engagement on DUNIN7/loomworks.
Implementation notes: docs/phase-impl-notes/phase-41-implementation-notes-v0_1.md absorbs execution-time surprises and findings.
companion_name = "Companion" after migration.PATCH /me/companion-name updates the companion name. Validation enforces 1–50 characters.ActorRef(kind="companion") constructs correctly via actor_from_companion(person).visibility = 'personal'.'operator' membership on their personal engagement.person.personal_engagement_id FK points to the correct personal engagement.GET /me/dashboard/active, needs_you, recent exclude personal engagement items.GET /operator/dashboard excludes personal engagement items.
On acceptance: tag substrate repo as phase-41-companion-identity-and-personal-engagement. Write implementation notes.
companion_name TEXT NOT NULL DEFAULT 'Companion' and personal_engagement_id UUID FK→engagements.'companion' value.'personal' value.visibility = 'personal' engagements.actor_from_companion helper.PATCH /me/companion-name.phase-41-companion-identity-and-personal-engagement on substrate repo.
Read the Change Request document at the path I supply below. This is
CR-2026-055 v0.1, the Phase 41 Change Request. You are the executing
agent named in the CR.
CR path: ~/Downloads/phase-41-cr-companion-identity-and-personal-engagement-v0_1.md
Phase 41 is the first Arc 2 phase. It introduces the Companion as a
named entity and the personal engagement as a person-level memory
surface.
Four deliverables: companion naming on person table, ActorRef
kind="companion", personal engagement auto-creation (migration +
signup), dashboard exclusion for personal engagements.
Substrate-only phase. Six steps, one checkpoint.
Code baseline: at HEAD after Phase 40. Post-Phase-40 test count.
Run pre-flight (Step 0) per Section 3.2. The Step 0 checklist
includes: person table, actor_kind enum, visibility enum, dashboard
router, orchestration dashboard wrapper, signup transaction, Phase 15
migration pattern, actor_from_person helper, Seed model,
SeedInductionLoopCycle model.
Per Section 13, six steps with one checkpoint. Auto-mode posture:
Steps 0–6 auto-mode-proceed; Checkpoint A halts for Operator
to confirm and tag.
Pre-flight surprises (Section 3.2 ground-truth divergence) stop
execution at Step 0 and drive a CR v0.2 revision; do not proceed
through divergence.
Implementation notes at Checkpoint A:
docs/phase-impl-notes/phase-41-implementation-notes-v0_1.md
absorbs execution-time surprises and findings.
DUNIN7 — Done In Seven LLC — Miami, Florida Phase 41: Companion Identity and Personal Engagement — CR v0.1 — 2026-05-05