For: scoping-notes/loomworks-creation-conversation-memory-pathway-scoping-note-v0_1.md (currently staged in ~/Downloads, not yet in loomworks-record).
Method: Read against the engine codebase (/Users/dunin7/loomworks-engine @ 813f13f) and the live dev database (playground_dev). Never the surface. Findings only — no code changes.
Date: 2026-06-01.
The scoping note's framing holds up against the code: committing an engagement writes an Engagement object and a wasDerivedFrom relationship, and nothing else. No assertion is written anywhere on the creation path. The seed is never re-homed off the commons — the committed engagement holds only a seed_ref pointer to a seed physically resident on ADMINISTRATIVE_ENGAGEMENT_ID (= E0001). Both the zero-assertions finding and the seed-scope bug are confirmed at code and DB level. One conversation→assertion mechanism already exists (the in-engagement remember intent) and is the nearest analog the new pathway should mirror, but it is wired only to existing engagements, never to the creation conversation.
Two confirmation discrepancies surfaced and are flagged: the spec has no §16 (the deferred question lives in §13), and the DB shows 7 orphaned seeds on E0001, not 6.
Three-step flow (src/loomworks/engagement/creation.py, docstring lines 1–18):
draft_seed (creation.py:82) — builds a candidate Seed and append_event(event_kind="seed_drafted"). Critically, engagement_id=ADMINISTRATIVE_ENGAGEMENT_ID (lines 117, 135). The seed's only home pre-instantiation is the commons. No Engagement object exists yet.induct_seed (creation.py:409) — runs the convergence/divergence loop; findings + cycles also written on ADMINISTRATIVE_ENGAGEMENT_ID.commit_engagement / commit_engagement_divergent (creation.py:681 / 725) — both delegate to _create_engagement (creation.py:847).
**Where extraction from the conversation happens.** Two extractor paths, selected by output_path, both inside the commit endpoint POST /me/engagements/from-conversation (src/loomworks/api/routers/me_create_engagement.py):
brief → _execute_brief_path → inline draft_seed + induct_seed (me_create_engagement.py:374).discovery_record → _execute_discovery_record_path → extract_seed_from_discovery (writes a discovery_to_seed_extracted Memory event with FORAY anchor standard), then draft_seed, then induct_seed (me_create_engagement.py:413–465; converse seam at converse.py:243 _call_extract_seed_from_discovery).So the conversation is read once, at commit time, by the seed extractor. The seed-vs-assertion separation the design requires is already physically true: the extractor produces only a seed.
The commit endpoint, step by step (me_create_engagement.py:560–719):
_find_resumable_candidate_engagement, :264/:594) or, on the first commit turn, run an extractor path._load_induction_state (:632).commit_engagement (:677) or commit_engagement_divergent (:669).engagement_created_from_assistance terminal event on the person's personal engagement (:684–709), carrying source_conversation_turn_ids=[t.id for t in creation_turns] and created_engagement_id.
The activation point. Inside _create_engagement (creation.py:847):
UPDATE engagements SET state='active', candidate_seed_id=NULL, title=COALESCE(...) (:875–883).append_event(event_kind="engagement_committed") writing the Engagement object with seed_ref=MemoryRef(id=seed.id, version=seed.version) (:899).add_relationship(vocabulary="wasDerivedFrom") binding engagement→founding seed (:913).
Transaction discipline. The creation functions do not begin or commit — they ride the caller's session transaction (creation.py docstring lines 13–17). append_event flushes after each write. The whole endpoint (Steps 4–7) is one request-scoped transaction; the FastAPI get_db_session dependency commits at request end. The engagement becomes active atomically with the endpoint's transaction commit — the state='active' UPDATE, the engagement_committed event, the wasDerivedFrom relationship, and the personal-engagement audit event all land together or not at all.
What _create_engagement does NOT write: any assertion. Confirmed by reading every write in the function.
Confirmed at code level. The seed is created with engagement_id=ADMINISTRATIVE_ENGAGEMENT_ID in draft_seed (creation.py:117) and append_event writes it to that engagement (:135). At commit, _create_engagement writes the Engagement object with seed_ref=MemoryRef(id=seed.id, …) (creation.py:894) — a pointer. The seed memory object is never re-homed; its engagement_id stays the commons.
ADMINISTRATIVE_ENGAGEMENT_ID = UUID("00000000-0000-0000-0000-000000000001") (src/loomworks/engagement/bootstrap.py:26). DB confirms this UUID is display_identifier = E0001, title "Administrative", active. So "seed lives on the commons E0001" is literally true.
Confirmed at DB level (playground_dev):
a83c3ff5-…, active, divergent) engagement object v1 holds seed_ref.id = 6db3f37c-ed68-4990-9858-af9451326646.6db3f37c is physically homed on E0001 (engagement_id='00000000-…-0001', object_type='seed', v1). E0060 holds only the pointer.
The 6 "orphaned" seeds — actually 7. Counting distinct object_type='seed' lineages on E0001 and testing each against seed_ref from every surviving engagement row:
00000000×2 (bootstrap/admin seeds), 11111111 (commons/personal), 5ec7ce30, 671ce997, 6db3f37c (E0060), 7ee5c384, 88bed7ee.095922a6, 1d144307, 3c9ecc82, 5138664a, 67572f70, b8ca0b76, f485b8bb.⚠️ Discrepancy to resolve before the CR: the walk handoff and scoping-note §5/§2 cite 6 orphaned candidate seeds; the DB shows 7 unreferenced seed lineages. Likely explanation: 6 never-committed candidates (E0049/50/56/57/58/59) + E0061's degenerate committed-then-removed seed = 7, where the handoff counted only the 6 candidates. Worth reconciling so the CR's cleanup target is exact, and so we don't assume the cleanup mechanism (seed-scope fix) catches exactly the set the handoff named.
What would change to scope the seed to the engagement. The seed is born on the commons because at draft_seed time no engagement exists. Options the CR will choose between:
_create_engagement, after the state='active' promotion, re-write/migrate the seed memory object onto engagement_id (a new seed event on the engagement, or a move). Lightest change; the seed_ref then resolves within-engagement. Must preserve the seed's wasRevisionOf/induction lineage that currently lives on E0001.S#### engagement, the seed is drafted there and the home is real from the start.get_founding_seed/get_current_seed (src/loomworks/engagement/seed_lookup.py) and _load_candidate_seed (engagements.py) read by seed_ref/candidate_seed_id, so they follow the pointer regardless — but any code that assumes seeds live on ADMIN (e.g. revise_seed/mark_seed_divergent write to prior_seed.engagement_id, creation.py:298,379) must be audited. The induction loop (_load_induction_state) is hard-coded to ADMINISTRATIVE_ENGAGEMENT_ID (creation.py:652,670) and per-seed-scoped by seed_id — re-homing the seed without touching induction's write location is possible, but the CR must decide whether findings/cycles also move.Orphan cleanup is a consequence, not a separate fix. Once seeds are scoped to engagements and deletion cascades (or the seed moves off the commons), deleting an engagement takes its seed. The 7 existing orphans are pre-fix residue and need a one-time sweep regardless.
On the creation path: no. Grepped add_assertion/commit_assertion/revise_assertion across orchestration/, discovery_to_seed.py, seed_extraction.py, me_create_engagement.py, seed_commit_from_brief.py — zero hits in the creation flow. The creation conversation is read only to extract the seed. The pathway is genuinely unbuilt.
The nearest existing analog (partial, live, but for existing engagements only): the in-engagement remember/contribute intent in src/loomworks/orchestration/router.py:282. It takes classified.extracted_parameters["knowledge_text"] (one fact, classifier-extracted, per turn) and calls add_assertion(... grammar_element="definition") → a held assertion. It hard-returns error="no_project" when engagement_id is None (router.py:266) — i.e. it cannot run on a NULL-scoped creation conversation. It does not auto-commit (held only; commit is a separate ceremony — the inbox save → commit_assertion, orchestration/routers/inbox.py:280). This is the seam the new pathway should generalize: same add_assertion write, but driven by a batch read of the creation conversation rather than one classified turn, and scoped to the freshly-committed engagement.
The assertion-commit API the pathway will write through (src/loomworks/engagement/assertions.py):
| Function | Signature (kw-only) | Provenance / state |
|---|---|---|
| add_assertion (:93) | engagement_id, content, grammar_element∈{definition,constraint}, normative_force∈{required,recommended,optional}="required", actor: ActorRef, db, metadata: dict|None | Creates held v1. provenance=Provenance(wasAttributedTo=actor, wasGeneratedBy=…). metadata carries the pathway provenance — the file pathway uses metadata.source_mode ("text", file modes, source_file_id, extraction_method, …; see contributions.py:242–267). The creation pathway would set e.g. source_mode="creation_conversation" + the source turn id. Both humans and agents MAY add (R-B34). |
| commit_assertion (:139) | engagement_id, assertion_id, actor, db, [_drift_agent, _agent_runner] | held → committed. Human-only — raises AgentCannotCommitError if actor.kind=='agent' (:168). Assigns display_number atomically (UPDATE … RETURNING, :192–204). provenance.wasRevisionOf → the held version; sets committed_at, committed_by. Optional non-blocking drift check. |
| revise_assertion (:275) | engagement_id, assertion_id, new_content, new_normative_force, actor, db | New held version + records a wasRevisionOf Relationship (:340). This is the corrections-preserved mechanism. |
| retract_assertion (:353) | …, rationale (required, non-empty) | New retracted version; rationale mandatory. |
How corrections-preserved is enforced. There is no distinct supersede operation — corrections-preserved is implemented as the revise_assertion → new held version + wasRevisionOf relationship chain, with every prior version reachable via get_assertion_history (:568). This matches spec §03 "Supersession … without retracting it — the trajectory is preserved" and seed v0.9 "corrections preserved, not smoothed" (line 167). For the pathway, a mid-conversation correction ("actually east, not west") must land as a revision of the prior assertion, not a fresh independent assertion and not an overwrite — preserving both the original and the correction. Note the held-then-commit ceremony: add_assertion only holds; something must drive commit_assertion. Per spec §02, retraction before commit is silent — so any held assertion the Operator rejects pre-commit leaves no trail, which is correct for the review step.
This is the load-bearing input to the §16/§13 Decision C.
The draft seed lives on ADMINISTRATIVE_ENGAGEMENT_ID (E0001) as a seed_drafted Memory event (§1), with the candidate engagement row's candidate_seed_id pointing at it (_wire_candidate_seed_id, me_create_engagement.py:249). A candidate engagement row already exists at draft time (created by the draft endpoint; promoted in place at commit) — so there is already a per-creation engagements row in state != 'active' carrying candidate_seed_id.
The creation conversation turns live at engagement_id IS NULL — "Project-less creation turns share one scope (engagement_id IS NULL)" (converse.py:125, :741). They are never re-parented to the new engagement. This is precisely spec §13 "Engagement creation conversation orphaning": "The Discovery dialog that births a new Engagement leaves its conversation at engagement_id IS NULL … unreachable from that Engagement's surface. Either re-parent on commit, or surface the audit event's turn references." The only durable link is engagement_created_from_assistance.source_conversation_turn_ids recorded on the personal engagement (me_create_engagement.py:699).
Implication for Decision C. Today the model is effectively a hybrid: a real candidate engagement row exists, but the conversation lives in NULL-scope and the seed lives on the commons. The two clean resolutions:
S#### engagement): make the creation conversation a first-class engagement — re-parent NULL-scoped turns to it, draft the seed there, then promote/migrate to E####. Coherent with "engagement as universal adapter"; adds a second engagement + a migration step. Spec status: §13 "Seed creation as recursive engagement — Investigation filed."This brief's recommendation feeds the v0.2 note (see §6 below).
| Item | Claim | Verdict |
|---|---|---|
| Engine main | 813f13f | ✅ Confirmed. git rev-parse HEAD = 813f13ffa97a6ec6b660135f1fc023dc15d850a5, branch main. |
| Seed-vs-Memory distinct holdings | spec §02 | ✅ Confirmed. §02 "The Engagement … holds a seed (its frame of reference), a Memory (accumulated knowledge as assertions)"; "The seed … lives as the Engagement's lens for relevance evaluation." Assertion = "atomic unit of Memory," carries provenance "how it entered Memory — typed, spoken, extracted from a document, derived from an image," lifecycle held/committed/retracted/superseded. |
| Creation conversation named as a contribution pathway | spec §03 | ✅ Confirmed, verbatim. §03 "Population — how Memory grows": "Contributions enter Memory through several pathways: typed turns…, voice turns…, file uploads…, image-vision…, audio…, and the engagement-creation conversation itself." |
| The deferred S####-vs-surface question | scoping note cites §16 | ⚠️ Canonical-source correction. Spec v0.4 has no §16 — it ends at §15. The deferred question lives in §13 "Seed creation as recursive engagement" (Status: Investigation filed). The closely-related §13 "Engagement creation conversation orphaning" (Status: Planned) is the orphaning half. The v0.2 note must cite §13, not §16. |
| Current seed version | note scoped against v0.8; memory/handoff cite v0.9 | ✅ v0.9 is current (v0.8 is archived under candidate-seeds/loomworks/archive/). v0.9 does not move the load-bearing commitments: seed-as-frame (line 49 "reads the engagement's seed and all committed assertions"), Memory-as-assertions (lines 21/39/49), corrections-preserved (lines 37, 167 "Corrections preserved, not smoothed … superseded assertions remain in memory as corrections"). Safe to re-cite as v0.9. |
| Highest current-status manifest | — | ✅ v0.48 (current-status/current-status-manifest-v0_48.md, dated 2026-05-31). (Note: spec §13's operational-hygiene cluster still says the manifest is stale at v0.41 — the spec's own statement is itself stale; the manifest has since advanced to v0.48.) |
Decision A — When assertions are written. → Write at commit, from the reviewed conversation.
The conversation is already read exactly once, at commit, by the seed extractor (§1). Riding that same read to also produce held assertions keeps Memory population atomic with the engagement becoming real (Step 6's single transaction), and means the conversation only needs to be readable at commit — not continuously accreting. The held-then-commit ceremony argues for: extract → add_assertion (held, batch) → surface for Operator review → commit_assertion the accepted set. Pre-commit rejection is silent (spec §02), which is the right semantics for the review gate. Avoid the incremental per-turn model — it would require a real home for assertions before the engagement exists (pulling toward the heavier Option 1) for no clear gain, given the conversation is reviewed wholesale anyway.
Decision B — Extraction grain and provenance.
Grain = a stated fact (one add_assertion, grammar_element="definition" by default, mirroring the file pathway's contributions.py:162), not a whole turn. Provenance via metadata: source_mode="creation_conversation" + the source conversation_turn id (the same turn ids already captured in source_conversation_turn_ids). Mid-conversation corrections must use revise_assertion against the prior assertion (preserving both via wasRevisionOf), never a silent overwrite — enforce this in the extractor's design, since the LLM extraction step is where smoothing would creep in (cf. the "bypass-responder-for-structural-contracts" discipline: the corrections-preserved guarantee should be a structural contract, not a prompt suggestion).
Decision C — Seed-creation-conversation scope (§13, not §16). → Recommend Option 2 (dedicated/staging surface), settled jointly with A.
Because A writes at commit from a wholesale-reviewed conversation, the conversation only needs to be readable at commit — it does not need a first-class engagement home to accrue into. The infrastructure already leans this way: a candidate engagements row exists, candidate_seed_id points at the draft seed, and the conversation is NULL-scoped with turn-ids captured on the personal engagement. The minimal, design-consistent move is: at commit, (i) re-home the seed onto the engagement (Decision §2a), (ii) write the creation-conversation assertions onto the engagement, and (iii) resolve the §13 orphaning by re-parenting the NULL-scoped turns to the new engagement (or surfacing them via the audit event's turn refs). Option 1 (S####) remains the more methodologically pure long-term answer but introduces a second engagement + migration for no near-term payoff; recommend recording it as the deferred-direction it already is in §13 and not adopting it for this phase.
Open items the v0.2 note should carry:
creation_conversation provenance, the seed scoped to it (not E0001), and Manifestation → Shaping → Render becomes runnable.
Loomworks Step 0 inspection brief — creation-conversation → Memory-assertion pathway — v0.1 — 2026-06-01. Read against engine 813f13f and playground_dev. No code changes this step.