Skip to content

🤖 feat: persist native plan review in workspace history - #4317

Open
ThomasK33 wants to merge 16 commits into
ThomasK33/plan-review-runtime-prerequisitefrom
ThomasK33/plan-review-1-backend
Open

ThomasK33 wants to merge 16 commits into
ThomasK33/plan-review-runtime-prerequisitefrom
ThomasK33/plan-review-1-backend

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 21, 2026

Copy link
Copy Markdown
Member

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_plan keeps 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

  1. Append immutable proposal snapshots and resolve/reopen records. Submit feedback through the normal user-message path. Reconstruct review state across compaction through workspace.planReview endpoints and provide deterministic unresolved-thread context in plan mode.
  2. Keep hidden records out of provider requests, auto-model routing, compaction copies and retention budgets, human-turn detection, transcript/debug display, sidebar status (bounded by the latest manual reset), title generation, branch summaries, and refinement prompts. Preserve independent review records when editing an ordinary message.
  3. Neutralize unauthenticated review/peer wrappers in history and same-turn tool inputs/results, including decoded textual SVG and JSON object keys, without rewriting stored text. Bound the complete feedback row, including schema-valid retry options.
  4. Bound snapshot capture settlement and retire abandoned captures so subsequent turns are not held behind stale work. The regular-file acquisition safeguard belongs to 🤖 fix(runtime): prevent non-regular plan files from starving filesystem workers #4339; this layer verifies its interaction with capture and proposal completion.
  5. Self-heal malformed persisted history sequences in both the review projection and ordinary transcript replay/pagination. Preserve persisted bytes unless an existing rewrite targets the row. Real HistoryService regressions cover the hidden 50-row tail and pending-user eligibility control.

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 passed make 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

  • A manual reset is a privacy floor. Direct post-reset replies can reach the model, but they do not restore pre-reset thread content to deterministic review context. The intended behavior was explained on the resolved reset thread.
  • Authentic feedback uses the approved dedicated XML user-message transport. Attachment guidance treats wrapper-like instructions in images, PDFs and other media as untrusted data; it is not pixel authentication or a demonstrated defense against multimodal prompt injection. A system-only transport would require a separate design. Follow-up owner: plan-review delivery; trigger: provider/attachment trust redesign.
  • Preexisting sidebar-status prompts can include user-pasted wrapper text, and consecutive-user merging can discard secondary inlined SVG text. These are tracked follow-ups, not claimed fixed. Owner: plan-review delivery; trigger: provider-boundary audit after this backend layer.
  • Full local-suite validation is not claimed. Earlier broad runs exposed baseline Git/environment failures and Storybook budget drift; that evidence remains retained. No unrelated repository repair, test suppression or budget rebaseline is included. Owner: plan-review delivery; trigger: the next full-suite/budget maintenance pass. Required exact-head CI remains mandatory.

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_plan card: comment on a block or
selected 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 as
append-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):

  1. No changes to the propose_plan tool (schema, result, behavior). Snapshotting is a session-level
    listener on tool completion, outside the tool.
  2. No browser persistence of review data (no localStorage / usePersistedState). Unsent comments
    live only in component state until "Send feedback".
  3. Same workspace: proposals, feedback, revisions and resolution all happen in the existing plan-agent
    conversation; no separate review workspace, no external daemon.
  4. Feedback reaches the agent as a dedicated XML-formatted user message rendered specially in the UI,
    like <mux_agent_message> / <mux_subagent_report>.

Execution model: three implement-flow runs, 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 planMarkdown to every run; each run implements
only the scope named for it and treats the Design sections as the shared contract.

Current state (verified at 6f573ea)

  • ProposePlanToolCall.tsx renders the plan with MarkdownRenderer; annotate mode (Shift+A,
    KEYBINDS.TOGGLE_PLAN_ANNOTATE) swaps to PlanAnnotationViewSelectableDiffRenderer (raw Markdown
    lines prefixed with a space, src/browser/features/Tools/PlanAnnotationView.tsx:14-43) and hides the TOC.
  • Notes go to localStorage via useReviews (src/browser/hooks/useReviews.ts:60-68), anchored by line
    numbers only; Implement / Continue in Auto send the literal "Implement the plan"
    (ProposePlanToolCall.tsx:616-625) and ignore notes.
  • propose_plan returns { 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).
  • Seams this plan builds on:
    • Envelope pattern (JSON in a root XML tag, </<\/, anchored regex parse):
      src/common/utils/agentMessageEnvelope.ts:23-46,101.
    • Provider provenance: neutralizeAgentEnvelopeLookalikesForProvider.ts:41-88, called from
      src/node/services/messagePipeline.ts:146.
    • Model exclusion of UI-only rows: keepContextRow (turnContextAssembler.ts:73-74) via
      isWorkflowDisplayOnlyMessage (src/common/utils/workflowRunMessages.ts:257-259).
    • Synthetic user rows are skipped at human-turn boundaries through one predicate,
      isSyntheticSnapshotUserMessage (src/common/types/message.ts:1467-1475), used by rollingCut.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) and workspaceGoalService.ts:806
      already skip any synthetic row.
    • Compaction tail copies: compactionHandler.buildPreservedTailCopies (compactionHandler.ts:1133-1193)
      copies every row in the tail as synthetic + rlmPreservedTailCopy, with no display-only exclusion.
    • Transcript hiding: shouldHideMessageFromTranscript (StreamingMessageAggregator.ts:3705-3711) hides
      synthetic && uiVisible !== true rows unless window.api.debugLlmRequest.
    • Live updates: hidden synthetic rows are still stored in the aggregator (addMessage, getAllMessages(),
      StreamingMessageAggregator.ts:1074,1503) and WorkspaceStore bumps subscribeKey listeners on every
      whole-message event (WorkspaceStore.ts:5275-5280).
    • Non-waking append + publish: historyService.appendToHistory (:3496) then
      session.emitChatEvent({ ...message, type: "message" }) (pattern: appendWorkflowRunInvocation,
      workspaceService.ts:11365-11445).
    • Full history read: historyService.iterateFullHistory(workspaceId, "forward"|"backward", visitor)
      (historyService.ts:2144-2148); manual reset marker: isDurableContextResetBoundaryMarker
      (compactionBoundary.ts:42-54). Read-side row limit SESSION_HISTORY_MAX_LINE_BYTES = 1 MiB
      (contextBudget.ts:55, oversized rows skipped in historyScanner.ts:774-779); no write-side limit.
    • Tool completion hook with toolCallId + result: agentSession.ts:9189-9218 (tool-call-end
      listener; propose_plan already special-cased there for sidebar refresh). ToolCallEndEvent.result
      is unknown (schemas/stream.ts:501-519).
    • Plan-mode instruction injection: turnContextAssembler.ts:262-278 (pure module; callers pass history).
    • Streamdown React tree with components/rehypePlugins, hast node.position on 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.
    • sendMessage options pass muxMetadata: z.any() through (schemas/stream.ts:936).

