14 of 26 done · Phase 2 cut · 9 files · 2026-09-06

Mira Conversation Organization

Four months of Mira live in one 196-turn scroll with three segments over it, 190 of them structure-free. Give segments total coverage, backfill the history, index it — and close the one-way guard that let two convening paths race.

14Tasks done
2Phases landed
6Yours to test
1Phase cut
The gist

Mira keeps one unbroken conversation per project. CodeRaven's is now 196 turns across four months, and exactly three segments sit over it — two of them created today. The rendering machinery for structure is already built and good; it has almost nothing to work on, because a segment opens only when a deck card is clicked and typing into the composer opens nothing.

This plan gives segments total coverage, backfills the existing history in one pass, and puts an index over the result. It keeps one thread and one CLI session throughout. It also closes a concurrency defect found while diagnosing the original complaint: two convening paths can run at once on the same segment.

The problem

Segments were built to structure the stream without splitting it, and the rendering half works. MiraSegmentDivider marks where a segment began, MiraCollapsedSegmentRow stands in for a finished one, and seedCollapseIfNeeded folds closed segments on arrival so history does not push the live conversation off screen.

The coverage half does not. A segment opens only from MiraRoomView.start when a card is clicked; MiraService.sendMessage from the composer never opens one. The on-disk segment file for this project holds three records:

OpenedKindTitleState
2026-09-01 16:49ZbrainstormThink out loud with meclosed
2026-09-06 07:21ZmeetingRunning without meclosed
2026-09-06 13:52ZmeetingRunning without meclosed

Roughly 190 of 196 turns belong to no segment at all. The ordinary way of talking to Mira produces structure-free transcript, and the structure that exists covers the minority path. There is no way to answer “where did we talk about that” short of scrolling four months.

The defect found on the way in

Diagnosing the original complaint surfaced a concurrency bug that belongs to this plan. The third segment above was opened by “Run it without me” at 13:52:32Z and closed by convene(_:) 65 seconds later — its recap reads Handed off to a meeting: …, a string only convene writes. In between, at 13:53:07Z, the delegated runner started a real meeting.

Two convening paths ran concurrently on one segment. The runner started the meeting; convene then staged a second roster, navigated to the Meetings tab, prefilled MeetingRoomDraft with the same topic, and closed the segment out from under the runner — which closes it again when run() returns. No duplicate meeting record exists only because the setup sheet was never confirmed. MiraDelegatedMeetingRunner guards one direction; convene(_:) guards nothing.

Approach

Keep one thread and one CLI session

Coverage and retrieval are the stated problems. Neither requires splitting the transcript, and the single session is what carries four months of project context into every turn — it is also why proposeMeeting works by resuming the thread and reading what Mira just said.

Make coverage total before building anything to navigate it

An index over three segments is an index over nothing. Opening an implicit .ongoing segment on composer turns, closed on a silence gap, is what turns the existing rendering machinery into something worth using.

Backfill in one explicit pass, not on launch

Splitting 196 turns on time gaps and titling each run costs a model call and rewrites months of visible history. That belongs behind a button someone pressed, with an isBackfilled flag so a bad pass can be found and re-run.

Rejected — real per-segment threads

Keying MiraThreadStore by (projectId, threadId) with a session each is the shape the segment model was written to avoid; its header comment says so in as many words. It destroys the cross-topic continuity Mira exists for, forces re-scoping on every consumer of thread(for:) — the failure class project_ideation_singleton_session_scoping already records — and breaks proposeMeeting for free.

Deferred — one thread, per-segment CLI sessions

Keep the single visible transcript, give each segment its own resumed session seeded with a recap. This gets the model-focus benefit with none of the history loss. It is deferred because the complaint on the table is navigability, not focus, and this plan does not block it.

Decisions made for you

Judgment call

Ninety-minute silence gap as the topic boundary

A guess, and it will merge two conversations held back to back and split one held across a lunch. The titles carry the meaning; the boundaries are approximate. Reversible — it is one configurable value read at close time, not baked into stored records.

Judgment call

The convening race is Phase 0, not a follow-up

It is a live defect with a reproduction on disk from this morning, and it sits in the same code the rest of the plan touches. Fixing it first means the later phases are not built over a known race.

Judgment call

The segment index replaces the “Set aside” sheet rather than joining it

Parked topics and segments are already two stores showing the same migrated records, and the header button and the deck list disagree about what exists. Adding a third surface would make that worse. Parked becomes a filter within the index.

The work

Phase 0 — One convening path

5 tasks Lands independently; a live defect with a reproduction on disk.
  • Guard convene(_:) against a live meeting: return early with a room error when StandupMeetingService.activeMeeting is unconcluded, matching the guard MiraDelegatedMeetingRunner.run already applies
  • Guard convene(_:) against a delegated run in flight (MiraDelegatedMeetingRunner.shared.isRunning), so the strip button cannot stage a second roster while the runner owns the segment
  • Hide or disable the strip's “Convene this” button while a delegated run owns that segment, so the guard is visible rather than a click that reports an error
  • Make segmentStore.close a no-op on an already-closed segment, so the delegated runner's trailing close cannot overwrite a recap written by convene
  • Add a MeetingRoomDraft prefill guard in ExpertsView.consumeMiraHandoff so a stale handoff cannot prefill a room for a meeting that is already running

Phase 1 — Total segment coverage

