DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-105-stele-extraction-phase-2-v0_1.md

Loomworks change request — Stele extraction Phase 2 (move the actor constructors; split orchestration/helpers.py; reference the ActorRef type, do not move it) — CR-2026-105 — v0.1

Version. 0.1 Date. 2026-06-14 Status. Change request — the third executable phase of the Stele extraction, drafted against the post-Phase-1 tree (engine origin/main f1cf170; suite baseline 2920 passed / 46 skipped / 0 failed; alembic head 0083, unchanged). Phase 2 moves the two actor constructors — the helpers that label a recorded action with who performed it — out of orchestration/helpers.py into stele/. This is a file split, not a whole-module move: helpers.py also holds a project-membership exception and verifier that stay. The ActorRef type is owned by the memory substrate and is referenced, not moved. Markdown primary (technical consumer: Claude Code). Halt-before-push; per-step commits; verify against engine-database truth, not the suite alone.

Decisions this CR rests on (settled, carried from Phase 0/1): (1) Stele's interface is a host-agnostic, in-process-first / service-ready API contract. (2) The contract is complete (passkey-revoke, TOTP-rotate arrive Phase 5). (3) Sign-up split is approach B (Phase 4, not here). (4) Move, not duplicate — one implementation throughout; safety from small reversible slices. (5) White-label / multi-language hooks thread into user-facing ops in later phases. (6) Phase 0 and Phase 1 are complete and pushed (engine f1cf170): the full core identity surface (session.py, webauthn.py, recovery.py, models.py, registry.py, credentials.py) lives in stele/ under a shared Base at loomworks.identity_base; the gating persons.id FK / Base decision is locked (D1 = own-its-own-table, D2 = Branch 1, physical split deferred to Phase 6).

Source grounding. The Stele body-of-work brief v0.4 (§5 phase table, Phase 2 row = "Move the actor construction … Low, contained"); the integration design v0.2 (§3.3 the actor constructors; §5.2 ActorRef referenced-not-moved); the Phase 1 completion record v0.1 (§7 Phase 2 handoff and its cautions); and a tree-truth verification at f1cf170 recorded in stele-phase-2-tree-truth-v0_1 and §3 below. This CR is drafted against that pull, not from memory — the Phase 0 / Phase 1 grounding discipline.


Plain-language summary

This moves the two small functions that stamp a recorded action with who did it — one for a person, one for that person's Companion — into the Stele module, where the rest of the identity code now lives. The functions are tiny and produce a value (an ActorRef) whose type belongs to the memory substrate; Phase 2 moves the constructors, not the type — the type is referenced by import, exactly as the integration design requires.

The one real care-point: the file these functions live in today, orchestration/helpers.py, also holds two unrelated things — a project-membership error and a project-membership check — that must stay where they are. So this is a split, not a whole-file move: extract the two actor functions into Stele, leave the project-membership pieces in orchestration/, and fix up the imports on both sides. This is the same hazard Phase 1 met when persons/models.py held both moving and staying classes; the discipline is identical (split per-symbol, repoint each importer, change no behaviour).

