Commit b47c23b
authored
🤖 fix: refresh post-compaction context after plan writes (#1119)
🤖 Fix post-compaction context sidebar staleness after plan writes.
- Backend now emits refreshed workspace metadata (including
`postCompaction` state) after relevant tool completions (`file_edit_*`,
`propose_plan`) when the experiment is enabled.
- Refresh is debounced in `WorkspaceService` to coalesce rapid tool
sequences.
Validation:
- `make static-check`
- Added unit tests for the tool-call-end trigger and the debounce
behavior.
---
<details>
<summary>📋 Implementation Plan</summary>
# Fix: Post-compaction context UI not updating when plan is written
## Summary
When the **Post-Compaction Context** experiment is enabled, the
right-sidebar section (Costs tab) should show a “Plan file” item as soon
as the agent writes the plan file. Currently it **doesn’t update until
the user toggles the experiment off/on**.
## Repro (current behavior)
1. Enable **Settings → Experiments → Post-Compaction Context**.
2. In a workspace, have the agent write a plan (uses `file_edit_*` tools
to create/update the plan file).
3. Observe: the right-sidebar **Post-Compaction Context** section does
**not** appear / does not show “Plan file”.
4. Toggle the experiment off then on.
5. Observe: the section updates and now shows the “Plan file” item.
## Root cause (why this happens)
**Frontend data source is stale.**
- UI path:
- `CostsTab.tsx` renders `<PostCompactionSection … />` only when
`postCompactionEnabled`.
- `usePostCompactionState(workspaceId)` reads
`workspaceMetadata.get(workspaceId)?.postCompaction` from
`WorkspaceContext`.
- If `planPath` is `null`, the section won’t render (it early-returns
when there’s no plan and no tracked files).
- Where does `workspaceMetadata[*].postCompaction` come from?
- It is only included when `WorkspaceContext.loadWorkspaceMetadata()`
calls `api.workspace.list({ includePostCompaction: true })`.
- That list call is triggered on mount and when the user toggles the
experiment (see `ExperimentsSection.tsx` calling
`refreshWorkspaceMetadata()`).
- Critically:
- Writing the plan uses `file_edit_*` tools → creates history entries
and writes the plan file on disk.
- **No code path automatically refreshes workspace metadata (or
postCompaction state) after a file-edit tool finishes.**
- Backend only emits metadata enriched with postCompaction in one place
today: **after compaction completes** (`onCompactionComplete` callback
wired in `WorkspaceService.getOrCreateSession`).
So the UI is correct given its inputs; it’s just never told to
recompute/reload post-compaction state when plan/file edits happen.
## Recommended fix (Approach A): backend-driven metadata refresh on tool
completion
**Goal:** after a `file_edit_*` tool finishes (and after
`propose_plan`), emit an updated workspace metadata event that includes
fresh `postCompaction` state.
This keeps the UI model simple: `WorkspaceContext` already subscribes to
metadata events and will update `workspaceMetadata`, which will flow
into `usePostCompactionState` and re-render the right sidebar.
### Implementation plan
#### 1) Add a new “post-compaction state may have changed” callback to
`AgentSession`
- Extend `AgentSession` constructor args to include something like:
- `onPostCompactionStateChange?: () => void`
- Store it on the session.
#### 2) Trigger that callback on relevant tool completions
In `AgentSession.attachAiListeners()`:
- In the `"tool-call-end"` handler, after `emitChatEvent(payload)`:
- If tool name is one of:
- `file_edit_insert`
- `file_edit_replace_string`
- (optionally) `propose_plan` (since it can create/validate plan state)
- Then call `this.onPostCompactionStateChange?.()`.
**Optional gating:** only do this if the session has seen
`options?.experiments?.postCompactionContext === true` recently.
- Store `this.postCompactionExperimentEnabled` from the latest
`sendMessage` options.
- Gate callback invocation behind it.
#### 3) Implement the callback in `WorkspaceService` (debounced + safe)
In `WorkspaceService.getOrCreateSession()` pass:
- `onPostCompactionStateChange: () =>
schedulePostCompactionMetadataRefresh(workspaceId)`
Implement `schedulePostCompactionMetadataRefresh` in `WorkspaceService`:
- Coalesce bursts (multiple file edits) with a short debounce (e.g.
100–250ms).
- On fire:
- `const metadata = await this.getInfo(workspaceId)`
- If present, compute `const postCompaction = await
this.getPostCompactionState(workspaceId)`
- `this.sessions.get(workspaceId)?.emitMetadata({ ...metadata,
postCompaction })`
- Wrap in try/catch; treat runtime-unreachable as “don’t crash; skip
emitting postCompaction”.
#### 4) Ensure metadata updates don’t accidentally drop postCompaction
- Today, some metadata events may not include `postCompaction`.
- With this approach, we explicitly re-emit enriched metadata after file
edits.
- (Optional follow-up) When experiment is enabled, also enrich *other*
metadata emits (create/title update/etc.) to avoid losing the field.
### Validation
- Manual:
- Enable experiment.
- Write plan.
- Confirm Post-Compaction Context section appears immediately (no
experiment toggle needed).
- Edit plan again; confirm it remains visible.
- Edit a normal file (exec mode) and ensure tracked files list updates
as expected.
- Regression:
- With experiment disabled, ensure no extra metadata spam and section
stays hidden.
### Tests
- Add a unit test around the new trigger behavior:
- Simulate an `AgentSession` receiving a `tool-call-end` event for
`file_edit_*`.
- Assert `onPostCompactionStateChange` callback is invoked (and
debounced once for bursts).
- Add a `WorkspaceService` unit test for debounced refresh:
- Stub `getInfo()` and `getPostCompactionState()`.
- Call scheduler multiple times; assert a single
`session.emitMetadata()` with enriched metadata.
### Net LoC estimate (product code)
- **~80–160 LoC**
- AgentSession: add callback field + invoke on tool-call-end.
- WorkspaceService: debounce + enriched metadata emit.
## Alternative fix (Approach B): frontend-driven fetch of
post-compaction state
Instead of relying on `workspaceMetadata.postCompaction`, update
`usePostCompactionState` to call `api.workspace.getPostCompactionState({
workspaceId })`:
- On mount / workspace change.
- On tool-call-end events (would require subscribing to WorkspaceStore
or a custom event).
### Pros
- Uses existing API (`getPostCompactionState`) without expanding backend
event behavior.
### Cons
- Needs a reliable “tool-call-end happened” signal on the frontend.
- More moving parts in UI (event wiring, polling, or store integration).
### Net LoC estimate (product code)
- **~120–220 LoC**
- Hook changes + new event/listener plumbing.
## Decision
Proceed with **Approach A (backend-driven metadata refresh)**:
- It matches existing architecture (metadata subscription already
exists).
- It fixes the immediate bug (plan write doesn’t update sidebar) without
adding UI polling.
- It can be debounced centrally and is straightforward to reason about.
</details>
---
_Generated with `mux`_
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>1 parent 17ff207 commit b47c23b
File tree
4 files changed
+375
-17
lines changed- src/node/services
4 files changed
+375
-17
lines changedLines changed: 168 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| 94 | + | |
| 95 | + | |
94 | 96 | | |
95 | 97 | | |
96 | 98 | | |
| |||
102 | 104 | | |
103 | 105 | | |
104 | 106 | | |
| 107 | + | |
105 | 108 | | |
106 | 109 | | |
107 | 110 | | |
| |||
128 | 131 | | |
129 | 132 | | |
130 | 133 | | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
131 | 139 | | |
132 | 140 | | |
133 | 141 | | |
| |||
140 | 148 | | |
141 | 149 | | |
142 | 150 | | |
| 151 | + | |
143 | 152 | | |
144 | 153 | | |
145 | 154 | | |
| |||
154 | 163 | | |
155 | 164 | | |
156 | 165 | | |
| 166 | + | |
157 | 167 | | |
158 | 168 | | |
159 | 169 | | |
| |||
546 | 556 | | |
547 | 557 | | |
548 | 558 | | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
549 | 563 | | |
550 | 564 | | |
551 | 565 | | |
| |||
611 | 625 | | |
612 | 626 | | |
613 | 627 | | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
614 | 640 | | |
615 | 641 | | |
616 | 642 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| 18 | + | |
| 19 | + | |
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
| |||
116 | 119 | | |
117 | 120 | | |
118 | 121 | | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
0 commit comments