Skip to content

feat(agents): persist per-session plan state as a typed projection#528

Open
david-engelmann wants to merge 1 commit into
dohooo:mainfrom
david-engelmann:feature/session-plan-state
Open

feat(agents): persist per-session plan state as a typed projection#528
david-engelmann wants to merge 1 commit into
dohooo:mainfrom
david-engelmann:feature/session-plan-state

Conversation

@david-engelmann
Copy link
Copy Markdown
Contributor

Summary

  • Adds a backend projection of the latest agent plan per session, so a future pinned-plan UI can survive reloads without rescanning the chat transcript.
  • Codex turn/plan/updated events and Claude ExitPlanMode tool calls get normalised into the same typed Plan shape and upserted into a new session_plan_state table.
  • New getSessionPlanState Tauri command + UiMutationEvent::SessionPlanChanged invalidation channel. The pipeline accumulator/adapter — and therefore the existing inline plan card / todo list rendering — are untouched; this is a parallel side-table write.

Why

Closes #410. The bug is that Helmor already knows what the active plan is at runtime, but the only place it lives is the chat scrollback. After a reload the frontend has to rescan messages to reconstruct the plan, which is fragile and forces every consumer to re-implement the projection.

Scope boundary

Included:

  • session_plan_state table with idempotent migration + snapshot test.
  • agents::session_plan module — typed Plan, PlanItem, PlanItemStatus, PlanStatus, PlanSource, SessionPlanState shapes, plus pure parsers for both providers.
  • Streaming-bridge hooks at the two existing plan-event sites:
    • Codex turn/plan/updated in the default arm.
    • ExitPlanMode next to the existing persist_exit_plan_message call.
  • UiMutationEvent::SessionPlanChanged { session_id } with frontend type mirror, bridge handler, query key + sessionPlanStateQueryOptions.
  • get_session_plan_state(session_id) Tauri command + getSessionPlanState TS wrapper.
  • Tests:
    • 9 projection unit tests (Codex status normalisation, current-item resolution, ExitPlanMode bullet extraction with nested-line guard, raw-text fallback, empty/missing inputs, upsert round-trip, in-place replacement).
    • Schema migration snapshot for legacy DBs.
    • UI sync serialization test for the new variant + camelCase regression gate extended.

Deferred (intentionally out of scope; subsequent PRs in track C):

  • Pinned-plan UI (Track C / PR C2).
  • Continue / revise prompt builders + buttons (Track C / PR C3).
  • Per-provider plan-completion / cancellation signals (the enum stays Active for now; new variants land when there's a clear provider signal to drive them).

Design notes

  • The pipeline accumulator emits the inline plan card / todo-list exactly as before — projection is a separate observation. That keeps the existing pipeline snapshot suites stable and means the storage-shape "MUST have snapshot test coverage" rule from AGENTS.md applies only to the new table, not to the message rows.
  • Codex events lack stable per-step ids; the projection derives codex-{idx} so the pinned UI has a key and replays land in lockstep when a subsequent turn/plan/updated arrives.
  • ExitPlanMode is free-text markdown. The parser pulls top-level bullets into structured items (rejecting indented sub-items by checking for leading whitespace before the bullet) and keeps the raw markdown as a rawText fallback so the panel can render prose-only or nested plans even though the structured list is shallow.
  • load_session_plan_state logs and returns None on JSON validation failure rather than bubbling — a stale row from a hypothetical breaking shape change won't crash the panel.

Test plan

  • cd src-tauri && cargo nextest run — 1144 / 1144 pass (9 new projection tests + 1 new migration test + 1 new ui_sync test).
  • cd src-tauri && cargo test --doc — clean.
  • cd src-tauri && cargo fmt --all -- --check — clean.
  • bun run lint — biome + clippy -D warnings clean.
  • bun run typecheck — clean.
  • bun run test:frontend — 1092 / 1092 pass.

Coverage added

New tests:

  • agents::session_plan::tests::codex_plan_maps_statuses_and_picks_current_item
  • agents::session_plan::tests::codex_plan_falls_back_to_first_pending_when_no_in_progress
  • agents::session_plan::tests::codex_plan_returns_none_for_missing_or_empty_plan_array
  • agents::session_plan::tests::codex_plan_treats_unknown_status_as_pending
  • agents::session_plan::tests::exit_plan_mode_extracts_bullet_items_and_preserves_raw_text
  • agents::session_plan::tests::exit_plan_mode_returns_none_for_empty_or_missing_plan
  • agents::session_plan::tests::exit_plan_mode_handles_unbulleted_prose_as_raw_text_only
  • agents::session_plan::tests::upsert_and_load_round_trips_typed_shape
  • agents::session_plan::tests::upsert_replaces_existing_row_in_place
  • ui_sync::events::tests::session_plan_changed_uses_camel_case_type_and_field
  • tests/schema_migrations.rs::session_plan_state_migration_creates_table_on_legacy_dbs

Snapshot updates:

  • tests/snapshots/schema_migrations__session_plan_state_migration.snap (new).

Follow-up

This is PR C1 in the durable-plan-state track. The natural follow-up slices:

  • C2: pinned-plan UI rendering getSessionPlanState with sessionPlanChanged invalidation.
  • C3: continue / revise prompt builders + composer actions.

The backend API is intended to be stable enough that either of those can land independently.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

@david-engelmann is attempting to deploy a commit to the Caspian's Team Team on Vercel.

A member of the Team first needs to authorize it.

@david-engelmann david-engelmann force-pushed the feature/session-plan-state branch 2 times, most recently from f33407b to 95cb7c9 Compare May 14, 2026 19:10
Provider plan/todo events (Codex turn/plan/updated, Claude ExitPlanMode)
get normalised into a typed Plan and upserted into a new
session_plan_state table so a pinned-plan UI can render the latest plan
without rescanning scrollback after a reload.

The pipeline accumulator + adapter are untouched: the inline plan card
and todo-list rendering keep the same wire shape. The projection is a
parallel side-table write triggered at the same persistence points
where exit-plan messages and Codex stream events already fire.

Adds:
- session_plan_state table + idempotent migration.
- agents::session_plan module with PlanSource / PlanItemStatus /
  PlanStatus enums and a typed Plan/SessionPlanState shape.
- plan_from_codex_event + plan_from_exit_plan_mode parsers that
  normalise statuses, derive stable per-position item ids, and keep
  raw text for the unstructured ExitPlanMode case.
- upsert_session_plan{,_via_pool} + load_session_plan_state.
- UiMutationEvent::SessionPlanChanged with frontend mirror + bridge
  handler + sessionPlanStateQueryOptions for cache invalidation.
- get_session_plan_state Tauri command + getSessionPlanState TS
  wrapper.

Tests:
- 9 projection unit tests covering Codex status mapping, currentItemId
  resolution, ExitPlanMode bullet extraction, unbulleted-prose fallback,
  empty/missing input, upsert round-trip, and in-place replacement.
- Schema migration snapshot for legacy DBs.
- UI sync serialization test for the new variant + extension of the
  camelCase regression gate.

Closes dohooo#410 (backend projection slice; pinned-plan UI + continue/revise
actions are follow-up PRs in track C).
@david-engelmann david-engelmann force-pushed the feature/session-plan-state branch from 95cb7c9 to e77d229 Compare May 14, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Pin the active agent plan at the bottom of the chat

1 participant