## Problem
The summarization prompt explicitly tells the model to **"Omit
pleasantries and next-step suggestions"** and the summary prefix frames
the compacted context as passive history: `Summary of earlier chat
context:`. After compaction mid-task, the model reads a factual recap
with no forward momentum, loses its direction, and either stops or asks
the user what to do.
## Research
I compared our compaction prompt against several other agents:
| Agent | Key Pattern |
|---|---|
| **Codex** | Prompt says *"Include what remains to be done (clear next
steps)"*. Prefix: *"Another language model started to solve this
problem..."* |
| **Mux** | Includes *"Current state of the work (what's done, what's in
progress)"* + appends the user's follow-up intent |
| **Continue** | *"Make sure it is clear what the current stream of work
was at the very end prior to compaction so that you can continue exactly
where you left off"* |
| **Copilot Chat** | Dedicated sections for *Active Work State*, *Recent
Operations*, *Pre-Summary State*, and a *Continuation Plan* with
explicit next actions |
**Every other major agent explicitly preserves forward intent and
in-progress state.** Coder was the only one telling the model to omit
next steps.
## Changes
**Summary prompt:**
- Removes `Omit next-step suggestions`
- Adds structured `Include:` list with explicit items for in-progress
work, remaining work, and the specific action being performed when
compaction fired
- Frames the operation as `context compaction` (matching Codex's
framing)
**Summary prefix:**
- Old: `Summary of earlier chat context:`
- New: `The following is a summary of the earlier conversation. The
assistant was actively working when the context was compacted. Continue
the work described below:`
The prefix is the first thing the model reads post-compaction — framing
it as an active handoff with an explicit "Continue" directive primes the
model to resume work rather than wait.
Split from #22693 per review feedback.
Fixes multiple bugs in coderd/chatd and sub-packages including race
conditions, transaction safety, stream buffer bounds, retry limits, and
enterprise relay improvements.
See commit message for full list.
## Problem
Context compaction in chatd persisted durable messages for the
`chat_summarized` tool call and result via `publishMessage`, but never
published `message_part` streaming events via `publishMessagePart`. This
meant connected clients had no streaming representation of the
compaction.
The client's `streamState` (built entirely from `message_part` events in
`streamState.ts`) never saw the compaction tool call, so:
- No **"Summarizing..."** running state was shown to the user during
summary generation (which can take up to 90s).
- The durable `message` events arrived after or interleaved with the
`status: waiting` event, causing the tool to appear as "Summarized" with
the chat appearing to just stop.
## Fix
### 1. `CompactionOptions.OnStart` callback (chatloop)
Added an `OnStart` callback to `CompactionOptions`, called in
`maybeCompact` right before `generateCompactionSummary` (the slow LLM
call). This gives `chatd` a hook to publish the tool-call `message_part`
immediately when compaction begins.
### 2. Tool-result streaming part (chatd)
`persistChatContextSummary` now publishes a tool-result `message_part`
before the durable `message` events, so clients transition from
"Summarizing..." to "Summarized" before the status change arrives.
### Event ordering is now:
1. `message_part` (tool call via `OnStart`) — client shows
"Summarizing..."
2. LLM generates summary (up to 90s)
3. `message_part` (tool result) — client shows "Summarized" in stream
state
4. `message` (assistant) — durable message persisted, stream state
resets
5. `message` (tool) — durable tool result persisted
6. `status: waiting` — chat transitions to idle
## Tests
- **`OnStartFiresBeforePersist`**: Verifies callback ordering is
`on_start` → `generate` → `persist`.
- **`OnStartNotCalledBelowThreshold`**: Verifies `OnStart` is not called
when context usage is below the compaction threshold.