Document: cr-create-entry-cold-open-v0_1
Version: v0.1
Date: 2026-06-28
Type: Phase change request — engine only (loomworks-engine). Unblocks the already-built door-1 frontend branch (feat-create-conversation-opening-turn, OL fab220d, committed + unpushed). The two ship together.
Consumer: Claude Code (executor).
Branch base: engine main @ 5ccc3d7.
Origin: The door-1 live check found that the create_engagement_entry greeting 400s on an empty opener — generate_response unconditionally appends {"role":"user","content": operator_message}, and the Anthropic Messages API rejects an empty/system-only call. The greeting machinery works in response to a non-empty opener; there is no cold-open path. The frontend correctly fires an empty opening converse; the engine cannot answer it. This adds the cold-open.
Grounding: the engine cold-open Step-0 grounding (the generate_response message-array construction, the Anthropic API constraint, the orient server-compose-and-bypass precedent, the turn-recording site, the _scope_creation_history slice — engine 5ccc3d7, confirmed against the live engine + real Anthropic where noted).
When the create page opens, the front end asks the engine to greet the Operator before they've typed anything. The engine can't do that today: its way of producing a reply always sends the Operator's message to the AI model, and an empty message is rejected. So the create page currently shows an error fallback instead of a greeting. This fixes it the simplest correct way: the create greeting becomes a fixed opening line the engine composes directly — no AI call, no empty message, no error possible — using the exact same mechanism an existing feature (orient) already uses to compose a message without calling the model. It also stops the engine from recording the empty Operator message as a turn (which was corrupting later turns). The result: open the create page, get a clean greeting, and the conversation begins for real on your first answer.
generate_response always appends a user turn. responder.py:113-114: builds history → messages, then unconditionally messages.append({"role":"user","content": operator_message}). For a cold open (history [], operator_message="") this yields [{"role":"user","content":""}].400 messages.0: user messages must have non-empty content. The Messages API requires a non-empty first user turn; a system-only/empty-messages call is impossible, not merely unbuilt. (The classifier call site, classifier.py:214, confirms the house pattern — always exactly one non-empty user turn.) So any LLM cold-open is impossible; the greeting must be composed without an LLM call.converse.py:997) server-composes companion_message and bypasses assemble_prompt + generate_response entirely, then falls through to normal turn-recording. This is exactly the shape a cold-open greeting needs, and it is proven working in the repo._route_create_engagement_entry does no greeting work (router.py:1742) — it verifies the personal engagement, returns operation_data={"creation_mode":"entry"}; the responder composes the greeting from create_engagement_entry.md + persona today. (That template is written to respond to a signal — it has no cold-open form.)converse.py:1172-1196) — inside the non-NON_CONVERSATION branch, the operator turn (content=body.message) then the companion turn are written. The empty operator turn poisoned subsequent turns (the live check found all converse broken until the 8 empty turns were cleaned). The cold-open must skip recording the empty operator turn, while still recording the companion greeting (it forms the create_engagement_entry boundary + shows in scrollback)._scope_creation_history (converse.py:141-152) already slices the active turn's history to begin at the companion entry turn; with or without the operator opener recorded, the active turn's history begins [assistant entry-greeting]. (One pre-existing property — the active turn's history leads with an assistant role — is unchanged by cold-open but flagged for the live check, since it was not fully resolvable read-only.)Chosen: a templated greeting, server-composed, LLM-bypassed — the orient pattern — plus skip recording the empty operator turn.
Considered and set aside: Option S (call the LLM with a synthetic non-persisted user framing — "[The Operator just opened the create surface — greet them]"). Viable and provenance-clean (the synthetic text is never persisted), but it reintroduces the LLM round-trip (and thus the fallback-error risk that just broke) for no benefit at the cold open — there is no Operator input yet for the LLM's persona-awareness to work with. The LLM would be greeting into the void.
Why T is not just simpler but more correct: the create greeting is the opening of a structured flow — the same moment every time, with no context yet (the Operator hasn't spoken). A fixed greeting is honest about what that moment is: a deterministic door-opening, not a conversation. The LLM's warmth has nothing to work with until the Operator's first real answer — which is exactly where the conversation (and the LLM) begins, on turn two. Option S tries to make the model speak first about nothing; Option T accepts the first turn is a fixed invitation and the model joins when there's something to respond to. T also eliminates the empty-user-turn failure mode by construction (no LLM call → no user turn → no 400 → cannot hit the fallback), and reuses a proven in-repo mechanism.
create_engagement_entry cold-open.Per-step commits, suite green at each, halt-before-push. Engine only.
Confirm against engine 5ccc3d7:
converse.py:997) — exactly how orient server-composes companion_message and bypasses assemble_prompt/generate_response, then rejoins turn-recording. The cold-open mirrors this; confirm the precise rejoin point and that the bypass is clean.intent == create_engagement_entry AND body.message empty (the frontend sends intent_hint="create_engagement_entry" + message: ""). Confirm the dispatch point where this branch attaches (before the responder call, parallel to orient).converse.py:1172-1196) — exactly where to skip the operator-turn write when body.message is empty while still writing the companion greeting. Confirm the companion greeting still forms the create_engagement_entry boundary _scope_creation_history expects.companion_name). Lean: a fixed greeting that asks the first elicitation question ("Let's set up a new engagement. What's the work, and who's it for?"), optionally interpolating the companion name. Plain-terms.Report findings, then build.
At the dispatch point (Step-0.3), add a cold-open branch: when intent == create_engagement_entry and the opener message is empty, server-compose the greeting (the orient pattern — set companion_message to the templated greeting, bypass assemble_prompt/generate_response). No LLM call.
Suite expectation: an empty-message create_engagement_entry converse returns the templated greeting as companion_message (NOT the LLM fallback, NOT a 400); a non-empty create_engagement_entry still routes to the responder + entry template (unchanged).
At the turn-recording site (Step-0.4): when the opener message is empty, skip the operator-turn write; still write the companion greeting turn (forming the entry boundary).
Suite expectation: after a cold-open, the turn log holds the companion greeting and no empty operator turn; subsequent converse turns are unpoisoned (no 400 cascade — the live check's failure mode); the active-turn history scoping still finds its [assistant entry-greeting] boundary.
Against the running engine + real Anthropic (not mocks — this is the lesson the door-1 live check taught):
create_engagement_entry opener → the templated greeting renders (no fallback, no 400).Acceptance is this live sequence passing — not a green suite alone.
create_engagement_entry opener returns a clean templated greeting (verified live, real engine + real Anthropic), never the LLM fallback, never a 400.feat-create-conversation-opening-turn (fab220d) is correct as-is; with this engine fix, the cold-open opening call gets a real greeting. Coordinate the two pushes (engine + OL) so the surface and its greeting land together.DUNIN7 — Done In Seven LLC — Miami, Florida Change Request — Create-entry cold-open greeting (engine) — v0.1 — 2026-06-28 The create_engagement_entry greeting 400s on an empty opener (generate_response always appends a user turn; the Anthropic API rejects empty content — a system-only call is impossible). Fix: a templated greeting server-composed via the proven orient bypass pattern (no LLM call → no empty user turn → no fallback), plus skip recording the empty operator turn (the data-integrity half — empty turns poisoned subsequent converse). Option T over Option S (LLM-with-synthetic-framing) because the cold open has no context for the LLM's warmth — a fixed invitation is more correct, and the LLM joins on the first real answer. Acceptance is a live entry→active→commit sequence (the door-1 lesson: exists is not works). Unblocks the door-1 frontend branch; the two ship together. Base engine 5ccc3d7.