5 tasks Everything after this depends on it.
  • Open an implicit .ongoing segment in MiraService.sendMessage when a composer turn arrives for a project with no open segment
  • Close an implicit segment after a configurable silence gap (default 90 minutes), evaluated on the next turn rather than on a timer
  • Leave framing turns (isFraming: true) out of implicit-segment creation, since card openings already declare their own segment
  • Have Mira title an implicit segment on close, reusing the parked-topic recap prompt shape rather than adding a new one
  • Verify an implicit segment never steals the open slot from a card-declared segment (MiraSegmentStore.open closes whatever is open)

Phase 2 — Backfill the existing thread cut

5 tasks Cut on 2026-09-06. The existing four months are not worth an irreversible model-driven rewrite, and the conversation that matters is the live one. Coverage from Phase 1 is forward-only, so history stays as it is and everything after reads as structured.
  • Write MiraSegmentBackfill — splits an existing thread into runs on a time gap, one pass per project, idempotent via a stored marker
  • Title each backfilled run with a single batched Mira call rather than one call per run, capped so a 196-turn thread is one invocation
  • Write backfilled segments as .closed with migratedFromTopicId left nil and a new isBackfilled flag, so a bad pass can be identified and re-run
  • Run the backfill behind an explicit action in the Room rather than on launch, since it costs a model call and rewrites months of history
  • Confirm MiraConversationView.seedCollapseIfNeeded folds backfilled segments on arrival and the transcript still scrolls to the live end

Phase 3 — Retrieval

5 tasks The payoff phase; useless before Phases 1 and 2.
  • Add a segment index listing every segment for the project — title, kind tint, date, artifact chip, turn count
  • Scroll the transcript to a segment's startTurnId on selection, reusing MiraScrollRequest
  • Filter the index by kind and by free text over title and recap
  • Replace the header's “Set aside” sheet with the index, with parked segments as a filter within it rather than a separate surface
  • Retire MiraParkedTopicStore reads from the Room, leaving MiraSegmentMigration as the only consumerpartial

Phase 4 — Manual testing

6 tasks Yours. None of this is verifiable from a build.
  • Type into the composer with no card clicked and confirm a segment divider appears and is titled once the conversation goes quietyou
  • Run the backfill on CodeRaven's 196-turn thread and confirm the transcript arrives folded into legible titled segments rather than one scrollyou
  • Confirm the backfill is idempotent — run it twice and confirm the second pass creates nothingyou
  • Open the segment index, pick a segment from May, and confirm the transcript scrolls to ityou
  • Start “Run it without me”, then press the strip's Convene button, and confirm the second path is refused rather than staging a duplicate rosteryou
  • Confirm parked segments still resume correctly from the index after MiraParkedTopicStore stops being readyou

Risks & edge cases

Accepted

Coverage is forward-only

With Phase 2 cut, the 196 turns already on disk keep their three segments and the index lists those three plus everything from here on. Old history is still a scroll. That was the explicit trade: an irreversible model-driven rewrite of four months was not worth it for history that is not at risk.

Edge case

A silence gap is a guess at a topic boundary

Ninety minutes merges two conversations held back to back and splits one held across a lunch. The UI should describe segments as approximate rather than implying Mira decided where each topic ended.

Edge case

Phase 0 removes an affordance that currently works

Disabling “Convene this” during a delegated run takes away the button in exactly the case where the runner has wedged. The recourse is MiraDelegatedMeetingRunner.cancel(), added this session, and it is reachable only from the Workbench row.

Risk

Retiring the parked-topic reads is a one-way door for the Room

The store keeps its data and MiraSegmentMigration keeps running, so records survive. Any surface still reading topics directly needs auditing before Phase 3's last task lands.

Thin spot

Implicit segments could produce noise instead of structure

If most composer exchanges are two turns long, the backfill yields dozens of two-turn segments and the index becomes its own scroll. A minimum-turn threshold before a run earns a segment is the obvious adjustment, and it is not specified here because the right number comes from seeing the first backfill.

Out of scope

Final review

Phases 0, 1 and most of 3 landed and the build is green. What that buys: convening is single-path and both buttons say why when it is unavailable; a composer turn now opens a segment, so typing produces structure instead of undifferentiated transcript; and the header opens a searchable index of every conversation rather than a sheet listing only what was set aside.

Cutting Phase 2 removed the part I was least sure of, and it cost less than it looks. Coverage is forward-only, so the four months already on disk stay as three segments in the index while everything from here reads as structured.

Where it is still weak. The silence gap is a guess and 90 minutes is a number I picked, not one I measured — it is a UserDefaults key precisely because the right value comes from living with it. Titling costs a model call per closed segment, fired in the background; a day of short exchanges means a run of small titled segments, and if the index turns noisy the fix is a minimum-turn threshold before a stretch earns a name. And none of this is runtime-verified: the six checks in Phase 4 are the real test, and a build proves only that it compiles.

One loose end I did not tie. MiraParkedTopicsView is now unreferenced, and MiraParkedTopicStore is still written by the park-a-turn-range flow and read for captured turn ids. Finishing that retirement means moving parking onto segments, which is its own change rather than a tidy-up at the end of this one.

85/100

Up from 74 with Phase 2 cut, which removed the irreversible step and the part whose output quality could not be predicted. What remains is mechanical and it compiles. Held below 90 by two things: the 90-minute gap is an unmeasured guess, and nothing here has been exercised against a running app. The Phase 4 checks are what would move it.