Repository navigation
Conversation
Exercise the reader Retry callback through the observation lifecycle and real transcript controller. Cover initial recovery failure, repeated Retry, session handoff, observation replacement, and unmount. Generated-by: Codex
Keep the observation controller available before the first transcript publication so Retry can recover after the initial read and automatic recovery fail. Fence retry admission and late errors by session selection and observation identity. Generated-by: Codex
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
Full review of 5948cb61 (two commits: d72579a0 adds the test and 5948cb61 the fix; +270/-7, 5 files). This PR hasn't been reviewed before, and it doesn't link an issue.
What changed. The observation effect now records { sessionId, controller } in a new workspace.observationRef, and clears it in cleanup only if it still points at the same observation. retryMessages reloads that controller instead of transcriptRangeRef.current, which is set only after a successful publication. It is guarded by observation.sessionId === sessionId and isSessionSelected(sessionId), and the error report is rechecked after the await.
Correctness.
- The guard passes in exactly the failure case.
isSessionSelectedrequires the published idactiveIdRefto equal the session (session-workspace-actions.ts:231). On a read failure,applyReadErrorcallscommitTranscript(sessionId, [])(use-conversation-observation.ts:71), which setsactiveIdRefand leavestranscriptRangeRefundefined. So after a failure Retry can proceed, while Retry for a different selected Session is still rejected. - No controller divergence. Only the observation effect publishes into
transcriptRangeRef(viapublishTranscript/commitTranscript). There is one controller per effect run: re-subscribing to events reuses it. SoobservationRef.controlleris always the controller that is displayed, or would be once published. - Lifecycle. The identity-checked clear in cleanup handles StrictMode re-mounts and authority-revision replacement. A reload in flight when the effect is torn down is closed by
controller.close(), and its error is suppressed by the post-await identity and selection check.messageRetryPendingis released infinally. refreshMessagesstill usestranscriptRangeRefon purpose, since it needs a published range. That matches the stated scope.
Findings
- P3: a failed Retry shows two error toasts (see the inline comment on
transcript-commands.ts:58). - Nit, no grade:
use-conversation-observation.ts:51uses an inlineimport('../model/conversation-workspace.js')type. A top-levelimport typewould match the rest of the file.
Tests. There are 10 cases that drive the real createDesktopTranscriptRangeController through ConversationLifecycle. They cover:
- Retry after the initial read and the automatic recovery both fail (proved RED before the fix)
- automatic recovery alone, and a failed Retry followed by a successful one
- an A to B handoff, observation-authority replacement, and unmount with a pending read
No test asserts the toast count. Putting the test in apps/desktop/src/main/__tests__ follows the existing convention (47 conversation tests live there).
i18n: no UI copy changes, so all three locales are unaffected. Protocol: not touched.
Status: MERGEABLE (merge state BLOCKED, review required). The head is on current main 3597abe8, git merge-tree is clean, and git diff --check is clean. Hosted CI (label and test) passes on 5948cb61. I did not run the suites or the Electron UI locally. This review is not merge approval.
The range controller's reload() hands a failure to its recovery, which reports it through the observation's onError (the "Failed to load task" toast and read error), and then rethrows. retryMessages reported the rethrown error again as a refresh failure, so one failed Retry raised two toasts and replaced the read error with the refresh message. Leave the report to the controller, as its own gap and recovery reloads do, and only release the pending Retry. The controller still withholds a reload that lands on the cached transcript, so Retry stays quiet then too. refreshMessages never calls reload() and keeps its own report. The retry test now records error toasts and checks that a failed Retry raises exactly one, keeps the read error, and that a successful Retry raises none.
Summary
When the first transcript read and its automatic recovery both fail, Retry does nothing because it can only reach a controller after a successful publication. Keep the current observation controller available to Retry so the next read restores the conversation. Check session selection and observation identity to reject stale commands and errors.
This covers transcript Retry only. Published-history readiness and the observation effect's cleanup ownership remain intact.
Verification
Retry must issue another transcript read before any publication: 2 !== 3. The 10 new regression cases now pass, and 153 related conversation, transcript, and navigation tests pass throughnode --test.npm run build,npm run typecheck,npm run lint,npm run format:check, and Knip for desktop and UI pass. Renderer architecture against the base, Astryx surface inventory, Windows inventory, app-shell hooks, E2E budget, and ASF headers pass.AI use
Tool(s) and scope: Codex with GPT-6 Astra for investigation, implementation, regression tests, review, and verification. Both commits include
Generated-by: Codex.Checklist
Does this PR entail a change in behavior?