Design

1. Records (src/common/utils/planReview/)

Four record kinds, each one MuxMessage row with a <mux_plan_review> text part
(formatPlanReviewEnvelope, same escaping/parse strictness as agentMessageEnvelope.ts) and
metadata.muxMetadata = { type: "plan-review", kind, recordId, snapshotId?, threadId?, feedbackId? }:

// planReviewRecord.ts (zod schema + types)
type PlanReviewAnchor = { startLine: number; endLine: number }; // 1-based, inclusive, lines of the snapshot
type PlanReviewRecord =
  | { v: 1; kind: "snapshot"; snapshotId; planPath; contentHash; proposalToolCallId?: string; content: string }
  | { v: 1; kind: "feedback"; feedbackId; snapshotId; contentHash; summary?: string;
      comments: Array<{ threadId; anchor: PlanReviewAnchor; quote: string; body: string }>;   // new threads
      replies:  Array<{ replyId; threadId; body: string }> }                                   // on existing threads
  | { v: 1; kind: "resolve"; threadId }
  | { v: 1; kind: "reopen";  threadId };

Row shape:

kind role synthetic reaches model visible in transcript
snapshot, resolve, reopen user true (uiVisible unset) no no (also hidden in debug-LLM mode, since never sent)
feedback user no (real user message) yes yes, as a compact "Plan feedback · N comments" card

Invariants (checked by the backend before append, re-asserted by the projection):

  • feedback.comments[].anchor within 1..lineCount(snapshot), body non-empty; replies[].threadId
    references an existing thread; resolve/reopen reference an existing thread and are idempotent.
  • snapshot.contentHash = sha256(content); snapshots are deduplicated by hash per workspace.
  • Rows are never edited or deleted; state is a replay in historySequence order.
  • Rows with rlmPreservedTailCopy or a recordId already 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 text
parses as an envelope, and parsed kind/recordId/threadId/snapshotId/feedbackId match the metadata
(mirrors getValidAgentPeerMessage). Unknown kind/v, mismatches and dangling references are skipped
with log.debug (self-healing).

interface PlanReviewState {
  snapshots: Array<{ snapshotId; planPath; contentHash; proposalToolCallId?; content; historySequence }>;
  threads:   Array<{ threadId; snapshotId; anchor; quote; body; feedbackId; historySequence;
                     replies: Array<{ replyId; author: "user" | "agent"; body; historySequence }>;
                     resolved: boolean }>;
  feedbacks: Array<{ feedbackId; snapshotId; threadIds: string[]; historySequence }>;
}

A thread exists only once it was sent; resolved is the last resolve/reopen record.
Sent never implies resolved; only the user resolves.

3. Model visibility and turn-boundary safety

  • isPlanReviewRecordMessage(m) = muxMetadata.type === "plan-review" && kind !== "feedback".
  • New isModelHiddenMessage(m) (src/common/utils/messages/modelHiddenMessages.ts) =
    isWorkflowDisplayOnlyMessage(m) || isPlanReviewRecordMessage(m); used in keepContextRow
    (turnContextAssembler.ts:73) and in buildPreservedTailCopies so record rows are neither sent nor
    copied across compaction boundaries. Audit the compaction request builders (compactionHandler.ts,
    continuousCompactor.ts) and context-usage stats for the same filter.
  • Extend the body of isSyntheticSnapshotUserMessage with || isPlanReviewRecordMessage(message) (with a
    comment: 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.
  • shouldHideMessageFromTranscript additionally hides isPlanReviewRecordMessage rows even when
    debugLlmRequest is on (that mode shows what the model sees; these rows are not sent).
  • Neutralizer: rewrite unauthenticated <mux_plan_review> to <user_pasted_mux_plan_review> in text and
    tool parts; authentic = role: "user", muxMetadata.type === "plan-review", envelope cross-checks.
  • Deterministic review context for the plan agent: when effectiveMode === "plan" and unresolved threads
    exist, the assemblePromptPayload caller (AgentSession, which owns historyService) derives the state
    over full history after the latest durable manual reset marker (iterateFullHistory(..., "backward"),
    stop at isDurableContextResetBoundaryMarker) and adds a <plan-review-state> block to
    additionalSystemInstructions: 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).
  • Static guidance in 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_plan again, never claim a thread resolved.

