Commit 5dae4a3
authored
🤖 fix: keep optimistic goal visible during streaming (#3602)
## 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`_
<!-- mux-attribution: model=anthropic:claude-opus-4-8 thinking=xhigh
costs=n/a -->1 parent 9dcfc83 commit 5dae4a3
3 files changed
Lines changed: 154 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
439 | 439 | | |
440 | 440 | | |
441 | 441 | | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
442 | 460 | | |
443 | 461 | | |
444 | 462 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1624 | 1624 | | |
1625 | 1625 | | |
1626 | 1626 | | |
| 1627 | + | |
| 1628 | + | |
| 1629 | + | |
| 1630 | + | |
| 1631 | + | |
| 1632 | + | |
| 1633 | + | |
| 1634 | + | |
| 1635 | + | |
| 1636 | + | |
| 1637 | + | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
| 1641 | + | |
| 1642 | + | |
| 1643 | + | |
| 1644 | + | |
| 1645 | + | |
| 1646 | + | |
| 1647 | + | |
| 1648 | + | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
| 1652 | + | |
| 1653 | + | |
| 1654 | + | |
| 1655 | + | |
| 1656 | + | |
| 1657 | + | |
| 1658 | + | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
| 1662 | + | |
| 1663 | + | |
| 1664 | + | |
| 1665 | + | |
| 1666 | + | |
| 1667 | + | |
| 1668 | + | |
| 1669 | + | |
| 1670 | + | |
| 1671 | + | |
| 1672 | + | |
| 1673 | + | |
| 1674 | + | |
| 1675 | + | |
| 1676 | + | |
| 1677 | + | |
| 1678 | + | |
| 1679 | + | |
| 1680 | + | |
| 1681 | + | |
| 1682 | + | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
| 1686 | + | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
| 1695 | + | |
| 1696 | + | |
| 1697 | + | |
| 1698 | + | |
| 1699 | + | |
| 1700 | + | |
| 1701 | + | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
| 1706 | + | |
| 1707 | + | |
| 1708 | + | |
| 1709 | + | |
| 1710 | + | |
| 1711 | + | |
| 1712 | + | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
| 1716 | + | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
1627 | 1720 | | |
1628 | 1721 | | |
1629 | 1722 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2076 | 2076 | | |
2077 | 2077 | | |
2078 | 2078 | | |
2079 | | - | |
| 2079 | + | |
| 2080 | + | |
| 2081 | + | |
| 2082 | + | |
2080 | 2083 | | |
2081 | 2084 | | |
2082 | 2085 | | |
| 2086 | + | |
| 2087 | + | |
| 2088 | + | |
| 2089 | + | |
| 2090 | + | |
| 2091 | + | |
| 2092 | + | |
| 2093 | + | |
| 2094 | + | |
| 2095 | + | |
| 2096 | + | |
| 2097 | + | |
| 2098 | + | |
| 2099 | + | |
| 2100 | + | |
| 2101 | + | |
| 2102 | + | |
| 2103 | + | |
| 2104 | + | |
| 2105 | + | |
| 2106 | + | |
| 2107 | + | |
| 2108 | + | |
| 2109 | + | |
| 2110 | + | |
| 2111 | + | |
| 2112 | + | |
| 2113 | + | |
| 2114 | + | |
2083 | 2115 | | |
2084 | 2116 | | |
2085 | 2117 | | |
| |||
7960 | 7992 | | |
7961 | 7993 | | |
7962 | 7994 | | |
7963 | | - | |
| 7995 | + | |
| 7996 | + | |
| 7997 | + | |
| 7998 | + | |
| 7999 | + | |
| 8000 | + | |
| 8001 | + | |
| 8002 | + | |
| 8003 | + | |
| 8004 | + | |
7964 | 8005 | | |
7965 | 8006 | | |
7966 | 8007 | | |
| |||
0 commit comments