feat(agents): persist per-session plan state as a typed projection#528
Open
david-engelmann wants to merge 1 commit into
Open
feat(agents): persist per-session plan state as a typed projection#528david-engelmann wants to merge 1 commit into
david-engelmann wants to merge 1 commit into
Conversation
|
@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. |
f33407b to
95cb7c9
Compare
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).
95cb7c9 to
e77d229
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
turn/plan/updatedevents and ClaudeExitPlanModetool calls get normalised into the same typedPlanshape and upserted into a newsession_plan_statetable.getSessionPlanStateTauri command +UiMutationEvent::SessionPlanChangedinvalidation 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_statetable with idempotent migration + snapshot test.agents::session_planmodule — typedPlan,PlanItem,PlanItemStatus,PlanStatus,PlanSource,SessionPlanStateshapes, plus pure parsers for both providers.turn/plan/updatedin the default arm.ExitPlanModenext to the existingpersist_exit_plan_messagecall.UiMutationEvent::SessionPlanChanged { session_id }with frontend type mirror, bridge handler, query key +sessionPlanStateQueryOptions.get_session_plan_state(session_id)Tauri command +getSessionPlanStateTS wrapper.Deferred (intentionally out of scope; subsequent PRs in track C):
Activefor now; new variants land when there's a clear provider signal to drive them).Design notes
codex-{idx}so the pinned UI has a key and replays land in lockstep when a subsequentturn/plan/updatedarrives.rawTextfallback so the panel can render prose-only or nested plans even though the structured list is shallow.load_session_plan_statelogs and returnsNoneon 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 warningsclean.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_itemagents::session_plan::tests::codex_plan_falls_back_to_first_pending_when_no_in_progressagents::session_plan::tests::codex_plan_returns_none_for_missing_or_empty_plan_arrayagents::session_plan::tests::codex_plan_treats_unknown_status_as_pendingagents::session_plan::tests::exit_plan_mode_extracts_bullet_items_and_preserves_raw_textagents::session_plan::tests::exit_plan_mode_returns_none_for_empty_or_missing_planagents::session_plan::tests::exit_plan_mode_handles_unbulleted_prose_as_raw_text_onlyagents::session_plan::tests::upsert_and_load_round_trips_typed_shapeagents::session_plan::tests::upsert_replaces_existing_row_in_placeui_sync::events::tests::session_plan_changed_uses_camel_case_type_and_fieldtests/schema_migrations.rs::session_plan_state_migration_creates_table_on_legacy_dbsSnapshot 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:
getSessionPlanStatewithsessionPlanChangedinvalidation.The backend API is intended to be stable enough that either of those can land independently.