Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (2)
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3861e4c563
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38de2322fa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 38de2322fa
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 802ecd3466
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 802ecd3466
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Not-ready checkpoint — review limit reachedHead: Local static checks and targeted suites (including 15 IPC tests) pass. Remote UAT round 6 passed its targeted final-batch scenarios, with screenshots, recordings, raw provider/history evidence, and cleanup retained under The push automatically triggered the third code/security pair (“New commits”), consuming all six combined assessments before an additional manual request. No seventh review was requested. The final pair raised five code findings plus one security threat. A bounded read-only triage found:
Decision: hold; not ready. The advisor recommends stopping the repair/review loop at this checkpoint. New threads remain unresolved. No product changes were made during the final triage; Runs 2–3 remain blocked on this prerequisite. To resume, the next step is a bounded extension for these remaining correctness repairs and their validation/review, plus an explicit decision on the attachment trust contract. The proposed split keeps the code repairs in this backend layer and defers any system-only transport redesign to a separately accepted design; no new storage subsystem will be added here. Earlier follow-ups remain tracked in the PR description. Nothing has been merged. Generated with |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b207292f79
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🤖 Not-ready checkpoint — extended review allowance exhaustedHead: The five authorized repairs were pushed after parent Bun 1.3.5 validation passed: The final automatic code/security pair has completed: 8/8 combined assessments. Four code findings were triaged read-only:
Decision: hold, not ready. The advisor recommends respecting the bounded extension. Making even the small remaining fixes would create another head needing fresh validation/review. Security completion is not claimed as a clean verdict while its visual-media advisory remains in the summary. No further code changes, review triggers, or dependent UI work have been started. The already-running targeted UAT round 7 may finish only its existing five-fix scope and deadline. Any scoped PASS will not override the open defects. Evidence and cleanup results will be appended when available. The smallest resumption batch is two localized backend fixes with regressions, plus real-history replacements for only the new mocked-read tests. Keep it in this backend layer; no new subsystem or transport redesign, and no change to the reset boundary. This proposal is not another repair/review authorization. Nothing has been merged. Generated with |
🤖 UAT round 7 complete — no overall PASS; backend remains on holdTested exact head:
Readiness and next boundaryThe two final-review correctness findings and the real-history test gap remain open. A/C above add unresolved live acceptance failures pending classification. The advisor recommends holding this exact head and first doing bounded, explicitly authorized baseline/mechanism diagnosis of A/C before defining any further repair batch. Do not assume the remaining work is only a few lines. No new diagnosis, repairs, or review requests were started after the eight-assessment limit. Runs 2–3 remain blocked. All runtime CI jobs now pass, including Unit, Integration, Storybook, E2E and Visual Regression Testing. Isolation, cleanup and retained evidenceTwo audit passes found zero non-chat/workspace leak rows. Chat tooling first attempted a Remote command receipts show the owned dev-server/fixture stopped, no listeners on ports 33839/46827/8899, and the named browser closed. This cleanup was not independently rechecked. The runner watcher is stopped; the chat and remote evidence remain inspectable. Local evidence: Observed deep-payload failureDeep-payload live failure recording round7-stepC.webmRemaining scenario recordings and screenshot overviewScreenshot overview (includes failed attempts and obscured tutorial states): A — blocked-read stall, then feed and recovery (downscaled; unchanged duration). Blocked-read recording round7-stepA-compressed.webmA5 — Stop variant. Stop-variant recording round7-stepA5.webmB — feedback limits and normal submission. Feedback-limits recording round7-stepB.webmD — UI message edit; persisted-state proof is in the raw artifacts. Message-edit recording round7-stepD.webmE — idle-compaction scenario (offline backdating; downscaled, unchanged duration). Idle-compaction recording round7-stepE-compressed.webmS — smoke observations. Smoke recording round7-stepS.webmGenerated with |
|
The authorized repair work is active again. Nothing is ready to merge yet.
The merge remains blocked on those fixes, final exact-head UAT, fresh reviews, and required CI. No merge or gate bypass has occurred. Generated with |
…nt to hidden and malformed rows Addresses the three open Codex findings on PR #4317 (b207292): - computeKeepRecentTailStamp now filters model-hidden rows (plan-review records, workflow display rows) with the shared isModelHiddenMessage predicate before sizing the keep-recent tail. A single plan snapshot can exceed the whole RLM floor even though it never reaches a provider request, so charging it made the selector drop the entire visible tail. The stamp is still the selected row's durable historySequence, so the selector and its tests are unchanged. - derivePlanReviewState accepts only a non-negative integer historySequence and otherwise falls back to the visiting index, so a damaged persisted row (e.g. a string) no longer fails oRPC output validation on every getState/mutation. - The two new idle-compaction eligibility tests seed rows through the real HistoryService (appendToHistory) instead of mocking getLastMessages / getHistoryFromLatestBoundary, so the 50-row tail window and the latest-boundary fallback are exercised for real. New compactionRequests.test.ts covers the oversized hidden snapshot after the newest turn, before the newest human turn, and between an @file prelude and its prompt (durable sequence, not filtered index). --- _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$147.27`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=147.27 -->
b207292 to
17ef6d3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17ef6d3cf3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…pose workspace.planReview endpoints
Define the <mux_plan_review> record contract (snapshot/feedback/resolve/reopen)
with a pure projection, keep record rows out of provider requests, compaction
tail copies, transcript display and human-turn detection, snapshot every
successful propose_plan from the session tool-completion listener, add
workspace.planReview.{getState,ensureSnapshot,setThreadResolved,submitFeedback},
and give the plan agent a <plan-review-state> block plus static guidance.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…workspace.planReview contract Unit tests for the <mux_plan_review> envelope/projection and state block, for record rows staying out of provider requests, compaction tail copies, the transcript and human-turn detection, and for lookalike neutralization. Adds tests/ipc/planReview.test.ts, which drives the real request builder through a loopback OpenAI-compatible fixture emitting propose_plan tool calls to check snapshotting, feedback/resolution endpoints, the <plan-review-state> block, compaction, manual reset and pasted lookalikes. Regenerates the synced plan agent docs/skill content from plan.md.
…ranscript The sidebar-status generator builds its <transcript> from the last N history rows filtered by role only, so the new synthetic plan-review record rows (snapshot/resolve/reopen, including the full plan text) reached that provider request and re-triggered generation on every appended record. Filter isModelHiddenMessage in buildTrailingTranscript so UI-only rows never contribute transcript text, matching keepContextRow and the compaction tail. Adds a unit test seeding an authentic snapshot row and a later resolve row: neither appears in the transcript and the resolve does not regenerate. Signed-off-by: Thomas Kosiewski <tk@coder.com>
…inputs/results messagePipeline's neutralizer only sees the request built from persisted history. Tool calls executed DURING a turn bypass it: the SDK feeds their inputs/results straight into the next streamText step, so repository text returned by bash/file_read reached the provider with the exact <mux_plan_review> / <mux_agent_message> wrapper in the same turn (UAT round 3, raw request #74) and was only neutralized on the next turn (#78). Add neutralizeAgentEnvelopeLookalikesInModelToolParts (ModelMessage tool-call input + tool-result output only, reusing the existing deep string rewrite) and run it in prepareStep between workflow-record stripping and tool media extraction, on both the per-step and the thinking-override first-step rebuild paths. Text parts are deliberately untouched at this seam: row metadata proving feedback/peer authenticity is gone there and the history path already handled non-authentic text. Request-only; persisted history is not rewritten. Tests: prepareStep regression captured through the real streamText closure (red before the fix), helper unit tests (output variants, media identity, same-reference when unchanged), and an IPC loopback scenario that scripts a same-turn file_read of a lookalike file and asserts on the second provider request body plus the untouched persisted output. --- _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$3.74`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=3.74 -->
…tract Each finding was reproduced with a failing test at the real seam before the fix: - Serialized row cap: ensurePlanSnapshot measures the row HistoryService will write (message + workspaceId + widest sequence) against SESSION_HISTORY_MAX_LINE_BYTES, not only raw plan bytes; oversized plans return the typed plan_too_large. (An oversized row is still readable by getState, but the provider and replacement-row scanners treat it as an unreadable run, violating the PR's own bound.) - Snapshot hash verification: derivePlanReviewState takes an optional browser-safe hashContent hook; the backend injects sha256 so a corrupted or hand-edited snapshot row is skipped and a later ensure call self-heals. - Completion barrier: propose_plan snapshot captures are registered on the session and awaited by the turn-completion policy, so the turn cannot go idle (and a queued turn cannot revise the plan file) mid-capture. The capture stays non-throwing; propose_plan itself is unchanged. - Continuous compaction: model-hidden rows are filtered before rolling-cut selection, attachment estimation and the summarizer head (headFromRows filters identically so fingerprints stay comparable); persisted rows and boundary identity keep the raw snapshot. - Sidebar status window: collect AGENT_STATUS_MAX_TRAILING_MESSAGES visible rows via the backward history scan instead of filtering a count-capped read. - MessageQueue: plan-review feedback entries are sealed in both orderings. - isPlanReviewRecordMessage: a row is provider-visible only when getAuthenticPlanReviewRecord returns feedback; every other plan-review-discriminated row (corrupted kind, extra parts, mismatched metadata) stays hidden. - Lone CR line endings normalize to LF like CRLF. Tests: unit regressions per finding (record, envelope, state, assembler, queue, status service, compactor, a new agentSession snapshot-barrier test), aggregator/assembler fixtures switched to authentic feedback envelopes, and three IPC loopback scenarios (oversized row, corrupt hash self-heal, feedback queued behind a held stream). --- _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$36.32`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=36.32 -->
…eadline, reset dedup, SVG neutralization) Round-2 review repairs on the native plan-review backend contract: - Re-snapshot after context reset (4065227293): the <plan-review-state> block keeps replaying pre-reset SNAPSHOT rows (only snapshots; pre-reset threads stay behind the privacy floor), so feedback on a plan deduplicated against a pre-reset snapshot is no longer dropped as dangling once its envelope leaves the active context. - Bound the state block (4065227298/4065227345): threads ordered by most recent user activity; each quoted text clipped to PLAN_REVIEW_STATE_MAX_TEXT_CHARS, reply tail to PLAN_REVIEW_STATE_MAX_REPLIES_PER_THREAD, whole block to PLAN_REVIEW_STATE_MAX_CHARS, always naming omitted threads. Rendering only; stored feedback is untouched. - Malformed persisted rows (4065227309): getAuthenticPlanReviewRecord guards the parts shape, so a valid-looking row with `parts: null` is skipped instead of throwing in getState/request assembly. - Bounded, cancellable snapshot capture (4065227321): each propose_plan capture carries an AbortController; completion policy waits only for a completed turn and only up to PLAN_REVIEW_SNAPSHOT_CAPTURE_TIMEOUT_MS (racing the deadline, since the stall can be the remote plan read itself); Stop/failed turns abort immediately. ensurePlanSnapshot checks the signal before/after the read and at append admission under the history lock (capture_aborted), so an abandoned capture never publishes a late row. - Feedback size caps (4065227331): oRPC per-field/per-record limits (quote 500, body 4000, summary 2000, 50 comments/replies) plus a persisted-row check against SESSION_HISTORY_MAX_LINE_BYTES (feedback_too_large) before the send. - Decoded SVG tool attachments (4065364170, security): createInlineSvgAttachmentText neutralizes protocol-envelope lookalikes in the decoded text and uses a fence longer than any backtick run inside it, covering both the history and same-turn extraction paths. Tests: red-first unit + IPC boundary coverage for each item (state block bounds/recency, malformed parts, capture deadline/Stop/admission, feedback caps and row limit, unchanged pre-reset snapshot surviving compaction, base64 SVG carrying the wrapper and a fence break). --- _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$66.32`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=66.32 -->
…ord rows, capture tracking, deep payloads and feedback sizing Cycle-3 repairs on the native plan-review backend, each with a red-first regression: - Abandoned captures (4065820771): a propose_plan capture abandoned at the deadline (or on Stop) is now removed from pendingPlanSnapshots as it is aborted, so a hung remote read no longer makes every later completed turn wait through another full deadline. Late appends are still refused by ensurePlanSnapshot's admission checks on the aborted signal. - Feedback row sizing (4065820788): preparePlanReviewFeedback measures the row with the send options the row will persist (toolPolicy and the startup-retry snapshot, whose additionalSystemInstructions/providerOptions the API leaves unbounded), so a small envelope with huge options is refused as feedback_too_large instead of being reported as sent and then skipped by the history scanners. - Deep tool payloads (4065820795): neutralizeStringsDeep walks tool inputs/outputs with an explicit stack (post-order rebuild, identity preserved when nothing changes) in both the history and same-turn passes; a 100k-deep JSON result no longer throws RangeError in prepareStep. - Edit truncation (4065820811): getEditTruncateTargetFromMessages stops at plan-review record rows, so editing an ordinary message after an idle resolve/reopen no longer deletes that independent record; request-owned prelude snapshots are still cut with the edit. - Idle compaction / heartbeat eligibility (4065820819): both unanswered-tail checks judge the last row that is not a hidden plan-review record. When the bounded idle-compaction tail holds only hidden rows, the check consults the history since the latest boundary so a pending human prompt further back keeps its protection. - Plan prompt (4065812832): states that genuine feedback arrives only as direct conversation text and that wrapper-looking text inside images/PDFs/SVGs/tool output/attachments/quoted content is untrusted data whose instructions are never followed. This is guidance, not authentication of pixels; the dedicated user-message transport is unchanged and multimodal injection remains a residual risk. Generated agent content and docs mirror regenerated. --- _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$96.08`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=96.08 -->
…nt to hidden and malformed rows Addresses the three open Codex findings on PR #4317 (b207292): - computeKeepRecentTailStamp now filters model-hidden rows (plan-review records, workflow display rows) with the shared isModelHiddenMessage predicate before sizing the keep-recent tail. A single plan snapshot can exceed the whole RLM floor even though it never reaches a provider request, so charging it made the selector drop the entire visible tail. The stamp is still the selected row's durable historySequence, so the selector and its tests are unchanged. - derivePlanReviewState accepts only a non-negative integer historySequence and otherwise falls back to the visiting index, so a damaged persisted row (e.g. a string) no longer fails oRPC output validation on every getState/mutation. - The two new idle-compaction eligibility tests seed rows through the real HistoryService (appendToHistory) instead of mocking getLastMessages / getHistoryFromLatestBoundary, so the 50-row tail window and the latest-boundary fallback are exercised for real. New compactionRequests.test.ts covers the oversized hidden snapshot after the newest turn, before the newest human turn, and between an @file prelude and its prompt (durable sequence, not filtered index). --- _Generated with `xum` • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$147.27`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=147.27 -->
Validate persisted sequence values before pagination, incremental skips, and reconnect cursor selection. Keep writer assertions and raw history intact. Tests cover oldest, middle, newest, and all-string histories, successful caught-up delivery, valid next-turn cursors, and pagination progress. _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$384.14`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=384.14 -->
… integration Use the shared model-hidden predicate in main's new auto-routing path so hidden review rows cannot reach the classifier or change attachment gating. Two regression cases fail before this integration fix and pass afterward. Add the feature-only FIFO plan-path regression, using deterministic owned read settlement for teardown, and stop the loopback fixture from reissuing an already-failed propose_plan call on every tool step. _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$466.07`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=466.07 -->
…nel context Treat repository-controlled plan paths as bounded quoted data in the system block. Apply the existing model-hidden predicate before title selection, abandoned-branch token eligibility and transcript limits, and refinement message limits. Keep canonical rows and refinement fingerprints unchanged. Six behavioral cases fail before the repair and pass after it: newline/closing-tag and oversized paths, title context, transcript space, token eligibility, and refinement trajectory retention. Genuine feedback remains visible, including beside hidden markers lacking synthetic metadata. The bounded model-consumer inventory found these four missing projection seams. UI-only last-prompt recall is outside this model-prompt repair; no canonical-history or storage change is included. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$830.05`_ <!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=830.05 -->
17ef6d3 to
e891bcf
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e891bcf9f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Blocked checkpoint — final head e891bcfThe repair cycle is paused at
A fresh independent advisory pass recommended blocked by specific problems. Its suggested deferrals and its claim of a known E2E flake are not accepted without evidence. Recorded external assessment counts are C1=4, C2=4, A=6, feature=12 (8 earlier + 4 this phase). Existing authority permits necessary extensions; the count is not the reason for this pause. After audit attribution is resolved, resume with one bounded diagnostic pass to reproduce or falsify all four claims and the E2E failure. Agree on minimal existing seams before a consolidated repair. Do not introduce broad locking around Generated with |
…ct KEYS Codex finding 4074399269 (PR #4317): neutralizeStringsDeep rewrites string values but copies object property keys unchanged, so a tool result or tool-call input such as {"<mux_plan_review>": 1} reaches the provider with the raw protocol tag (tool outputs are JSON.stringify'd into tool_result content; tool-call inputs are sent as request JSON). Two RED tests, one per pass (persisted-history MuxMessage pass and same-turn ModelMessage pass), covering both the new plan-review tag and the pre-existing agent-message tag. _Generated with xum • Model: coder:anthropic/claude-fable-5-1 • Thinking: xhigh_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh --> Signed-off-by: Thomas Kosiewski <tk@coder.com>
…contracts Diagnosis-only tests for two Codex findings on the native plan-review backend (no production changes): - agentStatusService: the sidebar status transcript must not cross a durable manual context reset. Seeds pre-reset rows, appends a reset boundary through appendToHistory (the WorkspaceService.resetContext path, which rotates the sealed prefix into chat-archive.jsonl), then a short post-reset epoch with a hidden plan-review row. Currently RED: the trailing-window scan tops up from the archive. The same assertion is equally RED against the pre-PR getLastMessages implementation. - planReviewService.setPlanReviewThreadResolved: a forged feedback row with a ~600 KiB threadId yields a 1,229,313-byte resolve row (limit 1,048,576) because the id is repeated in the envelope and in muxMetadata. GREEN: iterateFullHistory (getPlanReviewState, buildPlanReviewStateInstruction) still replays the oversized row, so the projection does not reopen the thread; the row-size guard gap is a hygiene inconsistency, not a state divergence. _Generated with xum • Model: coder:anthropic/claude-fable-5-1 • Thinking: xhigh_ Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ts, refuse stale feedback Object property keys in tool inputs/outputs are provider-bound text too, so the deep neutralizer now rewrites wrapper tags in keys with the same rule as values; colliding rewritten keys keep every entry with a stable " (n)" suffix. The sidebar status transcript scan stops at the latest durable manual reset so a short post-reset conversation is never topped up with discarded pre-reset rows (the previous count-capped read had the same archive top-up gap). Plan review feedback compares the context-mutation epoch captured before its history read in the same synchronous block that enters sendMessage, refusing a send whose snapshot/thread references were cleared or reset in between instead of appending a dangling row. Three behavioral regressions fail before these changes and pass after them; the imported history-contract test also shows an oversized resolve row is still replayed, so no record-size change was needed. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$876.63`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=876.63 -->
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 598009748e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…pending questions answerable The feedback endpoint no longer accepts edit fields (editMessageId, historyEditPrecondition, unfencedEdit): an edit truncates history at its target before the row persists, which could delete the referenced snapshot or thread. The handler asserts the same for internal callers. The committed-history fallback of answerAskUserQuestion ignores model-hidden records when deciding whether a question is still the latest turn, so resolving or reopening a review thread cannot make a pending question unanswerable. Both behaviors have regressions that fail before and pass after this change. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$898.98`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=898.98 -->
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fca80cec10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…en records when inheriting turn correlation Thread ids come from persisted rows, so a forged or corrupted authentic feedback row could carry a closing tag in its id; the state block now quotes and bounds ids like every other untrusted field. A model-hidden review record appended after a correlated assistant cut is UI state, not a turn boundary: the wake continuation skips it and still inherits the workspace-turn correlation. Both regressions fail before and pass after this change. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:anthropic/claude-fable-5-1` • Thinking: `xhigh` • Cost: `$915.24`_ <!-- mux-attribution: model=coder:anthropic/claude-fable-5-1 thinking=xhigh costs=915.24 -->
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4843fa707
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const maxRowBytes = SESSION_HISTORY_MAX_LINE_BYTES - PLAN_REVIEW_FEEDBACK_ROW_HEADROOM_BYTES; | ||
| if (rowBytes > maxRowBytes) { |
There was a problem hiding this comment.
Measure the auto-compaction row before accepting feedback
When a large but accepted feedback submission triggers on-send auto-compaction, buildAutoCompactionRequest stores the envelope both in the compaction prompt text and in metadata.parsed.followUpContent.text. This check measures only the ordinary user-row shape, so a feedback row near the allowed limit can produce a compaction JSONL row over SESSION_HISTORY_MAX_LINE_BYTES; subsequent history scans skip that row, losing the compaction request and feedback. Fresh evidence beyond the earlier retry-metadata fix is this alternate persisted shape, which still is not measured.
Useful? React with 👍 / 👎.
| const appended = await deps.historyService.appendDerivedFromFullHistory( | ||
| args.workspaceId, |
There was a problem hiding this comment.
Keep hidden mutations from stranding compaction follow-ups
If a user resolves or reopens a thread after a compaction summary with pendingFollowUp commits but before its continuation dispatches, this direct append places a hidden non-copy row after the summary. AgentSession.dispatchPendingFollowUp treats every such row as unrelated content and returns false; startup recovery then inspects the hidden row rather than the summary, so the pending user follow-up remains stranded indefinitely. Either coordinate these appends with compaction settlement or make follow-up recovery ignore model-hidden rows.
Useful? React with 👍 / 👎.
| // Plan-review record rows are hidden UI state, not human turns: rolling cut, | ||
| // keep-recent-tail, retry eligibility and goal reconciliation must all skip them through | ||
| // this one predicate instead of mistaking them for a user prompt. Edit truncation skips | ||
| // them too but never cuts them (see getEditTruncateTargetFromMessages): unlike request | ||
| // preludes they are independent durable mutations. | ||
| isPlanReviewRecordMessage(message) |
There was a problem hiding this comment.
Skip hidden review rows during workspace-turn recovery
When a delegated workspace has a retrying or recoverable interrupted turn and the user resolves, reopens, or snapshots plan review state, this new synthetic user row can become the newest history entry. WorkspaceTurnManager.reconcileSettledWorkspaceTurn treats every uncorrelated user row as a superseding prompt, disables revival, and may return before reaching the correlated prompt or final assistant row, leaving the parent handle unsettled. The earlier inheritance fix covers compactionRequests, but this recovery scan still does not consult the new hidden-row predicate.
Useful? React with 👍 / 👎.
Paused at
|


Summary
Persist native plan-review state in workspace chat history. This is the backend layer only. Rendered review controls and loop closure remain later work;
propose_plankeeps its schema and result contract.This PR builds on #4339, above the live tool-input guard (#4336) and persisted tool-payload recovery (#4337), in native stack #4340. Those runtime safeguards are separate prerequisites, not duplicated in this diff.
Implementation and review focus
workspace.planReviewendpoints and provide deterministic unresolved-thread context in plan mode.Validation and status
Not ready to merge. Final exact-head remote UAT, fresh code/security reviews, and required CI are still gates.
On
a4843fa707710597de0941d978ad262166a411fd, pinned Bun 1.3.5 / Node 22.19.0 and the frozen lockfile passedmake static-check(78s), 919 review/compaction/message/context-management tests (7s), and the 18-case plan-review IPC suite (10s); the preceding heads also passed 959 touched-surface tests, 433 prior regressions, and 38 native-Node IPC cases. These are per-run counts with overlapping coverage, not a unique total. These are local results, not a remote UAT verdict.The latest repair neutralizes wrapper tags carried in tool-payload object keys (colliding rewritten keys keep every entry with a stable suffix), stops the sidebar status scan at the latest durable manual reset, and refuses plan-review feedback whose history read was overtaken by a context clear or reset before the send entered admission. Three behavioral regressions failed before the repair and pass after it, including a full plan-review IPC run without the admission check. An imported history-contract test also shows an oversized resolve/reopen row is still replayed on reload, so no record-size change was made.
The feedback endpoint now omits edit fields (an edit would truncate history before the row persists), and the committed-history fallback for answering a pending question ignores model-hidden records, so resolving a review thread cannot make a question unanswerable. Both have red-to-green regressions. Thread ids in the
<plan-review-state>block are quoted like every other persisted field, and a hidden review record appended after a correlated assistant cut no longer breaks workspace-turn inheritance for the next wake.The final repair quotes and bounds repository-controlled snapshot paths in system context. A bounded model-consumer inventory also found four missing filtering points: title selection, abandoned-branch token eligibility, the shared branch transcript builder, and refinement before its row limit. Six new behavioral cases failed before the repair and passed after it. Hidden rows no longer consume those budgets; genuine feedback remains visible. Canonical history and refinement fingerprints are unchanged.
Behavioral regressions first reproduced hidden RLM-tail token sizing, malformed projection sequences, and hidden review text/attachments influencing the auto-model router. The idle-compaction cases now exercise real persisted history instead of canned reads. The separate prerequisites retain red/green evidence for FIFO acquisition and over-deep live/persisted tool payloads, including non-v4 models and nested calls; engine-sensitive cases run under native Node.
Remote UAT round 7 on the predecessor returned no overall PASS: blocked plan reads stalled later work and deep payloads overflowed SDK cloning. Assessed predecessor evidence is retained, not reclassified as acceptance of this head. Round 8 is paused for attribution of unexpected same-user audit activity, before acceptance on the final head. Its predecessor captures are not acceptance evidence. Once that hold is resolved, the round must test the complete corrected stack from a fresh critical runner using loopback providers, screenshots, video, raw provider requests, restart/compaction, malformed replay, and more than four overlapping FIFO-path reads. No live paid-provider or multimodal-security result is claimed.
Accepted behavior and residual risk
History replay, snapshot lifecycle and provider preparation are the main regression surfaces. No browser review persistence, new review UI, backup subsystem, or privacy-floor change is included. Runs 2–3 remain outside this delivery.
📋 Implementation Plan
Native plan review inside
propose_plan(history-backed, Crit-inspired)Summary
Add inline review of the rendered plan directly in the
propose_plancard: comment on a block orselected text, reply to existing threads, and send everything to the plan agent as one dedicated
<mux_plan_review>message. Thread resolution and per-proposal plan snapshots are persisted asappend-only rows in the workspace chat history (
chat.jsonl), so the UI reconstructs the whole review —including older proposals — from history alone.
Hard constraints (product decisions; not negotiable during implementation):
propose_plantool (schema, result, behavior). Snapshotting is a session-levellistener on tool completion, outside the tool.
localStorage/usePersistedState). Unsent commentslive only in component state until "Send feedback".
conversation; no separate review workspace, no external daemon.
like
<mux_agent_message>/<mux_subagent_report>.Execution model: three
implement-flowruns, one stacked PR each (see "Execution with implement-flow").Net product LoC (excluding tests/stories): ~1,270 (+ ~150 for the optional agent-reply tool, not part
of the three runs). This document is passed verbatim as
planMarkdownto every run; each run implementsonly the scope named for it and treats the Design sections as the shared contract.
Current state (verified at
6f573ea)ProposePlanToolCall.tsxrenders the plan withMarkdownRenderer; annotate mode (Shift+A,KEYBINDS.TOGGLE_PLAN_ANNOTATE) swaps toPlanAnnotationView→SelectableDiffRenderer(raw Markdownlines prefixed with a space,
src/browser/features/Tools/PlanAnnotationView.tsx:14-43) and hides the TOC.localStorageviauseReviews(src/browser/hooks/useReviews.ts:60-68), anchored by linenumbers only;
Implement/Continue in Autosend the literal"Implement the plan"(
ProposePlanToolCall.tsx:616-625) and ignore notes.propose_planreturns{ success, planPath, message }(src/node/services/tools/propose_plan.ts:106-110);the file is mutable, so non-latest cards show
*Plan saved to <path>*(ProposePlanToolCall.tsx:345).</→<\/, anchored regex parse):src/common/utils/agentMessageEnvelope.ts:23-46,101.neutralizeAgentEnvelopeLookalikesForProvider.ts:41-88, called fromsrc/node/services/messagePipeline.ts:146.keepContextRow(turnContextAssembler.ts:73-74) viaisWorkflowDisplayOnlyMessage(src/common/utils/workflowRunMessages.ts:257-259).isSyntheticSnapshotUserMessage(src/common/types/message.ts:1467-1475), used byrollingCut.ts:121,124,keepRecentTail.ts:102,editTruncation.ts:29,historyService.ts:4375,agentSession.ts:2075,2286,5587,5656,workspaceGoalService.ts:748.lastUserPrompt(workspaceService.ts:15334) andworkspaceGoalService.ts:806already skip any
syntheticrow.compactionHandler.buildPreservedTailCopies(compactionHandler.ts:1133-1193)copies every row in the tail as
synthetic + rlmPreservedTailCopy, with no display-only exclusion.shouldHideMessageFromTranscript(StreamingMessageAggregator.ts:3705-3711) hidessynthetic && uiVisible !== truerows unlesswindow.api.debugLlmRequest.addMessage,getAllMessages(),StreamingMessageAggregator.ts:1074,1503) andWorkspaceStorebumpssubscribeKeylisteners on everywhole-message event (
WorkspaceStore.ts:5275-5280).historyService.appendToHistory(:3496) thensession.emitChatEvent({ ...message, type: "message" })(pattern:appendWorkflowRunInvocation,workspaceService.ts:11365-11445).historyService.iterateFullHistory(workspaceId, "forward"|"backward", visitor)(
historyService.ts:2144-2148); manual reset marker:isDurableContextResetBoundaryMarker(
compactionBoundary.ts:42-54). Read-side row limitSESSION_HISTORY_MAX_LINE_BYTES = 1 MiB(
contextBudget.ts:55, oversized rows skipped inhistoryScanner.ts:774-779); no write-side limit.toolCallId+result:agentSession.ts:9189-9218(tool-call-endlistener;
propose_planalready special-cased there for sidebar refresh).ToolCallEndEvent.resultis
unknown(schemas/stream.ts:501-519).turnContextAssembler.ts:262-278(pure module; callers passhistory).components/rehypePlugins, hastnode.positionon custom components(
MarkdownCore.tsx:204-216,node_modules/streamdown/dist/index.d.ts:13-25).useConfirmDialog()(src/browser/contexts/ConfirmDialogContext.tsx:22);createHash("sha256")convention.sendMessageoptions passmuxMetadata: z.any()through (schemas/stream.ts:936).Design
1. Records (
src/common/utils/planReview/)Four record kinds, each one
MuxMessagerow with a<mux_plan_review>text part(
formatPlanReviewEnvelope, same escaping/parse strictness asagentMessageEnvelope.ts) andmetadata.muxMetadata = { type: "plan-review", kind, recordId, snapshotId?, threadId?, feedbackId? }:Row shape:
snapshot,resolve,reopenusertrue(uiVisible unset)feedbackuserInvariants (checked by the backend before append, re-asserted by the projection):
feedback.comments[].anchorwithin1..lineCount(snapshot),bodynon-empty;replies[].threadIdreferences an existing thread;
resolve/reopenreference an existing thread and are idempotent.snapshot.contentHash = sha256(content); snapshots are deduplicated by hash per workspace.historySequenceorder.rlmPreservedTailCopyor arecordIdalready seen are ignored (compaction copies are inert).2. Projection (
derivePlanReviewState(messages): PlanReviewState)Pure and shared (backend + frontend). A row counts only if
muxMetadata.type === "plan-review", the textparses as an envelope, and parsed
kind/recordId/threadId/snapshotId/feedbackIdmatch the metadata(mirrors
getValidAgentPeerMessage). Unknownkind/v, mismatches and dangling references are skippedwith
log.debug(self-healing).A thread exists only once it was sent;
resolvedis the lastresolve/reopenrecord.Sent never implies resolved; only the user resolves.
3. Model visibility and turn-boundary safety
isPlanReviewRecordMessage(m)=muxMetadata.type === "plan-review" && kind !== "feedback".isModelHiddenMessage(m)(src/common/utils/messages/modelHiddenMessages.ts) =isWorkflowDisplayOnlyMessage(m) || isPlanReviewRecordMessage(m); used inkeepContextRow(
turnContextAssembler.ts:73) and inbuildPreservedTailCopiesso record rows are neither sent norcopied across compaction boundaries. Audit the compaction request builders (
compactionHandler.ts,continuousCompactor.ts) and context-usage stats for the same filter.isSyntheticSnapshotUserMessagewith|| isPlanReviewRecordMessage(message)(with acomment: record rows are hidden UI state, not human turns) so rolling cut, keep-recent-tail, edit
truncation, retry/rollover eligibility and goal reconciliation all skip them through the predicate they
already use.
shouldHideMessageFromTranscriptadditionally hidesisPlanReviewRecordMessagerows even whendebugLlmRequestis on (that mode shows what the model sees; these rows are not sent).<mux_plan_review>to<user_pasted_mux_plan_review>in text andtool parts; authentic =
role: "user",muxMetadata.type === "plan-review", envelope cross-checks.effectiveMode === "plan"and unresolved threadsexist, the
assemblePromptPayloadcaller (AgentSession, which ownshistoryService) derives the stateover full history after the latest durable manual reset marker (
iterateFullHistory(..., "backward"),stop at
isDurableContextResetBoundaryMarker) and adds a<plan-review-state>block toadditionalSystemInstructions: current snapshot hash, up to 30 unresolved threads (threadId, lines,quote, latest body) and an explicit truncation note. Derived every turn, never stored, independent of the
compaction summary. No new post-compaction attachment (exec mode does not need review state).
src/node/builtinAgents/plan.md: how to read a<mux_plan_review>feedback message(line numbers refer to the reviewed revision, which may differ from the current file; use
quote),address every item, revise the plan file, call
propose_planagain, never claim a thread resolved.4. Snapshots
tool-call-endlistener (agentSession.ts:9189-9218), whentoolName === "propose_plan"andresult.success === true, read the plan file through the workspaceruntime, hash it, and — unless a snapshot with that hash exists — append a
snapshotrow withproposalToolCallId = payload.toolCallId(non-waking append +emitChatEvent).propose_planends theplanning turn, so no agent edit can interleave. The hook is fully non-throwing (try/catch around read,
hash and append; failures
log.warn) so it can never reject or stall thetool-call-enddispatch oraffect the tool result. Content is CRLF-normalized (
\r\n→\n) before hashing and storing so anchorline numbers map 1:1 across platforms. Skip (with
log.warn) when content exceedsMAX_PLAN_SNAPSHOT_BYTES = 256 KiB(src/constants/), well under the 1 MiB read-side limit.planReview.ensureSnapshot): same routine, used when a card has no snapshot (historywritten before this feature, skipped/failed hook, ephemeral
plan-displaypreviews) and for"Review latest" after an external edit. Dedup by hash makes it idempotent.
5. Backend API (
WorkspaceService.planReview*+ oRPCworkspace.planReview.*)Schemas in
src/common/orpc/schemas/api.ts, handlers inrouter.ts(mirrorworkspace.replaceChatHistory,router.ts:1759-1767). Every mutation returns the freshPlanReviewState; the UI never composes envelopes.getState({ workspaceId })iterateFullHistory→derivePlanReviewState.ensureSnapshot({ workspaceId, proposalToolCallId? }){ snapshotId, contentHash, state }. Typed errors:plan_missing,plan_too_large.setThreadResolved({ workspaceId, threadId, resolved })resolve/reopen. Typed error:unknown_thread.submitFeedback({ workspaceId, snapshotId, summary?, comments: [{anchor, quote, body}], replies: [{threadId, body}], options })feedbackId/threadIds/replyIds, build thefeedbackenvelope, and send it as the user message through the existingsendMessagepath withoptions(agent/model/thinking from the UI, asImplementdoes) plus stampedmuxMetadata. Typed errors:unknown_snapshot,unknown_thread,invalid_anchor,nothing_to_send.Validation and append happen under the history write lock (same pattern as
appendToHistory), so twowindows cannot both resolve/reopen inconsistently.
6. Frontend
usePlanReview(workspaceId)(src/browser/hooks/usePlanReview.ts): fetchesgetStateon mount,applies the state returned by each mutation, and refetches when a new
plan-reviewrow shows up inaggregator.getAllMessages()(observed throughWorkspaceStore.subscribeKey— covers other windows, thecompletion hook and future agent replies). Backend state is the only authority; the frontend keeps just
unsent drafts (
{ anchor, quote, body }[],{ threadId, body }[]) in component state.Review mode in
ProposePlanToolCall(replacesPlanAnnotationView):Shift+Atoggles review mode (label "Review"). The card binds to the snapshot whoseproposalToolCallIdmatches its tool call (latest or not); if none, it callsensureSnapshot. Reviewmode renders the snapshot content, so anchors are stable. If the live plan file differs from the
snapshot (
getPlanContentalready re-fetches on focus), show "Plan changed since this snapshot — Reviewlatest" which creates a new snapshot; existing threads stay bound to their snapshot.
PlanReviewView(src/browser/features/Tools/ProposePlan/PlanReviewView.tsx) renders the snapshot astop-level Markdown blocks (
splitPlanBlocks.ts:remark-parse+remark-gfm,node.positionstart/end lines; link reference definitions are appended to every block so
[text][ref]still resolves)with one
MarkdownRendererper block and thread cards / composer interleaved as React siblings rightafter their anchored block (Crit's inline layout, any width, no DOM measurement or portals). Reading mode
keeps the single renderer, so normal rendering is untouched.
composer under the block; or select text in the view → floating "Comment" button. Anchor = enclosing
block range; a selection spanning blocks is clamped to
[firstBlock.startLine, lastBlock.endLine]soanchors never split a block;
quote=Selection.toString()trimmed, capped at 500 chars, else theblock's first 200 chars. Drafts render as cards with a "Draft" badge and Edit/Delete.
PlanReviewThread.tsx(visual language ofInlineReviewNote): badge (Draft / Sent / Resolved), quote,body, replies (author label), actions Reply (adds a draft reply) / Resolve / Reopen. Bodies are plain text.
"Open from earlier revisions (M)" collapsible listing unresolved threads bound to other snapshots with
Reply/Resolve.
Implement/Continue in AutocalluseConfirmDialog().confirmwhen drafts orunresolved threads exist ("Implement with open review comments? N unresolved, M unsent").
of
*Plan saved to…*.feedbackrows render inUserMessage.tsxas a compact card "Plan feedback · N comments"(expandable quote + body list); the raw envelope shows only if parsing fails.
KEYBINDS, hidden on mobile): comment on focused block/selectionshift+c, next/prev threadshift+j/shift+k, send feedbackmod+shift+enter(review mode only); composermod+enter/esc.buttons like the existing narrow
Implementhandling, gutter control becomes a leading-edge tap target.7. Removal made necessary by this change
PlanAnnotationView.tsx+ test and theuseReviewswiring inProposePlanToolCall.tsx(
planReviews,reviewActions). Code-review uses ofuseReviews,ReviewsBanner,AttachedReviewsPanelstay;
isPlanFilePath/normalizePlanFilePathstay if still referenced.plan-content-<workspaceId>render cache: it caches plan text for flash-free reload, not review data.Execution with implement-flow
Each run is one full
implement-flowinvocation (implement → validate → remote dogfood UAT → fix → polish→ PR → Codex review loop). Runs are sequential; each later run stacks on the previous run's branch.
Operator checklist (before each run)
<user>/plan-review-<n>-<topic>(rename an auto-generated branch first),created from the previous run's branch (
mainfor Run 1). Clean, committed tree; Project Trust granted.turnCap≥ 50), objective ending at "PR has green CI and aclean Codex verdict on the exact current head, no unresolved review threads, UAT evidence recorded".
skill://implement-flow/workflow.jsin the foreground with the args below; pass this wholedocument as
planMarkdownand the run'sproblem/uatFocusverbatim;maxUatRounds: 3;dogfoodRequiredstaystruefor every run.gh stack(base = previous run's branch;Run 1 bases on
main). Scope of a run is frozen once its PR exists: review fixes only; anything elsegoes to the next run or a tracked follow-up.
CODER_URL/CODER_SESSION_TOKEN). If neither is available the dogfood round reports a blocker; do not substitute alocal server.
Shared
environmentNotes(pass to every run)Run 1 · Backend: contract, projection, visibility, snapshot hook, endpoints, plan-agent context (~530 LoC)
Branch:
<user>/plan-review-1-backendfrommain.problem: "Persist plan-review state in workspace chat history without touching thepropose_plantool: define the
<mux_plan_review>record contract and pure projection, keep record rows out ofprovider requests, compaction copies and human-turn detection, snapshot every successful
propose_planfrom the session tool-completion listener, expose
workspace.planReview.{getState,ensureSnapshot, setThreadResolved,submitFeedback}, and give the plan agent a deterministic<plan-review-state>blockplus static guidance. No UI beyond what already exists."
Scope / files:
src/common/utils/planReview/{planReviewRecord.ts,planReviewEnvelope.ts,planReviewState.ts},src/common/types/message.ts(plan-reviewmetadata variant; extendisSyntheticSnapshotUserMessage),src/common/utils/messages/modelHiddenMessages.ts,turnContextAssembler.ts:73,compactionHandler.buildPreservedTailCopies,StreamingMessageAggregator.shouldHideMessageFromTranscript,neutralizeAgentEnvelopeLookalikesForProvider.ts,src/node/builtinAgents/plan.md,agentSession.ts(completion hook +
<plan-review-state>block),workspaceService.ts(planReview*),router.ts,schemas/api.ts,src/constants/planReview.ts. Out of scope: any component change other than theaggregator predicate;
PlanAnnotationViewstays until Run 2.Tests: colocated unit tests — envelope round-trip with
</in bodies; projection ordering, idempotentresolve/reopen, dangling refs and metadata/text mismatches ignored, preserved-tail copies and duplicate
recordIds ignored,sent ≠ resolved;rollingCut/keepRecentTailtreat record rows like snapshot rows;assemblePromptPayloadoutput andbuildPreservedTailCopiesexclude record rows; aggregator hides recordrows with and without debug mode; neutralizer rewrites lookalikes in text and tool output and keeps
authentic rows.
tests/ipc/planReview.test.ts(mockAiRouter) — apropose_plancompletion appends exactlyone snapshot with the proposal's
toolCallIdand content hash; an identical second proposal appends none;an oversized plan appends none and logs;
ensureSnapshotafter an external edit creates a second snapshot;setThreadResolvedinvariants;submitFeedbackyields one user row whose envelope parses back to the samecomments/replies with backend-stamped ids, wakes the agent (captured provider request contains the
envelope) and rejects bad anchors / unknown threads / empty payloads; after a compaction boundary
getStatestill returns everything and the next plan-mode request contains<plan-review-state>but nosnapshot text; after a manual reset the block omits pre-reset threads while
getStatekeeps them.validationCommand:make static-check && bun test src/common/utils/planReview src/common/utils/compaction src/common/utils/messages src/node/utils/messages turnContextAssembler compactionHandler StreamingMessageAggregator && TEST_INTEGRATION=1 bun x jest tests/ipc/planReview.test.ts(
bun testarguments are path substrings; do not run the wholesrc/node/servicestree.)uatFocus(remote dogfood agent; record a video of the whole session and screenshot each numbered step):workspace and obtain a proposal. Evidence: screenshot of the plan card;
chat.jsonlexcerpt showing onesnapshotrow whoseproposalToolCallIdequals thepropose_plantool call id and whosecontentHashequals
sha256sumof the plan file. Ask for the same plan again → no new snapshot row.apiproxy callplanReview.getState(snapshot present), thensubmitFeedbackwith onecomment anchored to a real block and a quote. Evidence: the transcript shows a new user message (raw
envelope text is acceptable in this run), the plan agent's next request in
devtools.jsonlcontains theenvelope, and the agent revises the plan and proposes again (second snapshot row).
devtools.jsonlshows a<plan-review-state>block listing theunresolved thread;
setThreadResolved(true)→ block disappears on the next turn./compact, then another turn:getStateunchanged; the request contains no snapshot text and norecord rows; transcript shows no record rows with API debug logs off and on.
<mux_plan_review>lookalike as a normal chat message →devtools.jsonlshows itrewritten to
<user_pasted_mux_plan_review>andgetStateis unchanged.Implementon the plan card, andShow Textstillbehave as before.
Run 2 · Review-mode UI: snapshot render, comments, drafts, send feedback, feedback card (~480 LoC)
Branch:
<user>/plan-review-2-review-uifrom<user>/plan-review-1-backend.problem: "Replace the raw-diff annotate mode of thepropose_plancard with review mode on the renderedplan: pin the card to its snapshot, comment on blocks or selected text, keep drafts in component state,
send them through
planReview.submitFeedback, render the feedback message as a compact transcript card,and remove
PlanAnnotationViewand the plan wiring ofuseReviews. Resolution UI, the implement gate,historical cards and carried-over threads are Run 3."
Scope / files:
usePlanReview.ts,ProposePlan/{PlanReviewView,PlanReviewThread}.tsx,ProposePlan/splitPlanBlocks.ts,ProposePlanToolCall.tsx,keybinds.ts,UserMessage.tsx(feedbackcard); delete
PlanAnnotationView*. Thread cards show Sent status and replies but no Resolve/Reopen yet.Tests:
splitPlanBlocks(headings, nested lists, fenced code with blank lines, tables, reference links,CRLF input, trailing newline; ranges cover the document without gaps);
ProposePlanToolCall.test.tsx—review mode renders snapshot content, block comment creates a draft with the right anchor, multi-block
selection clamps to block boundaries, Send calls
submitFeedbackwith drafts and clears them, Senddisabled with zero drafts, plan-changed banner, keybinds;
UserMessagerenders the compact feedback card;tests/ui/planReview.test.tsxfull-app: seeded proposal →Shift+A→ comment → send → transcript showsthe feedback card and the composer is untouched. Story:
App.*story with review mode at desktop and apinned
phoneviewport (parameters.pixel.matrix.viewports) whose play opens review and adds a draft.validationCommand:make static-check && bun test src/browser/features/Tools src/browser/features/Messages src/browser/hooks && TEST_INTEGRATION=1 bun x jest tests/ui/planReview.test.tsx, then run the plays of the touchedApp.*stories with the Storybook recipe from the environment notes.uatFocus(video of the whole session; screenshots per step at desktop and at 375 px, drawer closed):Shift+A: the cardshows "Review", renders the same content (compare against reading mode), the TOC is hidden, and the
pinned snapshot matches
chat.jsonl.mod+enter: a Draft card appears under thatblock with the block's quote. Select text inside the table → floating "Comment" → draft with the
selected quote. Edit one draft, delete the other, re-add it.
shift+c, type,mod+enter;shift+j/shift+kmove betweencards;
mod+shift+entersends.bodies, no raw XML), the agent revises and proposes again; the new card is the latest and the previous
card still shows its own snapshot in review mode.
snapshot — Review latest" → click: new snapshot pinned, sent threads still listed on the old snapshot.
backend with the same
XUM_ROOT: identical state.Show Text,Copy,Edit,Start Here,Implementstill work; nothing plan-related appears inlocalStorage(DevTools → Application).Run 3 · Loop closure: resolve/reopen, implement gate, historical cards, carried-over threads (~260 LoC)
Branch:
<user>/plan-review-3-loopfrom<user>/plan-review-2-review-ui.problem: "Close the review loop in thepropose_plancard: Resolve/Reopen throughplanReview.setThreadResolved, confirmation beforeImplement/Continue in Autowhile drafts orunresolved threads exist, render non-latest proposals from their snapshot with their threads, and list
unresolved threads from earlier snapshots on the latest card."
Scope / files:
ProposePlanToolCall.tsx,PlanReviewThread.tsx,usePlanReview.ts.Tests: Resolve/Reopen call
setThreadResolvedand badges follow state; Implement with open threads showsthe confirm dialog and sends only after confirm (and sends immediately with none); non-latest card renders
its snapshot and threads read-only except Reply/Resolve; a new revision lists unresolved threads from the
previous snapshot and resolving one updates both cards;
tests/uiextension: full loop comment → send →revision → resolve → implement without prompt.
validationCommand: as Run 2.uatFocus(video + screenshots at desktop and 375 px):revisions (2)", reply to one thread and resolve the other; the badge flips to Resolved,
chat.jsonlhasa
resolverow, reopening flips it back with areopenrow.Resolve but no new comments.
Implementwith one unresolved thread → confirmation dialog; cancel leaves the workspace in plan mode;confirm switches to exec and sends "Implement the plan". Resolve everything →
Implementsends withouta prompt.
/compact, restart the backend with the sameXUM_ROOT, reload: identical threads, statuses andsnapshot text on both cards;
devtools.jsonlshows no record rows in the next request.Later (not part of these runs) · Agent replies per thread (~150 LoC)
Plan-agent-only tool
plan_review_reply({ threadId, body }); the backend validates the thread and appendsa
replyrecord kind (author: "agent"). Preferred over parsing envelopes from assistant prose: toolresults are backend-constructed (provenance) and thread ids are validated. Not needed for the core loop —
in v1 the agent's revision plus prose is its reply.
Acceptance criteria
propose_planunchanged, a user can comment on a block or selected text of the rendered plan,reply to sent threads, and send all drafts as one
<mux_plan_review>user message the plan agentreceives (visible in
devtools.jsonlwith API debug logs on).localStorage; after restart and after compaction, reopening the workspace shows thesame threads, quotes, resolution and snapshot text on the same cards; the compaction summary is not
consulted.
<mux_plan_review>lookalike in tool output or pasted text is neutralized and cannot create/resolve threads.Implement/Continue in Autorequire confirmation while drafts or unresolved threads exist.history rows; that build would merely send them to the model as extra user text).
UAT evidence (remote dogfood, every run)
Dogfood UAT runs remotely through Coder Agents against the pushed branch SHA (never a local server): the
remote agent follows the run's
uatFocus, records a video of the whole session, screenshots every numberedstep (desktop and 375 px where the run says so), views each PNG and inspects recording frames before
attaching, and reports pass/fail per step with the exact SHA. Artifacts land in
.mux-uat/round-<n>/of thefeature worktree (excluded from git) with the chat URL; the PR description mentions that dogfood UAT ran and
passed, and the evidence is attached to the PR with
gh pr comment --attachonly when a reviewer asks.A round that cannot start (no Coder transport) is a blocker for that run, not a reason to test locally.
Accepted trade-offs
records were considered and deferred; they would add record kinds, withdraw logic and turn-boundary
exclusions for little gain).
no automatic relocation across revisions — threads stay bound to their snapshot and appear as "from
earlier revisions" on newer cards (stored quotes make later relocation possible).
block (reading mode is unaffected).
getStatescans full history per call; add an in-memory per-workspace cache keyed by last historysequence only if measured latency exceeds ~100 ms on large workspaces.
Generated with
xum• Model:coder:anthropic/claude-fable-5-1• Thinking:xhigh• Cost:$915.24