Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
da7dfa9
Add session persistence foundation: suspend, reactivate, awaiting-res…
backnotprop May 22, 2026
167ee5f
Add cycle-based decision model and updateContent to plan server
backnotprop May 22, 2026
e5afae8
Add session matching, persistent decision loop, and CLI support
backnotprop May 22, 2026
d1e80b9
Add awaiting-resubmission UI state to plan review
backnotprop May 22, 2026
27bf0ff
Document session persistence: AGENTS.md and backlog updates
backnotprop May 22, 2026
f5af86a
Self-review fixes: operator precedence bug and sessionRefs cleanup
backnotprop May 22, 2026
83344d1
Wire all three session types for persistence
backnotprop May 22, 2026
7feff89
Add awaiting-resubmission frontend state for annotate and code review
backnotprop May 22, 2026
db80f95
Quality fixes: extract updateContent, document findAwaitingSession
backnotprop May 22, 2026
b4ffc95
Extract shared decision cycle helper, consistent updateContent naming
backnotprop May 22, 2026
d46c92e
Fix 5 review findings: snapshot provider, origin check, exit handling…
backnotprop May 22, 2026
b2477d1
Add session persistence design docs and decisions
backnotprop May 24, 2026
17c77e1
Remove redundant HTML overview from tracking
backnotprop May 24, 2026
1c1f720
Add version history and diff support to annotate sessions
backnotprop May 25, 2026
9d1f921
Long-lived code review sessions with idle status
backnotprop May 25, 2026
678136e
Hide submit buttons while idle, no auto-dismiss
backnotprop May 26, 2026
f26fa65
Update decisions.md with implemented code review lifecycle
backnotprop May 26, 2026
7148471
Fix 4 review findings: idle TTL, late waiter, self-refresh, temp cleanup
backnotprop May 26, 2026
caa4865
Fix PR mode review reuse: pass fetched patch instead of local diff
backnotprop May 26, 2026
1a56bd0
Fix plan diff base tracking and linked-doc annotation leak
backnotprop May 26, 2026
893172f
Exclude folder annotate from persistence, fix review loop survival
backnotprop May 26, 2026
7204c54
Scope annotate matchKey by project to prevent cross-project collisions
backnotprop May 26, 2026
7b499f7
Update decisions.md with cross-cutting facts and current open items
backnotprop May 26, 2026
23dd2fa
Fix 5 cross-cutting issues: external annotations, waitForResult, plan…
backnotprop May 26, 2026
0e9fd2e
Collapse duplicate decision loops into shared registerDecisionLoop
backnotprop May 26, 2026
61dbe33
Fix 3 review findings: protocol test, legacy overlay, smart viewed-fi…
backnotprop May 26, 2026
fe9cde1
Fix CompletionOverlay feedback-sent visual to match CompletionBanner
backnotprop May 26, 2026
206104f
Code quality: exit cycle, util location, docs, decision cycle comment
backnotprop May 26, 2026
1616421
Sessions never die: remove TTL, persistent all annotate types, fix 8 …
backnotprop May 26, 2026
0b5ab63
Self-review: pass rawHtml through annotate reuse path, fix interface …
backnotprop May 26, 2026
fae1d20
Clean up decisions.md: mark fixed items, update stale decisions, add …
backnotprop May 26, 2026
8f4afcc
Fix zombie listener, folder reuse, and button state on refresh
backnotprop May 26, 2026
d3dadb1
Fix snapshot state wipe, editor annotations, remote share, HTML draft…
backnotprop May 26, 2026
dc63a1a
Fix session-revision handler: distinguish snapshots from live events
backnotprop May 26, 2026
8ba9576
Fix dead import, HTML→markdown switching, standalone awaiting overlay
backnotprop May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,23 @@ The daemon is the single long-running Bun server used by normal plan/review/anno
| `/daemon/projects/prs` | GET | List open PRs for a project (`?cwd=`) |
| `/daemon/projects/prs/detailed` | GET | List PRs with review metadata for dashboard (`?cwd=`) |
| `/daemon/fs/list` | GET | List directory contents (`?path=`) |
| `/daemon/ws` | WebSocket | Multiplex daemon lifecycle events, session-scoped external annotation events, agent job events, and correlated session actions |
| `/daemon/ws` | WebSocket | Multiplex daemon lifecycle events, session-scoped external annotation events, agent job events, session revision events, and correlated session actions |
| `/s/:id` | GET | Serve the browser HTML for a session |
| `/s/:id/api/...` | Any | Route browser API requests to that session's plan/review/annotate handler |

