Skip to content

Session persistence: denied sessions stay alive for resubmission#770

Open
backnotprop wants to merge 11 commits into
feat/session-lifecyclefrom
feat/session-persistence
Open

Session persistence: denied sessions stay alive for resubmission#770
backnotprop wants to merge 11 commits into
feat/session-lifecyclefrom
feat/session-persistence

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

Summary

Denied sessions stay alive instead of dying. When the agent revises and resubmits, the same session updates in place — no new tab, no lost context. Works for all three feedback-capable session types: plan review, code review, and file-based annotate.

Key changes

  • New session status: awaiting-resubmission — non-terminal, session stays routable with HTTP handler alive
  • Decision cycle model: Shared createDecisionCycle<T>() helper replaces one-shot promises in all three servers. Each deny starts a new cycle; approve/exit is final.
  • Session matching: Daemon matches resubmissions to existing sessions by match key (plan:project:slug, review:project:branch, annotate:filepath)
  • Frontend: Amber "waiting for agent to revise" banner, session-revision WebSocket subscription, content refresh with annotation clearing
  • CLI: Accepts awaiting-resubmission as valid non-error status (exit 0 with feedback)

What stays the same

  • Approve, exit, auto-close — all unchanged
  • Standalone/demo sessions — deny is still final
  • URL annotations, annotate-last, archive, goal-setup — not persistable, behave as before

Test plan

  • Plan: deny → awaiting banner → agent resubmits → session updates with diff
  • Annotate file: send feedback → awaiting → agent re-triggers annotate → content refreshes
  • Code review: send feedback → awaiting → agent re-triggers review → diff refreshes
  • Approve path unchanged for all three types
  • Exit path unchanged for all three types
  • Session expires after 10 min if agent doesn't resubmit
  • URL annotate and annotate-last do NOT persist (complete normally)

…ubmission

Protocol: add "awaiting-resubmission" status (non-terminal), add
"session-revision" event family, bump protocol version to 2.

Session store: add suspend() (resolves waiters WITHOUT disposing
resources — session stays alive), reactivate() (transitions back
to active), and matchKey field on records.

Server: skip the 2-second deletion timer on result endpoint for
awaiting-resubmission sessions — they need to stay alive for the
agent to resubmit.
Replace one-shot resolveDecision with a cycle system — each deny
resolves the current cycle and starts a new one. Approve resolves
the final cycle. Agent-originated sessions return awaitingResubmission
in the deny response.

Add updateContent(newPlan) method that updates plan content, saves
to version history, resets draft state, and publishes a
session-revision event via the WebSocket hub.

Add slug and getSnapshot to PlannotatorSession interface so the
factory can match resubmissions and serve correct snapshots.

Add session-revision to SessionEventFamily type.
Session factory: add sessionRefs registry and findAwaitingSession()
matching by matchKey. Plan sessions compute matchKey as
plan:project:slug. On resubmission, existing awaiting session is
matched and reactivated instead of creating a new one.

Add registerPersistentDecision() — loops on waitForDecision(),
suspending on deny and completing on approve. Replaces one-shot
registerSessionDecision for plan sessions.

Fix unhandled promise rejection: add .catch() to disposed promise
in both registerSessionDecision and registerPersistentDecision.

CLI: accept "awaiting-resubmission" as valid non-error status so
denied sessions output feedback and exit 0 instead of failing.
Extend CompletionBanner with 'awaiting' variant — amber spinner with
"Feedback sent — waiting for agent to revise..." message and optional
cancel button.

Plan review deny handler now checks response for awaitingResubmission
flag. When true, shows the awaiting banner instead of the completion
overlay.

Subscribe to session-revision events via daemon WebSocket. When the
agent resubmits and the session reactivates, the event carries the
new plan content. The UI updates: markdown refreshes, previousPlan
updates for diff, all annotations clear, awaiting state resets.
Add session persistence and resubmission section to AGENTS.md
covering the awaiting-resubmission status, matchKey matching, and
session-revision event family.

Update backlog: mark #3 (live plan updates) and #4 (session
persistence after completion) as done.
Fix operator precedence in registerPersistentDecision catch handler —
was evaluating as (A && B) || C instead of A && (B || C).

Extend findAwaitingSession to prune completed/expired/failed/cancelled
entries from sessionRefs map, preventing slow memory growth over
daemon lifetime.
Extract createDecisionScope helper to eliminate duplication between
registerSessionDecision and registerPersistentDecision. Define
SessionDecisionResult type for explicit contracts.

Annotate server: cycle-based decisions, updateContent for file-based
modes, awaitingResubmission in /api/feedback response.

Review server: cycle-based decisions, updateContent(rawPatch, gitRef),
awaitingResubmission for non-approved feedback.

Factory: generalize sessionRefs to PersistableSession interface.
Add matchKey computation and matching for annotate (annotate:filepath)
and review (review:project:branch or review:prUrl). Wire both with
registerPersistentDecision. URL and annotate-last modes keep one-shot
behavior (no persistent source to refresh).
Annotate: handleAnnotateFeedback now checks response for
awaitingResubmission flag, same pattern as plan deny handler.
Session-revision subscription already handles both plan and annotate
since they share App.tsx.

Code review: handleSendFeedback checks awaitingResubmission response.
Add session-revision event subscription that refreshes diff data,
clears annotations, and resets awaiting state. CompletionBanner shows
awaiting variant for all three surfaces.
Move plan server's inline updateContent closure to a named function
handleUpdateContent for readability. Document findAwaitingSession's
side effect of pruning terminal sessions during search.
Add createDecisionCycle<T>() and resolveAndCycle() to session-handler.ts.
All three servers (plan, annotate, review) now use the shared helper
instead of copy-pasting the cycle setup, resolve, and startNewCycle
pattern. Deny handlers collapse to a single resolveAndCycle() call.

Extract updateContent to named handleUpdateContent functions in all
three servers for consistency — inline closures in return blocks
replaced with named functions defined above the return.
…, empty content, TTL

1. Register no-op snapshot provider for session-revision in all three
   servers — prevents WebSocket disconnect when frontend subscribes.

2. Exclude plannotator-frontend from agent origins in resolveAndCycle
   — dashboard-created sessions complete on deny instead of hanging.

3. Complete session on exit: true in registerPersistentDecision —
   Exit button now properly terminates instead of suspending.

4. Check revision.plan !== undefined instead of truthiness — empty
   diffs and empty annotate content no longer ignored.

5. Store ttlMs on session record and restore it on reactivate() —
   reactivated sessions expire normally if abandoned.
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.

1 participant