4. Snapshots

  • Automatic: in the tool-call-end listener (agentSession.ts:9189-9218), when
    toolName === "propose_plan" and result.success === true, read the plan file through the workspace
    runtime, hash it, and — unless a snapshot with that hash exists — append a snapshot row with
    proposalToolCallId = payload.toolCallId (non-waking append + emitChatEvent). propose_plan ends the
    planning 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 the tool-call-end dispatch or
    affect the tool result. Content is CRLF-normalized (\r\n\n) before hashing and storing so anchor
    line numbers map 1:1 across platforms. Skip (with log.warn) when content exceeds
    MAX_PLAN_SNAPSHOT_BYTES = 256 KiB (src/constants/), well under the 1 MiB read-side limit.
  • On demand (planReview.ensureSnapshot): same routine, used when a card has no snapshot (history
    written before this feature, skipped/failed hook, ephemeral plan-display previews) and for
    "Review latest" after an external edit. Dedup by hash makes it idempotent.

5. Backend API (WorkspaceService.planReview* + oRPC workspace.planReview.*)

Schemas in src/common/orpc/schemas/api.ts, handlers in router.ts (mirror workspace.replaceChatHistory,
router.ts:1759-1767). Every mutation returns the fresh PlanReviewState; the UI never composes envelopes.

Endpoint Behavior
getState({ workspaceId }) iterateFullHistoryderivePlanReviewState.
ensureSnapshot({ workspaceId, proposalToolCallId? }) See §4; returns { snapshotId, contentHash, state }. Typed errors: plan_missing, plan_too_large.
setThreadResolved({ workspaceId, threadId, resolved }) Validate the thread exists; no-op if already in that state; else append resolve/reopen. Typed error: unknown_thread.
submitFeedback({ workspaceId, snapshotId, summary?, comments: [{anchor, quote, body}], replies: [{threadId, body}], options }) Validate anchors/threads against the projection, stamp feedbackId/threadIds/replyIds, build the feedback envelope, and send it as the user message through the existing sendMessage path with options (agent/model/thinking from the UI, as Implement does) plus stamped muxMetadata. 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 two
windows cannot both resolve/reopen inconsistently.

6. Frontend

usePlanReview(workspaceId) (src/browser/hooks/usePlanReview.ts): fetches getState on mount,
applies the state returned by each mutation, and refetches when a new plan-review row shows up in
aggregator.getAllMessages() (observed through WorkspaceStore.subscribeKey — covers other windows, the
completion 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 (replaces PlanAnnotationView):

  • Shift+A toggles review mode (label "Review"). The card binds to the snapshot whose
    proposalToolCallId matches its tool call (latest or not); if none, it calls ensureSnapshot. Review
    mode renders the snapshot content, so anchors are stable. If the live plan file differs from the
    snapshot (getPlanContent already re-fetches on focus), show "Plan changed since this snapshot — Review
    latest" which creates a new snapshot; existing threads stay bound to their snapshot.
  • PlanReviewView (src/browser/features/Tools/ProposePlan/PlanReviewView.tsx) renders the snapshot as
    top-level Markdown blocks (splitPlanBlocks.ts: remark-parse + remark-gfm, node.position
    start/end lines; link reference definitions are appended to every block so [text][ref] still resolves)
    with one MarkdownRenderer per block and thread cards / composer interleaved as React siblings right
    after 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.
  • Creating a comment: hover/focus a block (blocks are focusable in review mode) → gutter comment control →
    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] so
    anchors never split a block; quote = Selection.toString() trimmed, capped at 500 chars, else the
    block's first 200 chars. Drafts render as cards with a "Draft" badge and Edit/Delete.
  • PlanReviewThread.tsx (visual language of InlineReviewNote): badge (Draft / Sent / Resolved), quote,
    body, replies (author label), actions Reply (adds a draft reply) / Resolve / Reopen. Bodies are plain text.
  • Card footer in review mode: "Send feedback (N)" primary while drafts exist, optional summary field,
    "Open from earlier revisions (M)" collapsible listing unresolved threads bound to other snapshots with
    Reply/Resolve.
  • Implement gate: Implement / Continue in Auto call useConfirmDialog().confirm when drafts or
    unresolved threads exist ("Implement with open review comments? N unresolved, M unsent").
  • Non-latest cards with a snapshot render it (review mode allows Reply/Resolve, no new comments) instead
    of *Plan saved to…*.
  • Transcript: feedback rows render in UserMessage.tsx as a compact card "Plan feedback · N comments"
    (expandable quote + body list); the raw envelope shows only if parsing fails.
  • Keyboard (KEYBINDS, hidden on mobile): comment on focused block/selection shift+c, next/prev thread
    shift+j/shift+k, send feedback mod+shift+enter (review mode only); composer mod+enter / esc.
  • Narrow layout (~375 px): cards and composer full-width under the block, footer actions collapse to icon
    buttons like the existing narrow Implement handling, gutter control becomes a leading-edge tap target.

7. Removal made necessary by this change

  • Delete PlanAnnotationView.tsx + test and the useReviews wiring in ProposePlanToolCall.tsx
    (planReviews, reviewActions). Code-review uses of useReviews, ReviewsBanner, AttachedReviewsPanel
    stay; isPlanFilePath/normalizePlanFilePath stay if still referenced.
  • Keep the 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-flow invocation (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)

  1. Worktree on its final branch <user>/plan-review-<n>-<topic> (rename an auto-generated branch first),
    created from the previous run's branch (main for Run 1). Clean, committed tree; Project Trust granted.
  2. One durable goal spanning the whole run (turnCap ≥ 50), objective ending at "PR has green CI and a
    clean Codex verdict on the exact current head, no unresolved review threads, UAT evidence recorded".
  3. Invoke skill://implement-flow/workflow.js in the foreground with the args below; pass this whole
    document as planMarkdown and the run's problem / uatFocus verbatim; maxUatRounds: 3;
    dogfoodRequired stays true for every run.
  4. After the PR exists, link it into the native stack with 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 else
    goes to the next run or a tracked follow-up.
  5. Remote UAT transport: authenticated Coder MCP chat tools (fallback: curl with CODER_URL /
    CODER_SESSION_TOKEN). If neither is available the dogfood round reports a blocker; do not substitute a
    local server.

