Version. v0.1
Date. 2026-06-26
Author. Claude.ai (scoping)
For. The Operator, to decide scope before a change request is drafted.
Repository home. loomworks-record/scoping-notes/
What this does. It gives each person a single "where I am" — a current engagement the Companion holds onto. Today, every time you talk to the Companion the caller has to say which engagement the turn is about. There is no server-side memory of "the one I'm in right now." This adds that memory: one field, current_engagement_id, on the host_account row. The Companion sets it when you say "open Goosey"; the converse path reads it so an unqualified instruction like "commit held 14" resolves against the engagement you're in, without you naming it again.
Why it matters. This is the keystone the voice-first Companion rests on. Without a server-side "here," voice can't carry unqualified verbs — every spoken command would have to re-name its engagement. With it, "open Goosey" then "commit held 14" works the way speech actually works.
What decision is needed. Two, named in section 6:
current_engagement_id back to null (leaving an engagement, an explicit "close," session boundary, or nothing), and what converse does when focus is null and the caller also omits project_id.In scope. One engine field + migration; the converse read defaulting; the Companion write on "open X"; reading "here." Out of scope. A session table; concurrent contexts; gap-4's full referent set (rides on this structure later, not built here); reference resolution "the second one" engine-side (queue item C).
Grounding. Every structural claim below was confirmed against live engine state by Claude Code on 2026-06-26 (read-only inspection). Line numbers and signatures are live, not remembered. Where the handoff carried a claim that the live code corrected, the correction is marked.
From the engagement-identity record (substrate/loomworks-engagement-identity-record-v0_1, commit 8d8fbe9) and last session's Bin-3 findings (investigations/loomworks-companion-bin3-capability-findings-v0_1, commit e3da809):
host_account, single focus per person, not a session table. The session table is deferred until concurrent contexts are a real need — they are not today.conversation_turns for a (person, engagement) pair. This field is the first piece of per-person conversational state to live server-side.operator_sequence_number, your "23"; Personal is "0"), and E#### (display_identifier, instance-local label). UUID is the identity but stays invisible to humans.This note does not reopen any of those. It grounds them against the live engine and scopes the build.
host_account is the natural home — confirmed strongly
host_account already carries a nullable engagements FK and is already loaded on the converse path:
personal_engagement_id — uuid, nullable, FK → engagements(id) (host_account_personal_engagement_id_fkey). ORM at models.py:281: Mapped[UUID | None].companion_name — text, NOT NULL, default 'Companion'. ORM at models.py:276.id = principals.id), so one column = one focus per person — the single-focus rule falls out of the table shape.
current_engagement_id is a structural clone of personal_engagement_id: Mapped[UUID | None] + ForeignKey("engagements.id"), nullable.
engagements PK = id (uuid), engagements_pkey. The FK target is grounded on the real column.0094. The migration is the next revision after 0094.
Handler: src/loomworks/orchestration/routers/converse.py:579, async def operator_converse(...).
Today, scope comes from the request body every turn — body.project_id (ConverseRequest, schemas.py:539). At converse.py:643: if body.project_id is not None: → membership verify + context load. The caller supplies the engagement on every call. No server-side focus exists — a grep for current_engagement / focused_engagement / active_engagement returns nothing relevant.
The read site is already in hand. At converse.py:631 the handler loads host_account; at :632 it reads companion_name; at :637 it reads personal_engagement_id. current_engagement_id would be read at :637, beside personal_engagement_id, with no new database load — host_account is already loaded for this person on this call.
EngagementList.tsx:137 resolves on display_identifier ?? project_id (E#### / UUID). Not name, not per-Operator number at the address bar.page.tsx:16) tolerates 0, Personal, E####, or a UUID.engagement/redirect.py:103 _resolve_engagement_name(...), used by the cross-engagement redirect flow — not the address resolver.voiceRecognizer.ts:13-14 ("engagement N → expand the row whose operator_sequence_number === N"), driving EngagementList.tsx:48,55. It expands a list row; it does not resolve to an address and has no engine-side path.
Handoff correction (drift-surface). The handoff implied the per-person sequence might be host-side. It is not — operator_sequence_number lives on memberships (BIGINT, NOT NULL, UNIQUE per person), the per-person-per-engagement sequence. This does not change the current_engagement_id-on-host_account conclusion, but it means "open 7" (by number) has no resolution path to feed the spine today. That is decision 1 in section 6.
host_account: current_engagement_id, uuid, nullable, FK → engagements(id). Structural clone of personal_engagement_id. ORM beside it in models.py.0094): add the column + FK constraint. Up/down clean round-trip.converse.py:637/:643: when the caller omits body.project_id, default scope to host_account.current_engagement_id. Shape: engagement_id = body.project_id or host_account.current_engagement_id. The membership-verify and context-load at :643 then run against the resolved id. (Decision 2 governs what happens when both are null.)current_engagement_id for the person. (Whether this is a dedicated endpoint or folded into an existing operation is a CR-level detail, named in section 7.)current_engagement_id. Resolution handles are governed by decision 1.E####, per the identity record; UUID invisible).host_account row is the home.body.project_id callers. Existing callers that pass project_id are unaffected — the default only fires when it is omitted. This is additive and backward-compatible.The Bin-3 findings cost-sorted the four genuinely-missing capabilities. This is item B, and items C and D extend its structure:
The question. Which handles resolve "open X" to an engagement the spine can focus on?
redirect.py:103), redirect-flow only; reusable.E#### / UUID — resolves today (address resolver).memberships.operator_sequence_number. Adding it means a resolution path from number → engagement for this person.Options.
memberships for the person, wired into the Companion's resolve step.Best-guess default: 1A. Ship the handles that already resolve; defer number-resolution to its own small piece. The identity record names all three as valid handles, so 1B is on-trajectory whenever taken — but it is separable, and the spine doesn't need it to stand up. Confirm with one word, or say "1B" to fold it in.
The question (two halves).
current_engagement_id back to null)? Candidates: leaving an engagement in the UI; an explicit "close" / "leave"; nothing (focus persists until the next "open"); session boundary.project_id? Candidates: error asking which engagement; fall back to Personal (personal_engagement_id); no-op with a Companion prompt.Best-guess default:
project_id → the Companion asks which engagement (does not silently fall back to Personal — silent fallback would put commits in the wrong place). Confirm, or name a different pair.
project_id-unaffected; null-focus path per decision 2; migration round-trip).E#### rendering must not assume exactly four digits.
The Loomworks seed governs what the work is, the commons, who consumes it, voice, constraints, declared shape/render-types, success conditions, authorisation. It is silent on current-engagement focus and converse-path addressing — those sit below the seed, under the architecture specification (spec governs when the seed is silent). The one seed-adjacent surface is Companion addressing vocabulary, and the engagement-identity record already governs that (name + per-Operator number + E####; UUID invisible). No conflict with the seed's commitments is visible.
Version caveat (drift-surface). This note was drafted without the canonical seed in hand — the highest seed reachable to the drafting session was v0.8, while canonical is the highest loomworks-candidate-seed-vN_M on loomworks-record main. The spine topic is below the seed regardless of version, so the alignment conclusion holds; but at CR time, the executing session should confirm the canonical seed (and the architecture spec v0.4) say nothing this note contradicts.
The read-only Step-0 inspection that normally opens a build was run during this scoping pass (2026-06-26). Findings are in section 2, grounded against live code. A CR drawn from this note starts from grounded state; it does not need to re-run Step 0 unless the engine has moved (alembic head was 0094 at grounding).
DUNIN7 — Done In Seven LLC — Miami, Florida Current-engagement spine — scoping note — v0.1 — 2026-06-26