DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path investigations/loomworks-companion-bin3-capability-findings-v0_1.html

DUNIN7 · Loomworks · Findings

Companion Bin-3 capability gaps — findings and engine-work queue

Version. v0.1
Date. 2026-06-25
Author. Claude.ai, grounded on Claude Code (CC) read-only engine traces.
Status. Findings record. Confirms the four Bin-3 gaps, sorts them by what building each entails, surfaces the shared root of two of them, and records the Operator's structure decision. Produces a grounded, cost-sorted build queue. No build authorized; each item becomes its own scoping note → CR, fresh.
Grounds on. CC read-only traces (2026-06-25) of /operator/converse, the conversation/turn wiring, orchestration aggregates, the voice frontend. The Companion design document (loomworks-companion-design-v0_1). The engagement-identity decision record (addressing context).

Plain-language summary

The Companion design named four capabilities the ideal Companion needs that the engine doesn't obviously provide. This record confirms each and — more usefully — establishes what building each one costs, reshaping "four things to build" into a sorted queue:

  • Voice output (text-to-speech) is nearly free — pure frontend, no engine change. The engine already returns the Companion's message as text; the browser reads it aloud. Ship anytime.
  • Current-engagement context — the spine — has a small minimal form: one new field on an existing table (host_account.current_engagement_id), not a subsystem. Settled (field, not table).
  • Reference resolution ("that one", "the second") shares a root with the spine: both need a place to hold per-conversation state, and that place doesn't exist. Build the spine and reference resolution extends it.
  • Composite engagement-state read ("where is Goosey") works today by assembling existing per-room reads; a dedicated endpoint is an optimization, not a requirement.

1. The four gaps, confirmed and costed

Gap 1 — Conversational context / current-engagement (the spine)

Confirmed. No engine notion of "current engagement." Addressing resolves on display_identifier client-side (EngagementList.tsx:137address = row.display_identifier ?? project_id).

What converse persists between turns: only a flat conversation_turns log. No conversation_id, no session_id, no session/context table. Each turn row carries (person_id, engagement_id?, role, content, classified_intent, …). A "conversation" is implicit — the set of turns for a person+engagement pair — not a stored entity. Converse is stateless per call plus a caller-supplied project_id every turn; the only cross-turn memory is the turn log read back as history.

Durable "here": no conversation/session record exists to extend. The natural home is a new structure — minimally a per-person focused-engagement field on the existing host_account (which already holds personal_engagement_id + companion_name), or a dedicated session/context table.

Verdict: CONFIRMED-GAP · both (engine persists; Companion sets/reads) · needs new structure — minimal form is current_engagement_id on host_account.

Gap 2 — Composite engagement-state read

Confirmed. No single per-engagement four-room composite exists. /operator/dashboard is a cross-engagement rollup, not per-engagement. /operator/projects/{id}/story is a stub ("coming soon") — not built, and narrative-shaped, not state-shaped. memory_status is partial (Memory↔Manifestation freshness only).

Assemblable today from existing per-room list endpoints (assertions, manifestations, shape-events, renders, considerations, /me/dashboard/needs_you) via fan-out — no new engine route strictly required. A clean one-call "where is Goosey" would want a new aggregate endpoint for efficiency and a stable contract, but does not require one.

Verdict: CONFIRMED-GAP · both (fan-out works now; optimal is a new aggregate) · needs new structure only if done engine-side; otherwise orchestration over existing routes.

Gap 3 — Voice output (text-to-speech)

Confirmed input-only. Voice input is browser SpeechRecognition (useVoiceListening.ts). Voice output: none — no speechSynthesis / .speak( in the frontend, no TTS / audio-out in the engine.

Where it would live: pure frontend. The engine already returns companion_message text — that is the spoken form. TTS is the browser SpeechSynthesis API reading it; no engine involvement.

Verdict: CONFIRMED-GAP · Companion work only · extends existing — speak companion_message via browser SpeechSynthesis; no new structure, no engine change.

Gap 4 — Reference resolution ("that one" / "the second" / "it")

Confirmed absent. No anaphora/ordinal/referent resolution in converse. The classifier extracts explicit references only (commit_assertion pulls held_reference as the literal number). The one cross-turn carry is the commit-confirmation turn (an explicit number, not a resolved anaphor). No structured "things just shown" referent set with object ids between turns. (Converse passes recent_turns into the prompt, so the model may loosely resolve anaphora, but nothing deterministic maps "the second one" → an id.)

Seam — both, by path: Companion-side is the natural near-term home (the frontend owns the rendered ordered lists, so it can map "the second" → a concrete handle before the converse call). Engine-side resolution (the pure-voice path, no list rendered) needs a referent set persisted per conversation — which ties straight back to gap 1.

Verdict: CONFIRMED-GAP · both (Companion-side substitution; engine-side for pure-voice) · needs new structure engine-side (a per-conversation referent set, dependent on gap 1's structure); Companion-side extends existing rendering state.

2. The structural insight — gaps 1 and 4 share a root

There is no per-conversation server-side state object. A "conversation" today is implicit — just the set of turns for a person+engagement pair in conversation_turns. So the Companion has nowhere durable to hold anything between turns except the flat turn log.

Both the spine (current-engagement, gap 1) and the engine-side of reference resolution (the referent set, gap 4) need exactly this missing place. Build gap 1's structure and gap 4's engine-side extends it; skip it and both stay blocked. This is the single load-bearing structural decision in the queue. Gaps 2 and 3 are independent of this root.

3. The structure decision (Operator, settled)

Settled — field on host_account, not a session table

The Companion's "here" lives as a current_engagement_id field on host_account — single focused engagement per person.

  • host_account already holds the per-person host state (personal_engagement_id, companion_name), so this extends an existing table rather than introducing a subsystem.
  • It is the smallest thing that works and matches how a Companion is used — one "here" at a time (you're in Goosey, then FieldPilot).
  • Deferred alternative: a dedicated session/context table, needed only if multiple concurrent conversational contexts per person become a real requirement (several voice conversations in flight, a multi-device future). Promoting the field to a table is a contained migration when that need is concrete. Build the skeleton first.

When gap 4's engine-side is built, its per-conversation referent set hangs off this same per-person context.

4. The build queue (cost- and dependency-sorted)

OrderGapEngine workCompanion workDepends onCost
A3 — TTSnonespeak companion_message via browser SpeechSynthesisnothingnearly free, frontend-only
B1 — Current-engagement (spine)add current_engagement_id to host_account; converse reads/writesset/read focus; "open X" → set focusnothingsmall engine change + wiring
C4 — Reference resolutionper-conversation referent set (extends B)substitute "the second" from rendered lists (can precede)engine-side: BCompanion-side cheap now; engine-side rides on B
D2 — Composite readoptional new aggregate endpointfan-out over existing reads works nownothing (optimization)do when fan-out gets slow

Reading the order: A (TTS) ships independently anytime — depends on nothing, gives the Companion a voice (the missing half of voice-first). B (spine) is the keystone — small (current_engagement_id) but unblocks voice-direct and is the structure C extends. C — the Companion-side can come early; the engine-side waits for B. D is an optimization, not a blocker — fan-out works today.

5. Next step

Each queue item becomes its own scoping note → CR, drafted fresh per session discipline. The natural first scoping note is B — the current-engagement spine, now fully grounded: a current_engagement_id field on host_account, converse reading/writing it, the Companion setting it on "open X" and reading it for "here". A — TTS is an independent, near-free frontend piece that can be scoped in parallel whenever wanted.

What this record does not do