Skip to content

🤖 fix: keep optimistic goal visible during streaming#3602

Open
ammar-agent wants to merge 2 commits into
mainfrom
goal-ui-4y26
Open

🤖 fix: keep optimistic goal visible during streaming#3602
ammar-agent wants to merge 2 commits into
mainfrom
goal-ui-4y26

Conversation

@ammar-agent

@ammar-agent ammar-agent commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a transient UI bug where a goal set while the agent is streaming would briefly disappear from the Goal tab (it appears, vanishes, then comes back).

Background

When a goal is set mid-stream, WorkspaceGoalService intentionally defers the durable goal.json write until stream-end and holds the new goal as optimistic, transient state (pendingGoalSnapshots, surfaced to the renderer as a pendingPersistence goal). goal.json (and the persisted activity metadata) keep the pre-stream goal.

The problem: the live stream keeps emitting full workspace activity snapshots — status_set, todo_write/propose_plan, recency bumps, streaming-status updates — each built from persisted metadata, so each still carries the stale pre-stream goal. In the renderer those non-transient snapshots fully replace the in-memory activity entry, dropping the optimistic goal. It only reappeared on the next goal read or at stream-end, producing the flicker:

set goal G  -> transient overlay shows G        (visible)
status_set  -> full snapshot carries old goal D  (G vanishes)
get_goal    -> transient overlay re-emits G      (G reappears)

Implementation

The fix lives at the single activity-emit choke point, WorkspaceService.emitWorkspaceActivity. Before emitting, it overlays the goal service's pending optimistic snapshot (via the new WorkspaceGoalService.getPendingGoalSnapshot) onto the activity snapshot's goal field, so every mid-stream emit surfaces the optimistic goal instead of replaying the stale one.

This is robust because the goal service owns the pending state and clears it before emitting authoritative transitions:

  • Transient goal pushes already carry the optimistic goal (transientGoalOnly) and are skipped by the overlay.
  • User abort (recordUserStoppedStream) deletes the pending snapshot before pushing the reverted goal, so the overlay no longer fires and the discarded goal correctly disappears.
  • Stream-end persistence (applyPendingAfterStreamEnd) likewise clears the pending snapshot before pushing the durable goal.

A frontend-only guard was considered but rejected: the renderer cannot distinguish a stale mid-stream emit from an authoritative abort-revert (both carry the persisted goal and can arrive while streaming is still true), so any renderer heuristic would wrongly pin a discarded goal after an Esc/abort. Anchoring the fix in the goal-state owner avoids that ambiguity.

Validation

  • New WorkspaceService test drives the real path: queue a goal mid-stream, fire a status_set-style activity emit, and assert the emitted snapshot surfaces the optimistic (pendingPersistence) goal — then recordUserStoppedStream and assert a subsequent emit reverts to the persisted goal with no lingering optimistic state.
  • make static-check and the workspaceService / workspaceGoalService / WorkspaceStore suites pass.

Risks

Low. The overlay is a narrow read-only lookup at emit time that only changes behavior while a goal mutation is queued (mid-stream). Authoritative goal emits are untouched, and the active-goal count already excludes pending goals. The main area to watch is goal-tab/sidebar display during streaming, which the new test exercises end-to-end including the abort path.


Generated with mux • Model: anthropic:claude-opus-4-8 • Thinking: xhigh • Cost: n/a

@ammar-agent

Copy link
Copy Markdown
Collaborator 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: c34fdf6c53

ℹ️ 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/browser/stores/WorkspaceStore.ts Outdated
A goal set while the agent is streaming is published only as optimistic,
transient state in WorkspaceGoalService; the durable goal.json write is
deferred until stream-end. Activity snapshots emitted mid-stream from persisted
metadata (status_set, todo_write, recency, streaming) still carry the
pre-stream goal, and each one fully replaced the frontend activity entry,
dropping the optimistic goal so it flickered out of the Goal tab until the next
goal read re-overlaid it.

Overlay the goal service's pending snapshot in WorkspaceService.emitWorkspaceActivity
so every activity emit surfaces the optimistic goal while it is queued. The goal
service clears its pending snapshot before emitting authoritative reverts (user
abort) or durable persistence (stream-end), so those transitions win naturally
and the discarded goal does not linger after an abort.
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed the P2 abort-revert concern (thread PRRT_kwDOPxxmWM6LIK0V).

The earlier renderer-side guard was the wrong layer precisely because of this race: the renderer cannot tell a stale mid-stream activity emit apart from the authoritative abort-revert — both carry the persisted goal and can arrive while streaming is still true.

The fix now lives in WorkspaceService.emitWorkspaceActivity, which overlays the goal service's pending optimistic snapshot. Because recordUserStoppedStream deletes the pending snapshot before pushing the reverted goal, the overlay stops firing on abort and the discarded goal correctly disappears (no lingering optimistic goal, even if a follow-up stream starts). Same for stream-end persistence, which clears the pending snapshot before pushing the durable goal. Added a WorkspaceService test covering both the mid-stream surface and the abort revert.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Reworked the fix to live in the backend (WorkspaceService.emitWorkspaceActivity overlays the goal service's pending snapshot) instead of the renderer, which resolves the abort-revert race you flagged. Please take another look.

@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: 7bb107ff05

ℹ️ 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
getActivityList() builds straight from persisted extensionMetadata snapshots,
so a renderer reconnect/reload (or ACP client) during a mid-stream goal set
seeded the UI with the stale pre-stream goal until the next live emit. Apply
the same pending-goal overlay there.
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

Addressed (thread PRRT_kwDOPxxmWM6LIOj3): getActivityList() now applies the same overlayPendingGoal when building list entries, so a renderer reconnect/reload or ACP client listing activity during a mid-stream set_goal surfaces the optimistic goal instead of the stale persisted one. Extended the test to assert the bootstrap list path too.

@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

Applied the pending-goal overlay to getActivityList() so the renderer bootstrap / reconnect path no longer seeds a stale goal mid-stream. Please take another look.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: e637829c0a

ℹ️ 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".

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