Two myths from the brief get corrected here against the live tree. First, the brief's names (actor.for_principal / actor.for_companion) do not exist as code — the real symbols are actor_from_person and actor_from_companion. Second, "zero external callers" is false for these constructors: there are 6 callers outside the identity set (4 production routers + 2 tests), all of which repoint. And the brief's "~93 actor-label call sites" is stale — the real surface is 228 actor= kwargs and 73 inline ActorRef( constructions, none of which Phase 2 touches; Stele slots in underneath them unchanged. No schema change, no behaviour change. If anything looks off at the halt, the offending step reverts as a single commit.


1. Scope of Phase 2

In scope — split out of orchestration/helpers.py, move into stele/:

Destination. A new module in the Stele package holding the two constructors. Default placement: src/loomworks/stele/actor.py. [CC verifies at Step 0 pre-flight item 4] — confirm stele/actor.py does not already exist and the name does not collide; if the Stele package convention favors folding these into an existing Stele module (e.g. alongside the registry, since both take a Person), CC reports the alternative at pre-flight and the Operator picks. Default = a dedicated stele/actor.py, because the constructors are a distinct contract surface (integration design §3.3 names "Principal → actor" as its own operation group) and a dedicated module keeps the Phase 3 actor.for_agent constructor a clean later addition to the same file.

References (do not move) — the ActorRef type. ActorRef is defined at memory/base.py:17 and owned by the memory substrate (integration design §5.2). The moved constructors import it: from loomworks.memory.base import ActorRef. The import travels with the constructors; the type does not move. The response-layer mirrors (ActorRefResponse at api/schemas.py:2426, ActorRefSchema at api/schemas.py:4179) are not Stele's and are untouched.

Stays in orchestration/helpers.py (NOT moving this phase):

Explicitly NOT in scope (later phases, unchanged from the brief and Phase 1 §7 handoff):

2. The naming correction (recorded, not smoothed)

Brief v0.4 and integration design §3.3 name the constructors actor.for_principal / actor.for_companion / actor.for_agent. These names do not exist as code symbols at f1cf170. The live engine has:

Phase 2 uses the real symbol names (actor_from_person, actor_from_companion) and does not rename them — a rename would be a behaviour-adjacent change touching all 6 callers' call sites, not just their import lines, and renaming is out of scope for a move phase (Phase 0/1 discipline: import lines may change, symbols and behaviour may not). The dotted contract names (actor.for_principal) remain the integration-design contract vocabulary; the underscore names are the Python realization. This divergence is a doc-sync note for the next integration-design version (§7), not an execution-path change.

3. Tree-truth verification (at f1cf170) — blast radius and the split boundary

Move-not-duplicate means every importer of a moved symbol gets an import-line repoint (the Phase 0/1 mechanic). The new difficulty Phase 2 carries — identical in class to Phase 1's persons/models.py — is that helpers.py is a mixed module: it holds both moving (the two constructors) and staying (the project-membership exception + verifier) symbols. The split must be precise.

3.1 The two constructors and their importers

Importer files: 7 (4 production + 3 test + 0 script). All are outside helpers.py.

actor_from_person:

| File | Import line | Call site(s) | Note | |---|---|---|---| | orchestration/routers/inbox.py | :48 | :290 | module-top import | | orchestration/routers/converse.py | :53 | :1155 | module-top import | | orchestration/routers/composition.py | :41 | :150 | module-top import | | orchestration/router.py | :1099 | :1201 | function-local import | | tests/test_stepup_commit_gate.py | :26 | :257, :274 | module-top import | | tests/test_engagement_creation_walk_guards.py | :252, :332 | :282, :289, :337 | function-local imports |

actor_from_companion:

| File | Import line | Call site(s) | Note | |---|---|---|---| | orchestration/router.py | :70 | :293, :2053, :2221 | module-top import | | orchestration/routers/converse.py | :52 | :1190 | module-top import | | tests/test_actor_from_companion.py | :16 | :38, :46, :54, :61 | dedicated unit test — identity set |

3.2 Identity set vs outside callers

> "zero external callers" is FALSE for Phase 2. Read it as "zero outside the identity set" (the Phase 0 lesson) — and here that count is 6, not zero. Every one repoints to the new stele/ path. (All production callers are within orchestration/; no non-orchestration subsystem imports these constructors.)

3.3 The split boundary inside helpers.py (the central hazard)

orchestration/helpers.py holds, in order:

| Lines | Symbol | Disposition | |---|---|---| | :1-19 | module docstring (half about each side) | split — author a new docstring for stele/actor.py; trim the moved-away half from the helpers.py docstring | | :22-30 | imports (see partition below) | partition | | :33-46 | OperatorProjectNotFoundError | stays | | :48-60 | actor_from_person | moves | | :63-84 | actor_from_companion | moves | | :86+ | verify_project_membership(...) | stays |

Import partition (verified at f1cf170):

Person is the shared import: it is needed by both the moving constructors and the staying verifier, so after the split both stele/actor.py and orchestration/helpers.py import Person from loomworks.stele.registry. This is partition, not duplication of logic — each module imports the type it uses.

[CC verifies at Step 0 pre-flight item 2] — exact line numbers for the docstring span, the two functions, the exception, and the verifier against the live helpers.py, since a Phase-1-era edit could have shifted them by a line or two. The split boundary (which symbol goes where) is fixed by this CR; the exact line coordinates are confirmed at pre-flight.

3.4 The lookalike — do not conflate

api/routers/seed_conversation.py:115 defines _companion_actor() building ActorRef(kind="agent", id=_COMPANION_ACTOR_ID, display_name="Loomworks companion"). This is not actor_from_companion — it builds kind="agent" (a fixed system-companion agent id), lives in a different module, and is one of the 64 inline agent sites. Phase 2 moves only helpers.actor_from_companion (kind="companion", bound to person.id). Do not touch _companion_actor.

3.5 Label call sites — the brief's "~93" is stale and untouched regardless

The brief's "~93 places that use the identity to label an action" matches no single live pattern at f1cf170:

| Pattern | Count | |---|---| | \bactor= (kwarg passing an actor to an action) | 228 | | ActorRef\( (inline actor constructions) | 73 | | \bactor_ref= | 2 |

None of these are touched by Phase 2. Moving the two helpers constructors changes only their 7 importers (§3.1). The 228 actor= / 73 ActorRef( surface "slots in underneath them" unchanged — exactly the brief's intent. The CR records the real numbers (228 actor=, 73 ActorRef() and supersedes the stale "~93" so no one goes hunting for 93 sites that do not exist as a coherent set.

3.6 Halt condition (carried from Phase 0/1)

Halt and report if: any importer needs more than an import-line change (a logic edit); the split would require editing either staying symbol's behaviour; or the suite cannot be made green by repointing alone.

4. Steps (each its own commit; halt before push)

Step 2.1 — Create stele/actor.py and move the two constructors. Create src/loomworks/stele/actor.py (or the pre-flight-confirmed destination). Move actor_from_person (:48-60) and actor_from_companion (:63-84) into it, byte-identical bodies. Add the moving imports (ActorRef from memory.base; Person from stele.registry) and a fresh module docstring. In the same commit, remove the two functions from orchestration/helpers.py, trim the moved-away half of its docstring, and prune any import that helpers.py no longer uses only because the constructors left (audit: ActorRef — if verify_project_membership and the exception do not use it, the ActorRef import leaves with the constructors; Person stays, still used by the verifier). Repoint the dedicated unit test tests/test_actor_from_companion.py:16 to stele/actor.py in this same commit (it is the constructors' identity-set test). Full suite. Commit: Phase 2 step 2.1: move actor constructors to stele/actor.py.

> Per move-not-duplicate, the move and the removal-from-origin are one commit — there is never a window where both copies exist (the Phase 1 mechanic: each move + its origin-removal + its repoints bundle together).

Step 2.2 — Repoint the 4 production callers. Repoint to from loomworks.stele.actor import …:

No call-site logic changes — only the import path. Full suite + DB-truth (a sign-in turn through converse exercising a person-actor stamp against playground_dev, confirming the relocated constructor produces the same ActorRef). Commit: Phase 2 step 2.2: repoint production actor-constructor importers.

Step 2.3 — Repoint the remaining test callers. Repoint:

(tests/test_actor_from_companion.py was already repointed in Step 2.1.) Full suite. Commit: Phase 2 step 2.3: repoint test actor-constructor importers.

Step 2.4 — Fix the stale non-importer mentions (accuracy, non-breaking). These do not break the suite (no import), but become inaccurate after the move:

Full suite (still green; these are comment/docstring edits). Commit: Phase 2 step 2.4: sync stale comments/docstrings to new actor-constructor home.

Step 2.5 — Full-suite regression + database-truth. Run the complete suite (target: 2920/46/0, unchanged — no test added or removed, only repointed). Verify against playground_dev:

Halt and report. (Verification-only — no commit, matching Phase 1's Step 1.5.)

5. What must not happen in Phase 2

6. Halt and report

After Step 2.5, halt before any push. Report: the destination module chosen (stele/actor.py or the pre-flight alternative); confirmation the two constructors moved byte-identical and the two project-membership symbols stayed; the per-file importer-repoint list (6 outside callers + the identity-set unit test) with confirmation no call-site logic changed; the import-partition result (which imports moved, which stayed, Person on both sides); the comment/docstring sync; the no-schema-change confirmation (alembic 0083); and the full-suite + database-truth (person-actor and companion-actor stamps from stele/) result. On the Operator's review, push Phase 2; Phase 3 (get_current_person / session-resolution → Stele) is the next CR.

7. Doc-sync notes (not on the execution path)

8. Reversibility

Every step is its own commit; the offending step reverts as a single commit with no effect on the others. There is no schema change to unwind and no parallel copy to reconcile — a revert puts the moved constructors back in helpers.py and restores the prior import paths. The split is reversible by re-merging the two functions into helpers.py and reverting the import partition.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks change request — Stele extraction Phase 2 (the actor constructors) — CR-2026-105 — v0.1 — 2026-06-14