Repository navigation
fix(studio): unify verdict ledger and guard late side-thread verdicts - #2143
Merged
Merged
Conversation
kantord
marked this pull request as ready for review
October 7, 2026 11:15
peppescg
approved these changes
Oct 7, 2026
peppescg
left a comment
Contributor
There was a problem hiding this comment.
LGTM. No blockers. I checked these points:
VerdictLedger: the move from three Sets to one phase is equivalent on every path I traced (success: inFlight → acknowledged; throw: uncertain). The one behavior change is thatacknowledgednow blocks inapprovalDisabled, where the old code only blocked on inFlight/uncertain. The ask is already retracted and filtered at that point, so the stricter rule is fine.- Late-verdict guard: ledger writes stay unconditional and the keys include
sessionId, so a late ack can't poison the successor session. ResettingcontrolPendingon session change closes the lock-up. chat-escape.ts:expiryis only set while armed, so it's safe fordisarm()to clear it unconditionally. Removing thefocusinbranch is behavior-preserving, andlatest.current.onHintChangeis equivalent to the oldoptions.onHintChange.plan-continuation.ts: on deadline,AbortSignal.any+timeoutgives the sameuncertainresult. The!abortedcheck beforewait(250)is correct.- CI: I confirmed
STUDIO_REQUIRE_MECATEDis read only byserver/test/integration/mecated-mock.test.ts(viatask studio:test:integration), so dropping it from the Playwright step is correct.
Nits (non-blocking):
verdict-ledger.tsandverdict-ledger.test.tsare the only files infeatures/chat/missing the// SPDX-License-Identifier: Apache-2.0header.- With
Promise.racegone, the deadline only works ifwatchSessionActivityhonors the abort signal. The SDK does, and the bounded-stream test covers it, but this is a dependency to keep in mind. - Agree with the follow-ups in the description: add a regression test for the successor-session switch and port the workspace's four Sets to the ledger.
The red checks on the first run come from a cancelled run (Analysis cancelled → Lint coverage gate). A new CI run on the same SHA is in progress.
Merged
2 tasks
This was referenced Oct 7, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to the #1975 review (#2142): the side thread and the main workspace answered the same permission and plan asks with diverging verdict bookkeeping. This PR fixes the cheap, clearly-correct items and leaves the contested refactors for later.
VerdictLedger(verdict-ledger.ts): one phase per ask (idle → inFlight → acknowledged | uncertain) replaces the side panel's three parallel Sets, which could hold contradictory states. A secondbegin()on a non-idle ask is refused, so a double-click sends one POST and an uncertain ask cannot be retried blindly.controlPending. View writes are now skipped when the verdict's session is no longer the viewed one; ledger writes stay unconditional.controlPendingis also reset on session change.messageOwnsApproval/unmatchedApprovalsinchat-state.tsreplace themessage.tools?.some(approvalMatchesToolCall)expression copied at 7 sites.plan-continuation.ts: hand-rolledAbortController/deadline/Promise.racereplaced withAbortSignal.any([...]). The retry loop no longer sleeps after an abort (the existing bounded-stream test caught this).chat-escape.ts: dropped the redundantfocusinbranch;disarmis hoisted (stableuseCallback) and reused by the second effect.STUDIO_REQUIRE_MECATED=1from the Playwright step (only the integration task reads it).failed_preconditiondeliberately stays in the stale set ofresolvePlanAsk, with a comment saying why (see reviewer notes).Issue relationship
Relates to #2142
Type of change
Test plan
Baseline checks
task lint): not run; only Studio changed. Studiobiome checkandtscare clean.task test): not run; no Go changes. Studio suites pass (web 596, server 173).task test:race): not run; no Go changes.go run ./cmd/mecademo): not run; no Go changes.task docs)task site:build): not run; heading-only docs change.task api:check)task api:update+engine/CHANGELOG.mdNew unit tests cover the ledger (double begin refused, uncertain stays blocked until reset, asks tracked independently).
Changes
apps/web/src/features/chat/verdict-ledger.ts(+ test)apps/web/src/features/chat/side-thread-panel.tsxapps/web/src/features/chat/chat-workspace.tsxmessageOwnsApprovalapps/web/src/features/chat/chat-state.ts,chat-transcript.tsxmessageOwnsApproval,unmatchedApprovalsapps/web/src/features/chat/plan-continuation.tsAbortSignal.any; no sleep after abortapps/web/src/features/chat/chat-escape.tsdisarmapps/server/src/mecatl/chat.tsfailed_preconditionstays staleuser-docs/building/deployment/studio.md.github/workflows/ci.ymlUser-facing change
A plan or permission verdict that completes after switching a side thread to its successor session no longer shows a stale notice/error or locks the new session's controls. Plan verdict failures in the main chat now read "Could not confirm this verdict: …".
Special notes for reviewers
ordinary*/plan*in-flight and uncertain), so the two surfaces still differ on stale-ask feedback (workspace shows an error, side panel is silent) and on when uncertainty is cleared. Porting the workspace is the next step and the largest remaining item.usePlanVerdictControl,PendingAskCard, splittingchat-workspace.tsx, unifyingsettledAskKeyswithapprovalKey(the reducer'ssessionIdis optional), thestalePlanAskcode split, the remaining test gaps, and checking whetherdetail()'scompatibility()call adds a per-request round trip.failed_preconditionstays stale against the issue's suggestion: the Go server returns it (HTTP 412) when a live run owns its plan continuation, androutes/chat.test.tsrequires the 409stale_run_controlmapping.Fully or partially written by an AI agent.
🤖 Generated with Claude Code