Shared environmentNotes (pass to every run)

- Package manager: bun only (`bun@1.3.5`, see package.json `packageManager`). `make` targets are the source
  of truth: `make static-check` (lint, typecheck, fmt-check, shellcheck), `make typecheck`.
- Unit tests live next to sources and run with `bun test <path...>`. Integration tests live under `tests/`
  and run with `TEST_INTEGRATION=1 bun x jest <path>` (`tests/ipc` = backend via IPC, `tests/ui` = full
  app in happy-dom via `AppLoader`). Storybook plays (only full-app `App.*.stories.tsx` stories):
  `bun x storybook build -o "$HOME/<owned-dir>/storybook-static"` (keep it outside the checkout), serve it
  with `bun x http-server <dir> -a 127.0.0.1 -p <free-port> -s`, then
  `bun x test-storybook --testTimeout 30000 --url http://127.0.0.1:<port> <story path patterns>`; add
  `--verbose` to confirm each play ran. Story plays run at desktop size: force narrow width with a
  fixed-width wrapper when a play asserts phone layout.
- If a broad `bun test` run crashes in wasm frames (`wasm_trampoline_wasm_ipint_call_wide32`) with no
  assertion failures, rerun that invocation with `BUN_JSC_useWasmIPInt=0`; never persist that into
  Makefile/CI.
- `lint-shellcheck` scans every `*.sh` in the tree, including ignored dirs: keep helper scripts outside
  the checkout (or name them `.sh.txt`).
- Dev server: `make dev-server-sandbox` creates a fresh `XUM_ROOT` and free ports and prints the Vite URL;
  `KEEP_SANDBOX=1` keeps the root; `DEV_SERVER_SANDBOX_ARGS="--clean-providers --clean-projects"` starts
  empty. Restart the same instance with `make dev-server` and the same `XUM_ROOT`/ports env. One backend
  per `XUM_ROOT` (lockfile).
- A clean sandbox still defaults to a real model and fails with `api_key_not_found`: put a `providers.jsonc`
  in `XUM_ROOT` (real credentials from the workspace env if present, otherwise a loopback OpenAI-compatible
  SSE fixture registered as an `openai` provider with a dummy key and `baseUrl: http://127.0.0.1:<port>/v1`)
  and select that model in the workspace. For plan-review UAT the fixture scripts the plan agent: turn 1 →
  `file_edit_insert` the plan file + `propose_plan`; any turn whose latest user message contains
  `<mux_plan_review>` → `file_edit_replace_string` edits addressing the quotes + `propose_plan`.
- Enable Settings → API debug logs to get `<XUM_ROOT>/sessions/<workspace>/devtools.jsonl` (raw provider
  requests). History is `<XUM_ROOT>/sessions/<workspace>/chat.jsonl` (+ `chat.archive.jsonl` after
  compaction). Backend endpoints can be exercised through the built CLI's `api` proxy with `XUM_SERVER_URL`.
- agent-browser: close only sessions you own (`--session <name>`), never `close --all`. At phone widths the
  sidebar becomes a drawer that covers the transcript — dismiss it before screenshots. Programmatic scrolls
  re-pin to the transcript bottom within ~2 s; use keyboard `PageUp`/`Home`. View every PNG before attaching
  and pixel-compare "different" screenshots; use new filenames for re-captures; extract recording frames
  with `ffmpeg -vf fps=1` and inspect them.
- Repo rules that reviews enforce: React Compiler is on (no manual memo/useMemo/useCallback); consult the
  `react-effects` skill before adding effects; never call `localStorage` directly; shared `Tooltip`
  components, no native `title`; lucide icons, no emoji; colors via CSS variables; tool input schemas use
  `.nullish()`; no `void asyncFn()`; static imports only; `git mv` for moves; leave rationale as code
  comments; no tautological tests.

Run 1 · Backend: contract, projection, visibility, snapshot hook, endpoints, plan-agent context (~530 LoC)

Branch: <user>/plan-review-1-backend from main.

problem: "Persist plan-review state in workspace chat history without touching the propose_plan
tool: define the <mux_plan_review> record contract and pure projection, keep record rows out of
provider requests, compaction copies and human-turn detection, snapshot every successful propose_plan
from the session tool-completion listener, expose workspace.planReview.{getState,ensureSnapshot, setThreadResolved,submitFeedback}, and give the plan agent a deterministic <plan-review-state> block
plus 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-review metadata variant; extend isSyntheticSnapshotUserMessage),
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 the
aggregator predicate; PlanAnnotationView stays until Run 2.

Tests: colocated unit tests — envelope round-trip with </ in bodies; projection ordering, idempotent
resolve/reopen, dangling refs and metadata/text mismatches ignored, preserved-tail copies and duplicate
recordIds ignored, sent ≠ resolved; rollingCut/keepRecentTail treat record rows like snapshot rows;
assemblePromptPayload output and buildPreservedTailCopies exclude record rows; aggregator hides record
rows with and without debug mode; neutralizer rewrites lookalikes in text and tool output and keeps
authentic rows. tests/ipc/planReview.test.ts (mockAiRouter) — a propose_plan completion appends exactly
one snapshot with the proposal's toolCallId and content hash; an identical second proposal appends none;
an oversized plan appends none and logs; ensureSnapshot after an external edit creates a second snapshot;
setThreadResolved invariants; submitFeedback yields one user row whose envelope parses back to the same
comments/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
getState still returns everything and the next plan-mode request contains <plan-review-state> but no
snapshot text; after a manual reset the block omits pre-reset threads while getState keeps 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 test arguments are path substrings; do not run the whole src/node/services tree.)