Runtime live updates for daemon lifecycle events, external annotations, and agent jobs are delivered through `/daemon/ws`. Session-scoped updates subscribe by `{ family, sessionId }`. HTTP endpoints below remain for snapshots, mutations, uploads, and large payloads. AI query token streaming remains on `/api/ai/query`.
Runtime live updates for daemon lifecycle events, external annotations, agent jobs, and session revisions are delivered through `/daemon/ws`. Session-scoped updates subscribe by `{ family, sessionId }`. HTTP endpoints below remain for snapshots, mutations, uploads, and large payloads. AI query token streaming remains on `/api/ai/query`.

### Session Persistence and Resubmission

When a user denies a plan (or sends feedback on a review/annotation), the session enters `awaiting-resubmission` status instead of completing. The session's HTTP handler stays alive. When the agent replans and submits again via `POST /daemon/sessions`, the daemon matches the new submission to the existing session by a match key (`plan:project:slug` for plans, `review:project:branch` for reviews, `annotate:project:filePath` for single-file annotations). The session reactivates in place — the frontend receives a `session-revision` event via WebSocket with the updated content.

**Sessions never die.** No session type calls `store.complete()` from its decision handler. All sessions survive feedback, approve, and exit — the HTTP handler stays alive and the tab keeps working. `registerPersistentDecision` always calls `store.suspend()`. `registerReviewDecision` always calls `store.idle()`. Non-terminal sessions have no expiry timer.

**Session statuses (plan/annotate):** `active` → `awaiting-resubmission` (on any decision) → `active` (on resubmit) → `awaiting-resubmission` ... repeating.

**Session statuses (code review):** `active` → `idle` (on any decision) → `active` (on agent resubmit) → `idle` ... repeating.

**Event families:** `daemon`, `external-annotations`, `agent-jobs`, `session-revision`.

### Plan Server (`packages/server/index.ts`)

Expand Down Expand Up @@ -320,7 +332,9 @@ Runtime live updates for daemon lifecycle events, external annotations, and agen

