DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path scoping-notes/loomworks-current-engagement-spine-scoping-note-v0_2.html

DUNIN7 · Loomworks · Scoping Note

Current-engagement spine

Version. v0.2
Date. 2026-06-26
Author. Claude.ai (scoping)
For. The Operator. The two open decisions are now settled (section 6); this note records them.
Repository home. loomworks-record/scoping-notes/
Change from v0.1. Decisions settled — Decision 1 → 1B (all three addressing handles, including per-Operator number); Decision 2 → defaults (focus persists until next open/close; null focus + omitted scope → Companion asks). Sections 3 and 4 updated so by-number resolution is in scope.

Read this first

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. None now — both settled (section 6): (1) addressing scope for "open X"1B: name, E####/UUID, AND per-Operator number ("open 7") all resolve; by-number does not resolve today, so a number→engagement lookup is now in scope. (2) clear-focus semanticsdefaults: focus persists until next open/close; null focus + omitted project_id → Companion asks (no silent Personal fallback).

In scope. One engine field + migration; the converse read defaulting; the Companion write on "open X"; reading "here"; a per-Operator-number → engagement resolution path (new, per 1B). Out of scope. A session table; concurrent contexts; gap-4's full referent set (rides on this structure later); reference resolution 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 the live code corrected, the correction is marked.

1. What is settled coming in

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):

This note does not reopen any of those. It grounds them against the live engine and scopes the build.

2. Live engine state (grounded 2026-06-26, read-only)

2.1 host_account is the natural home — confirmed strongly

host_account already carries a nullable engagements FK and is already loaded on the converse path:

current_engagement_id is a structural clone of personal_engagement_id: Mapped[UUID | None] + ForeignKey("engagements.id"), nullable.

2.2 The converse read site already exists

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 loadhost_account is already loaded for this person on this call.

2.3 "open X" resolution today — the one correction to carry

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.

3. What this builds

3.1 Engine

  1. One field on host_account: current_engagement_id, uuid, nullable, FK → engagements(id). Structural clone of personal_engagement_id. ORM beside it in models.py.
  2. One migration (next revision after 0094): add the column + FK constraint. Up/down clean round-trip.
  3. Converse read defaulting at 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.)
  4. A write path to set focus: an engine operation the Companion calls on "open X" that sets current_engagement_id for the person. (Dedicated endpoint vs. folded into an existing operation is a CR-level detail, section 7.)

3.2 Companion

  1. "open X" → resolve → set focus. The Companion resolves the spoken handle, then sets current_engagement_id. Per Decision 1B, all three handles resolve: name (reuse redirect.py:103), E#### / UUID (existing address resolver), and per-Operator number (new — item 4).
  2. Reading "here." "where am I" / "what am I in" surfaces the current engagement (name + per-Operator number + E####, per the identity record; UUID invisible).
  3. Unqualified verbs ride the focus. After "open Goosey," "commit held 14" resolves against the focused engagement with no re-naming.
  4. Per-Operator-number resolution (new, Decision 1B). "open 7" → find the engagement whose memberships.operator_sequence_number equals 7 for this person → resolve to its engagement id. Today the number only drives frontend list-row expansion (voiceRecognizer.ts:13-14); this adds a real number→engagement resolution the Companion can act on, scoped to the calling person.

4. What this does NOT build

5. Why this is the keystone

The Bin-3 findings cost-sorted the four genuinely-missing capabilities. This is item B, and items C and D extend its structure:

6. Decisions — settled

Both decisions that were open in v0.1 are now decided by the Operator (2026-06-26). The original framing and options are preserved for the record; the decision is marked under each.

Decision 1 — Addressing scope for "open X" → 1B (decided)

The question. Which handles resolve "open X" to an engagement?

  • Name — resolves today (redirect.py:103), redirect-flow only; reusable.
  • E#### / UUID — resolves today (address resolver).
  • Per-Operator number ("open 7") — does not resolve to an address today; only a list-expansion shorthand on memberships.operator_sequence_number. Adding it means a number → engagement path for this person.

Options as offered

  • 1A — Name + E#### / UUID only (defer number-resolution). Smaller.
  • 1B — Add per-Operator number resolution so "open 7" works too. Larger.

DECIDED: 1B. All three addressing handles resolve. The number→engagement lookup against memberships (filtered to the calling person) is now in scope and built as part of this work — see section 3.2 item 4. Cost named plainly: by-number resolution does not exist anywhere today, so this is fresh build, not reuse; it folds into the same piece of work.

Decision 2 — Clear-focus and null-focus semantics → defaults (decided)

The question (two halves).

  • What clears focus (sets current_engagement_id back to null)?
  • What does converse do when focus is null AND the caller omits project_id?

DECIDED: defaults.

  • Focus persists until the next "open" or an explicit "close." No session-boundary clearing (no session object to hang it on). You stay where you were until you move.
  • Null focus + omitted project_id → the Companion asks which engagement. It does not silently fall back to Personal — silent fallback would risk filing a commit into the wrong engagement.

7. CR-level details deferred (named, not decided here)

8. Seed alignment

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 architecture spec v0.4) say nothing this note contradicts.

9. Step-0 inspection — already done

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).