uatFocus (remote dogfood agent; record a video of the whole session and screenshot each numbered step):

  1. Sandbox at the pushed SHA with the fixture/provider from the environment notes; create a plan-mode
    workspace and obtain a proposal. Evidence: screenshot of the plan card; chat.jsonl excerpt showing one
    snapshot row whose proposalToolCallId equals the propose_plan tool call id and whose contentHash
    equals sha256sum of the plan file. Ask for the same plan again → no new snapshot row.
  2. Via the CLI api proxy call planReview.getState (snapshot present), then submitFeedback with one
    comment 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.jsonl contains the
    envelope, and the agent revises the plan and proposes again (second snapshot row).
  3. Send any follow-up in plan mode → devtools.jsonl shows a <plan-review-state> block listing the
    unresolved thread; setThreadResolved(true) → block disappears on the next turn.
  4. /compact, then another turn: getState unchanged; the request contains no snapshot text and no
    record rows; transcript shows no record rows with API debug logs off and on.
  5. Paste a hand-written <mux_plan_review> lookalike as a normal chat message → devtools.jsonl shows it
    rewritten to <user_pasted_mux_plan_review> and getState is unchanged.
  6. Regression: peer-message and sub-agent report cards, Implement on the plan card, and Show Text still
    behave as before.

Run 2 · Review-mode UI: snapshot render, comments, drafts, send feedback, feedback card (~480 LoC)

Branch: <user>/plan-review-2-review-ui from <user>/plan-review-1-backend.