| Endpoint | Method | Purpose |
| --------------------- | ------ | ------------------------------------------ |
| `/api/plan` | GET | Returns `{ plan, origin, mode: "annotate", filePath, sourceInfo?, gate, renderAs?, rawHtml? }` |
| `/api/plan` | GET | Returns `{ plan, origin, mode: "annotate", filePath, sourceInfo?, gate, renderAs?, rawHtml?, previousPlan, versionInfo }` |
| `/api/plan/version` | GET | Fetch specific version (`?v=N`) — single-file annotate only |
| `/api/plan/versions` | GET | List all versions — single-file annotate only |
| `/api/feedback` | POST | Submit annotations (body: feedback, annotations) |
| `/api/approve` | POST | Approve without feedback (review-gate UX, `--gate`) |
| `/api/exit` | POST | Close session without feedback |
Expand Down
2 changes: 1 addition & 1 deletion apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ async function runDaemonSessionRequest(request: PluginRequest, options: { plugin
await cancelCreatedSession();
fail(completed.error.code, completed.error.message);
}
if (completed.session.status !== "completed") {
if (completed.session.status !== "completed" && completed.session.status !== "awaiting-resubmission" && completed.session.status !== "idle") {
fail(
completed.session.status,
completed.session.error ?? `Plannotator session ${completed.session.id} ended with status ${completed.session.status}.`,
Expand Down
27 changes: 4 additions & 23 deletions goals/frontend-session-lifecycle/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,15 @@ Some users will prefer each session opening in a new browser tab with auto-close

---

## 3. Live plan updates across deny/replan cycles
## ~~3. Live plan updates across deny/replan cycles~~ DONE

**Priority:** High — most-requested feature
**Size:** Large

When the agent resubmits a plan after denial, the existing session should reactivate in-place rather than spawning a new session.

**Desired behavior:**
- User denies plan, sends feedback, agent replans
- Agent calls ExitPlanMode again — daemon matches it to the existing session
- Session status flips from "completed" back to "active"
- Frontend receives a push notification via WebSocket
- Plan diff system shows what changed between versions
- The plan→deny→replan→approve cycle happens in one persistent session

**Open questions:**
- How does the daemon match a new plan submission to an existing session? By project + plan slug? By a correlation ID from the agent?
- Does the session status reset to "active" on resubmission, or show "updated"?
- How does this interact with the version history system already in `~/.plannotator/history/`?
Implemented in `feat/session-persistence`. Sessions enter `awaiting-resubmission` status on deny. Agent resubmission is matched by `plan:project:slug` and the session reactivates in place. Frontend receives `session-revision` WebSocket event with updated content.

---

## 4. Session persistence after completion

**Priority:** High — current behavior is broken
**Size:** Medium, tied to #3
## ~~4. Session persistence after completion~~ DONE

**Current bug:** When a session is approved/denied, the daemon disposes the session handler. The session disappears from the sidebar even though the route still resolves. API calls fail, so the plan content is gone.
Implemented in `feat/session-persistence`. Denied sessions stay alive (handler not disposed) in `awaiting-resubmission` state with no expiry. Sessions persist until daemon restart.

**Required behavior:**
- Completed sessions stay in the sidebar with a status badge (approved/denied)
Expand Down
147 changes: 147 additions & 0 deletions goals/session-persistence/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Session Persistence — Design Decisions

Tracking decisions made during PR #770 review and triage (2026-05-23).

---

## Product Facts

### Annotate Mode

- A user can annotate a document.
- A user can annotate a URL.
- A user can annotate a folder.
- A user can annotate any of the above asynchronously across multiple docs/agents.
- A user can submit/flush annotations through to the agent.
- An agent can, but may not, create new versions of the document.
- If an agent does, a user should be notified.
- Agent revisions may change the state of a document. If it does, a user is notified.
- The user should be able to see those new document versions. Diff mode should allow them to see previous.
- Annotation mode has a gating process, by default it is not used. If it is used, we should assume the agent will iterate with the user until an approval.
- The gating process is similar to the planning flow.
- If an agent gates a document the user already has open, the gate buttons would appear.
- Otherwise, the normal button set appears.

### Code Review Mode

- A code review session can be associated with a project (local dir) or GitHub PR or GitLab MR.
- In local mode, there is the possibility of the diffs changing under the session.
- When I review code, I can make annotations.
- I want to send/flush the annotations to the agent, or I can publish to GitHub/GitLab comments (if in PR mode).
- Code review no longer needs to end.
- If an agent session initiates a new code review session from same directory, ideally it would open in the existing session.
- But I would need to be notified of this.
- I would need to be notified if diffs change.
- In legacy tab mode, code review should show the full-screen completion overlay (countdown + close tab) after sending feedback, same as plan review. The inline banner is for embedded mode only.
- When a new diff arrives, files I've already viewed should stay hidden — unless the file actually changed in the new diff. Only show it again if the content is different.

### Cross-Cutting

- Every annotate session lives forever once created — single file, folder, last message, URL. No one-shot sessions. The tab stays open and interactive after feedback is sent. There are no exceptions.
- Folder annotate sessions are reusable. If the user annotates the same folder twice, the daemon should find the existing session and reactivate it — not create a new one. The match key is the folder path. There's no content to update (it's a file browser), but the session is reused as-is.
- Annotate-last is not reusable — "last message" has no stable identity across invocations. Each annotate-last creates a new session. This is fine; the command is rarely run twice in a row.
- Legacy tab mode is the only case where the tab closes after feedback — that's the full-screen overlay with countdown, and it's the expected legacy behavior.
- Sessions do not time out. A session, once created, lives until the daemon restarts. We do not kill sessions on a countdown. If a user submits feedback and the agent never comes back, that's for the user to see and decide — not for us to silently clean up.
- We should collect the right data (timestamps, feedback-sent-at, last-agent-contact) so we can eventually show the user: "you submitted feedback but it never came back." But that's a future UI concern, not a reason to expire sessions.
- When a revision arrives (plan, annotate, or review), any external annotations (lint results, agent comments) from the previous version must be cleared. They reference old content with wrong positions.
- `waitForResult` must return immediately if the result is already available — for both `idle` and `awaiting-resubmission` sessions. No consistency gaps.
- Plan/annotate actions (Approve, Deny, Send Feedback) must be disabled while awaiting resubmission. The agent already has the feedback — submitting again against stale content is wrong. Code review already handles this (buttons hidden when idle).
- Late WebSocket subscribers (tab refresh during awaiting) should receive the current state. The snapshot provider for `session-revision` must return the latest content, not null.
- HTML and markdown annotation should go through the same functional pipeline. The `--render-html` path diverges from markdown in a way that `updateContent` can't reach — `updateContent` must also update `rawHtml` for HTML sessions.
- PR review sessions that get reactivated need updated PR metadata (head SHA, etc.). The current implementation serves the correct diff but posts platform actions against the stale commit. Needs a bigger fix to make PR metadata updatable inside the session closure.
- Annotate history slug is computed once from the initial heading and doesn't update if the heading changes. Acceptable — versions stay intact, just filed under the old name on disk.
- The decision listener must stay alive after every user action — approve, deny, exit, send feedback. If the listener shuts down after approve/exit, the session looks alive but can't respond to future resubmissions. The agent hangs forever.
- Session collisions across worktrees of the same repo are not a real concern. This is a local app — one daemon per machine.

---

## Decisions

### Decision 1: Code review sessions are long-lived

**Status:** Implemented

Code review sessions use a new `"idle"` daemon status. The flow:

```
agent → plannotator review (CLI opens, blocks) → session active
session → user annotates → sends feedback → submit (CLI closes)
session → idle (user can browse and annotate, but no submit buttons — nobody is listening)
agent → plannotator review again (CLI opens) → reactivates the idle session
(repeats indefinitely)
```

Key behaviors:
- After feedback: session transitions to `idle` via `store.idle()`. The HTTP handler stays alive, resources stay alive. The user can browse the diff and make annotations, but Send Feedback / Approve buttons are hidden (no agent to receive them).
- On reactivation: agent triggers `plannotator review` from the same directory/branch. The daemon finds the idle session by matchKey, pushes the new diff via `updateContent`, and calls `store.reactivate()`. The frontend receives a `session-revision` WebSocket event, updates the diff, and re-shows the submit buttons.
- Infinite cycle: this repeats as many times as needed. No counter, no limit.
- Cleanup: idle sessions have no expiry. They live until daemon restart.

**Resolved questions:**
- Notification when diffs change: agent-triggered via `session-revision` event. No file watcher (user can manually switch diff type to refresh).
- Subsequent feedback without agent: not possible — submit buttons are hidden while idle.
- Cleanup: sessions persist until daemon restart (no TTL on non-terminal sessions).

### Decision 2: All annotate sessions are persistent

**Status:** Implemented

Every annotate session lives forever — single file, folder, URL, last message. No one-shot sessions. All annotate types use `registerPersistentDecision`, which never calls `store.complete()`. The session always suspends and the loop continues.

Single-file annotate is revisable: it has a matchKey, updateContent, and version history. The frontend shows "Waiting for agent to revise..." after feedback.

Folder, annotate-last, and URL annotate are non-revisable: no matchKey, no updateContent. The frontend shows "Feedback sent" after feedback. The session stays interactive — the user can keep browsing and send more feedback.

### Decision 3: "Feedback sent" state should be calm, not loading

**Status:** Implemented (code review), pending (plan/annotate)

**Code review:** After sending feedback, the `CompletionBanner` shows a green checkmark with "Feedback sent / Your annotations were delivered to the agent." The banner persists until the agent reactivates (no auto-dismiss). Submit buttons disappear. The session stays browsable.

**Plan/annotate:** Still uses the amber spinner "Waiting for agent to revise..." variant. This should eventually be made calmer, but it's lower priority because plan/annotate persistence works correctly (agent WILL resubmit).

**What this means for the current code:**
- Code review uses `'feedback-sent'` CompletionBanner variant (green checkmark, not spinner)
- Plan/annotate still uses `'awaiting'` variant (amber spinner) — acceptable for now
- For plan/annotate: actions should be disabled until the revision arrives (the agent already has the feedback and is working — re-submitting before the revision arrives doesn't make sense)
- For code review: different model, TBD based on Decision 1

### Decision 4: Hot loop prevention for non-agent origins

**Status:** Resolved

The `registerDecisionLoop` spin guard uses promise identity (`currentPromise === lastPromise`) to detect when no new cycle was started. When a non-agent origin calls `resolveAndCycle`, it resolves without calling `startNew()`, so the loop sees the same promise and exits cleanly. No hot loop.

### Decision 5: Clear external annotations on revision

**Status:** Implemented

All three `handleUpdateContent` functions (plan, annotate, review) call `externalAnnotations.clearAll()` before publishing the `session-revision` event.

### Decision 6: Session expiry

**Status:** Resolved — sessions don't expire

Non-terminal sessions (`awaiting-resubmission`, `idle`, `active` after first decision) have `expiresAt` deleted. `cleanupExpired()` skips them. Sessions live until daemon restart or explicit cancellation.

---

## Open Items

| Item | Severity | Status |
|------|----------|--------|
| External annotations not cleared on revision (all surfaces) | P2 | Fixed |
| Plan/annotate actions not disabled during awaiting | P2 | Fixed |
| `waitForResult` missing `awaiting-resubmission` short-circuit | P2 | Fixed |
| `session-revision` snapshot provider returns null | P2 | Fixed |
| `--render-html` resubmission shows stale HTML | P2 | Fixed — `handleUpdateContent` now accepts and updates `rawHtml` |
| PR reviews keep stale metadata on reuse | P1 | Deferred — needs PR metadata updatable in session closure |
| Gate flag not updated on resubmission | P2 | Deferred — if session was created ungated and agent resubmits with `--gate`, Approve button won't appear (user still sees Send Annotations + Close). Reverse also true: gated session stays gated even if agent resubmits without `--gate`. Fix: `updateContent` should accept and update the `gate` flag. |
| Provenance data for stale sessions | P3 | Deferred — collect timestamps (feedback-sent-at, last-agent-contact) so we can show "you submitted feedback but it never came back." Future UI concern. |
| `onCancel` never wired on awaiting banner | nit | Deferred |
| Session collisions across same-repo worktrees | nit | Accepted — local app, one daemon per machine |
| Annotate slug doesn't update on heading change | nit | Accepted — cosmetic, versions work correctly |
| VS Code editor annotations not cleared on revision | P2 | Fixed — `editorAnnotations.clearAll()` added to `handleUpdateContent` in plan and review servers |
| PR diff scope/baseline not reset on reuse | P2 | Deferred — part of the broader PR metadata staleness issue. `originalPRPatch`, `currentPRDiffScope` not updated in `handleUpdateContent`. |
| Remote share link stale on session reuse | P2 | Fixed — all three reuse paths regenerate `remoteShare` before returning the record |
| `sessionRefs` lazy cleanup | nit | Accepted — negligible memory |
Loading