problem: "Replace the raw-diff annotate mode of the propose_plan card with review mode on the rendered
plan: 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 PlanAnnotationView and the plan wiring of useReviews. 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 (feedback
card); 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 submitFeedback with drafts and clears them, Send
disabled with zero drafts, plan-changed banner, keybinds; UserMessage renders the compact feedback card;
tests/ui/planReview.test.tsx full-app: seeded proposal → Shift+A → comment → send → transcript shows
the feedback card and the composer is untouched. Story: App.* story with review mode at desktop and a
pinned phone viewport (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 touched App.* 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):

  1. Obtain a proposal containing headings, a table, a nested list and a fenced block. Shift+A: the card
    shows "Review", renders the same content (compare against reading mode), the TOC is hidden, and the
    pinned snapshot matches chat.jsonl.
  2. Hover/focus a paragraph → gutter control → composer → mod+enter: a Draft card appears under that
    block 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.
  3. Keyboard-only path: focus a block, shift+c, type, mod+enter; shift+j/shift+k move between
    cards; mod+shift+enter sends.
  4. "Send feedback (2)": drafts clear, the transcript shows "Plan feedback · 2 comments" (expand: quotes and
    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.
  5. Edit the plan file on disk from a shell while review mode is open → banner "Plan changed since this
    snapshot — Review latest" → click: new snapshot pinned, sent threads still listed on the old snapshot.
  6. Reload the page with an unsent draft: the draft is gone (expected), sent threads persist. Restart the
    backend with the same XUM_ROOT: identical state.
  7. 375 px: repeat steps 1–4; no horizontal overflow, cards full-width, footer actions collapsed to icons.
  8. Regression: reading mode unchanged (single renderer), Show Text, Copy, Edit, Start Here,
    Implement still work; nothing plan-related appears in localStorage (DevTools → Application).

Run 3 · Loop closure: resolve/reopen, implement gate, historical cards, carried-over threads (~260 LoC)

Branch: <user>/plan-review-3-loop from <user>/plan-review-2-review-ui.

problem: "Close the review loop in the propose_plan card: Resolve/Reopen through
planReview.setThreadResolved, confirmation before Implement / Continue in Auto while drafts or
unresolved 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 setThreadResolved and badges follow state; Implement with open threads shows
the 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/ui extension: full loop comment → send →
revision → resolve → implement without prompt.

validationCommand: as Run 2.

uatFocus (video + screenshots at desktop and 375 px):

  1. From a workspace with two proposals and two sent threads: on the latest card open "Open from earlier
    revisions (2)", reply to one thread and resolve the other; the badge flips to Resolved, chat.jsonl has
    a resolve row, reopening flips it back with a reopen row.
  2. Open the first (non-latest) card: it shows the originally proposed text with its threads and allows
    Resolve but no new comments.
  3. Implement with one unresolved thread → confirmation dialog; cancel leaves the workspace in plan mode;
    confirm switches to exec and sends "Implement the plan". Resolve everything → Implement sends without
    a prompt.
  4. /compact, restart the backend with the same XUM_ROOT, reload: identical threads, statuses and
    snapshot text on both cards; devtools.jsonl shows no record rows in the next request.
  5. Regression pass of the Run 2 scenarios at 375 px.

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 appends
a reply record kind (author: "agent"). Preferred over parsing envelopes from assistant prose: tool
results 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

  1. With propose_plan unchanged, 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 agent
    receives (visible in devtools.jsonl with API debug logs on).
  2. No review data in localStorage; after restart and after compaction, reopening the workspace shows the
    same threads, quotes, resolution and snapshot text on the same cards; the compaction summary is not
    consulted.
  3. Every completed proposal has a snapshot; older cards render the plan as proposed, with their threads.
  4. Sending never resolves a thread; only Resolve does.
  5. Record rows never appear in provider requests, compaction copies or human-turn detection; a
    <mux_plan_review> lookalike in tool output or pasted text is neutralized and cannot create/resolve threads.
  6. Implement / Continue in Auto require confirmation while drafts or unresolved threads exist.
  7. Works at ~375 px without right-edge overflow; every action has a keyboard path.
  8. Downgrade to a build without this feature keeps the workspace usable (rows are ordinary synthetic
    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 numbered
step (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 the
feature 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 --attach only 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

  • Unsent drafts are component state: a reload before "Send feedback" loses them (backend-persisted draft
    records were considered and deferred; they would add record kinds, withdraw logic and turn-boundary
    exclusions for little gain).
  • Anchors are top-level block ranges + quote; nested list items / table rows anchor to the parent block;
    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).
  • Every proposal stores the plan text in history (typically 5–50 KB, deduplicated by hash, capped at 256 KiB).
  • Review mode renders one Streamdown instance per top-level block; footnotes spanning blocks render per
    block (reading mode is unaffected).
  • Comment bodies are plain text; agent per-thread replies are a later follow-up (not part of the three runs).
  • getState scans full history per call; add an in-memory per-workspace cache keyed by last history
    sequence 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

@mintlify

mintlify Bot commented Sep 21, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Mux 🟢 Ready View Preview Sep 22, 2026, 7:07 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T20:37:24.803417Z a4843fa New commits
🔒 Security Review Completed 2026-09-22T20:29:42.847399Z a4843fa New commits

Security findings

Advisory findings (2)

ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/planReviewService.ts
Comment thread src/common/utils/planReview/planReviewState.ts
Comment thread src/node/services/agentSession.ts
Comment thread src/node/services/continuousCompactor.ts
Comment thread src/node/services/agentStatusService.ts Outdated
Comment thread src/node/services/workspaceService.ts
Comment thread src/common/utils/planReview/planReviewEnvelope.ts Outdated
Comment thread src/common/utils/planReview/planReviewRecord.ts Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/planReviewService.ts
Comment thread src/common/utils/planReview/planReviewState.ts Outdated
Comment thread src/common/utils/planReview/planReviewEnvelope.ts Outdated
Comment thread src/node/services/agentSession.ts Outdated
Comment thread src/common/orpc/schemas/api.ts Outdated
Comment thread src/common/utils/planReview/planReviewState.ts Outdated
Comment thread src/node/services/planReviewService.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ 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.

Comment thread src/node/services/streamManager.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/agentSession.ts Outdated
Comment thread src/node/services/planReviewService.ts
Comment thread src/common/types/message.ts
Comment thread src/node/services/planReviewService.ts
@ThomasK33

Copy link
Copy Markdown
Member Author

Not-ready checkpoint — review limit reached

Head: 802ecd3466ce488c7ab9fa75f7174a9265fa616a.

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 .mux-uat/round-6/. These results do not cover or dismiss the new findings below.

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:

  1. 4065820771 — confirmed: an abandoned capture remains in the pending set; a subsequent completed turn waits the full deadline again. A healthy fast successor capture was not lost in the reproduction. Proposed fix: detach aborted captures from settlement tracking, preserving the existing signal-based late-append refusal.
  2. 4065820788 — code-confirmed, no end-to-end reproduction: feedback size measurement excludes persisted retry options. Proposed fix: measure the actual options-bearing row rather than assume fixed headroom.
  3. 4065820795 — reproduced in both neutralizer entry points: deeply nested JSON overflows recursive traversal. Proposed fix: iterative traversal in the existing helper. A complete provider round-trip was not exercised in this triage.
  4. 4065820811 — reproduced: editing an ordinary message selects the preceding independent resolve record for truncation. Proposed fix: distinguish durable review mutations from request-owned snapshot preludes in the edit walk.
  5. 4065820819 — code-confirmed, not executed: raw last-user checks in idle compaction and heartbeat treat hidden review rows as unanswered prompts. Proposed fix: select the last relevant conversational row for those checks.
  6. 4065812832 — threat, not a demonstrated exploit: text neutralization cannot authenticate instructions rendered in image/PDF pixels. No command execution or exfiltration was demonstrated. System-only feedback transport would change the accepted dedicated-user-message design; attachment trust wording can be assessed separately without claiming it eliminates multimodal prompt injection.

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 xum • Model: coder:bedrock-mantle-us-west-2/openai.gpt-6-astra • Thinking: xhigh

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/planReviewService.ts
Comment thread src/common/types/message.ts
Comment thread src/node/services/idleCompactionService.test.ts Outdated
Comment thread src/common/utils/planReview/planReviewState.ts Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

🤖 Not-ready checkpoint — extended review allowance exhausted

Head: b207292f7962cd52fd11e16e08a09fa0fe30fd0d.

The five authorized repairs were pushed after parent Bun 1.3.5 validation passed: make static-check, 864 backend unit tests, and 15 IPC tests. The broader local unit run remains non-green with the exact-base/environment failures documented in the description; it is not waived or relabeled as passing.

The final automatic code/security pair has completed: 8/8 combined assessments. Four code findings were triaged read-only:

  1. 4069303613: resolved as the accepted reset privacy floor. A post-reset reply is sent directly to the model; the deterministic block does not restore its pre-reset thread. Guaranteed loss through compaction was not demonstrated.
  2. 4069303620: confirmed RLM tail starvation from counting hidden snapshots. Remains open.
  3. 4069303621: confirmed test-evidence gap from mocking HistoryService reads. Remains open; this is not a demonstrated standalone runtime P1 defect.
  4. 4069303628: confirmed output-schema failure from a malformed persisted historySequence. Remains open.

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 xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $235.32

@ThomasK33

ThomasK33 commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

🤖 UAT round 7 complete — no overall PASS; backend remains on hold

Tested exact head: b207292f7962cd52fd11e16e08a09fa0fe30fd0d. Remote chat and raw artifacts. Fresh checkout, frozen lockfile, Bun 1.3.5, backend started after the build; loopback provider fixture only. Main testing settled at 08:22Z; one evidence-only follow-up settled at 08:35Z.

Scenario Assessed result
Oversized feedback send options PASS: both 1 MiB additional instructions and schema-valid oversized provider options return feedback_too_large, with no new row or provider request.
Editing after an idle resolve/snapshot PASS: independent review rows survive; projected review state is unchanged. The initial Enter attempt showed “History changed”; the later Send click applied the edit.
Hidden-tail idle-compaction eligibility PASS for exercised eligibility: a 52-row hidden tail no longer prevents compaction. Timestamps were backdated offline before restart. The pending-user protective branch is not live-verified: Stop left an empty assistant row, so its intended precondition was absent. Heartbeat remains unit-only.
Deep tool payload Helper passes; live scenario fails. The leaf wrapper is neutralized, but AI SDK cloneModelMessage still overflows at depth 2100/3000. The UI mislabels this CONTEXT_EXCEEDED; the persisted deep row subsequently breaks debug capture, stream-end cleanup and compaction publication. SDK frames identify the failing code, not proven pre-existing causality: no baseline commit was executed.
Stalled plan capture Partial, with a failed live usability check. Deadline abandonment, late-result refusal, a healthy successor capture, and Stop followed by a 54 ms turn passed. With a FIFO read still blocked, however, later sends and workspace RPCs stalled until it was fed, about six minutes later. Threadpool exhaustion versus another awaited read was not isolated; this is not dismissed as a harmless probe artifact.
Smoke/trust wording Authentic feedback/context still works. Raw feedback envelopes remain visible user messages until the UI layer. Attachment-trust wording is guidance only; no vision-model protection claim.

Readiness and next boundary

The 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. Codex Comments and the Required aggregate fail; this is not a ready PR. Full local-unit limitations remain documented in the description.

Isolation, cleanup and retained evidence

Two audit passes found zero non-chat/workspace leak rows. Chat tooling first attempted a coder workspace, then provisioned a coder-prototypes fallback before the remote agent's first command; the failed workspace still exists. Neither runner nor remote agent performed a shared-deployment lifecycle mutation.

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: .mux-uat/round-7/README.md, 22 attachments plus 25 recovered archive files, hashes, raw provider/history/CLI evidence, video frames and audit receipts. Original failed attempts are retained. The videos below are live UI segments, not proof that every CLI setup command was recorded. A/E are downscaled re-encodes with original durations; all others are original attached recordings. Tutorial popovers obscure parts of some screenshots; backend claims use raw receipts, not those obscured images.

Observed deep-payload failure

Deep tool input aborts with stack overflow, displayed as CONTEXT_EXCEEDED

Deep-payload live failure recording

round7-stepC.webm
Remaining scenario recordings and screenshot overview

Screenshot overview (includes failed attempts and obscured tutorial states):
Round 7 screenshot overview, including failure states

A — blocked-read stall, then feed and recovery (downscaled; unchanged duration).

Blocked-read recording

round7-stepA-compressed.webm

A5 — Stop variant.

Stop-variant recording

round7-stepA5.webm

B — feedback limits and normal submission.

Feedback-limits recording

round7-stepB.webm

D — UI message edit; persisted-state proof is in the raw artifacts.

Message-edit recording

round7-stepD.webm

E — idle-compaction scenario (offline backdating; downscaled, unchanged duration).

Idle-compaction recording

round7-stepE-compressed.webm

S — smoke observations.

Smoke recording

round7-stepS.webm

Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $248.54

@ThomasK33

Copy link
Copy Markdown
Member Author

The authorized repair work is active again. Nothing is ready to merge yet.

  1. Runtime and tool-payload safety repairs are separated into prerequisite PRs 🤖 fix(stream): reject excessively nested tool inputs before SDK parsing #4336, 🤖 fix(history): recover excessively nested persisted tool payloads #4337, and 🤖 fix(runtime): prevent non-regular plan files from starving filesystem workers #4339, linked in native stack #4340. This PR remains the backend plan-review layer; Runs 2–3 are not included.
  2. The three outstanding findings here have been repaired locally. The rebased feature head 1acf81b29961e099499e2e044bee072bfc119f85 is not yet published. Local pinned-toolchain checks passed: make static-check, 819 contract/compaction/message tests, 78 routing/replay tests, and 28 native Node IPC tests.
  3. The prerequisite review found two more guard-coverage gaps: non-v4 provider models and persisted nested-tool-call payloads. Those fixes are being made in their owning prerequisite PRs before propagating the final stack. 🤖 fix(runtime): prevent non-regular plan files from starving filesystem workers #4339 has completed code/security reviews without findings on its current head.
  4. After propagation and fresh exact-head checks, I will update this PR, reply to its three repaired threads, and run independent remote Coder Agents UAT with screenshots, recordings, and raw request evidence. Earlier failed UAT remains retained and does not count as acceptance of the new head.

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 xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $474.36

ThomasK33 added a commit that referenced this pull request Sep 22, 2026
…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 -->
@ThomasK33
ThomasK33 force-pushed the ThomasK33/plan-review-1-backend branch from b207292 to 17ef6d3 Compare September 22, 2026 16:01
@ThomasK33
ThomasK33 changed the base branch from main to ThomasK33/plan-review-runtime-prerequisite September 22, 2026 16:01
@ThomasK33
ThomasK33 added this pull request to stack #4340 September 22, 2026 16:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/common/utils/messages/modelHiddenMessages.ts
Comment thread src/common/utils/planReview/planReviewState.ts Outdated
…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 -->

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/workspaceService.ts
Comment thread src/node/services/planReviewService.ts
Comment thread src/node/services/agentStatusService.ts
Comment thread src/node/utils/messages/neutralizeAgentEnvelopeLookalikesForProvider.ts Outdated
@ThomasK33

ThomasK33 commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

Blocked checkpoint — final head e891bcf

The repair cycle is paused at e891bcf9f2083cafce2044071b4a9246371e8d55. Nothing is merged. Required acceptance is unavailable, and the latest findings need behavioral verification before another coherent repair batch.

  1. Four final-head findings remain open:
    • 4074399256: the sidebar status scan may cross the latest manual-reset boundary.
    • 4074399269: protocol tags in JSON property keys may bypass neutralization.
    • 4074399238: feedback preparation and durable send admission may race with history replacement/truncation.
    • 4074399246: a long accepted thread identifier may make a resolve/reopen row exceed the history scanner limit.
      Source inspection supports the reset-boundary and row-size concerns. These four claims have not yet been reproduced behaviorally in this checkpoint. None is being declared fixed or deferred merely because similar baseline code exists or replay skips dangling rows.
  2. Final-head local validation passed: make static-check, 703 touched-surface tests, 821 contract tests, 429 prior regressions, and 36 native-Node IPC cases (per-run counts overlap). All functional build/test jobs in feature CI passed. Security review completed clean; code review and the Codex gate remain blocked. The native Codex-check log shows an immediate failure while reviews and previous findings were pending, not a demonstrated timeout.
  3. Prerequisite 🤖 fix(runtime): prevent non-regular plan files from starving filesystem workers #4339 has an unexplained Linux E2E failure: streamingBehavior.spec.ts timed out waiting for the Send message button to be enabled, including the test retry. This is not established as a flake. C1 🤖 fix(stream): reject excessively nested tool inputs before SDK parsing #4336 and C2 🤖 fix(history): recover excessively nested persisted tool payloads #4337 have clean code/security reviews and passing required CI.
  4. Remote UAT round 8 verified the exact head, frozen dependencies, build provenance and isolation, but ran zero acceptance scenarios on this final head. Unexpected same-user audit activity outside the runner's task tree remains unattributed. Setup and predecessor captures are not acceptance evidence. Round 8 has now closed BLOCKED. The owned backend, fixture and browser were stopped, their ports were verified free, and all watchers were stopped. The chat and evidence remain inspectable; shared workspace lifecycle was left to deployment policy. The caller verified 28 local artifact-manifest entries. A further unattributed credential audit event was found during closeout and escalated privately; no shared-deployment restoration was attempted. No acceptance scenario ran on the final head.

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 sendMessage on speculation. Any source change requires fresh exact-head validation, review and remote UAT. No additional review request, push or merge is being issued from this checkpoint.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $855.07

…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 -->

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/common/orpc/schemas/api.ts Outdated
Comment thread src/node/services/agentSession.planReviewSnapshot.test.ts
Comment thread src/node/services/planReviewService.ts
…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 -->

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/common/utils/planReview/planReviewState.ts Outdated
Comment thread src/node/services/contextManagement/compactionRequests.ts
…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 -->

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +391 to +392
const maxRowBytes = SESSION_HISTORY_MAX_LINE_BYTES - PLAN_REVIEW_FEEDBACK_ROW_HEADROOM_BYTES;
if (rowBytes > maxRowBytes) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +273 to +274
const appended = await deps.historyService.appendDerivedFromFullHistory(
args.workspaceId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +1498 to +1503
// 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@ThomasK33

Copy link
Copy Markdown
Member Author

Paused at a4843fa — not merge-ready

I am stopping this repair cycle here rather than starting another batch, because the review loop is not converging: this phase's code reviews produced 3 → 4 → 3 → 2 → 3 findings, and every round surfaces another history consumer that must treat hidden plan-review rows as non-turns. The three latest findings remain open on purpose:

  • 4076231899: the on-send auto-compaction request row embeds the feedback envelope and is not measured against the history row limit.
  • 4076231912: a resolve/reopen row appended between a compaction summary and its pending follow-up may strand the follow-up.
  • 4076231924: workspace-turn recovery may treat a hidden review row as a superseding prompt.

None of the three has been reproduced yet; severity labels are not behavioral proof, and the earlier oversized-row claim failed on reader semantics, so each needs its own test before any change.

State of this head: security review clean; all functional CI jobs pass except Test / E2E (linux) (streamingBehavior.spec.ts:14, Send button disabled at ui.ts:217, same signature as the failure seen today on a merge-queue branch with none of this stack's code and on #4339 before its rerun passed). Codex Comments/Required are red because of the open threads. #4336, #4337 and #4339 have clean reviews and passing required CI.

What I recommend next, pending direction: a bounded design reassessment of the conversation-projection contract (which readers of raw history must apply the shared model-hidden predicate, and where a projection helper should exist) instead of continuing to patch consumers one review at a time. Changing HistoryService reads to filter by default is not proposed: hidden rows are still lifecycle-relevant for replay, recovery and reset/compaction fences. The auto-compaction persistence finding is independent of that contract and needs its own measurement.

Final-head remote UAT remains separately blocked on attribution of the two unexplained audit events. Lifetime external assessments on this PR: 18.


Generated with xum • Model: coder:anthropic/claude-fable-5-1 • Thinking: xhigh • Cost: $916.94

This branch was successfully deployed

1 active (outdated) deployment
staging - docs e891bcf9 Deployed Sep 22, 2026 by mintlify[bot]
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