-
Notifications
You must be signed in to change notification settings - Fork 46
🤖 fix: stabilize post-compaction context #1932
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
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: 8167150b00
ℹ️ 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".
|
@codex review Addressed the thread about cleaning up persisted post-compaction state when compaction fails before clearing history. |
|
Codex Review: Didn't find any major issues. 🚀 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
cd3292e to
b9df113
Compare
Change-Id: Ia002a119d6e60811e37fc3d6a54811c55ea6e136 Signed-off-by: Thomas Kosiewski <[email protected]>
Change-Id: Ia3734839b9190a78d0447fb123439a372ca7d244 Signed-off-by: Thomas Kosiewski <[email protected]>
Change-Id: I112385af7acb7a2e9750e958dc88788abdf2bd7c Signed-off-by: Thomas Kosiewski <[email protected]>
b9df113 to
af87068
Compare
Summary
- Stabilizes post-compaction context injection so it can be always-on:
crash-safe persistence, hard size budgeting, and a context_exceeded loop
breaker.
Background
- After compaction, we inject plan + edited-file diffs back into the
prompt so the model retains critical context. Previously this was
experiment-gated and unsafe:
- pre-compaction diffs were stored in-memory only (lost on restart)
- injected context had no global cap (could trigger context_exceeded)
- context_exceeded could lead to repeated auto-compaction loops
Implementation
- Persist pending post-compaction diffs to
`~/.mux/sessions/<workspaceId>/post-compaction.json` and load them on
restart.
- Add a deterministic character budget for post-compaction injection
(plan truncated; diffs omitted once budget is hit), with explicit
truncation notes.
- If a stream that included post-compaction injection fails with
`context_exceeded` before any deltas, retry once without injection (and
discard pending state) to break loops.
- Graduate the feature out of the `POST_COMPACTION_CONTEXT` experiment:
remove schema/flag plumbing and always show the sidebar section.
Validation
- `bun test` (targeted post-compaction + experiment/telemetry tests)
- `make static-check`
Risks
- On retry, the model loses injected post-compaction context for that
turn (by design to avoid infinite loops). Pending state is discarded
with a log+sidebar refresh.
- Corrupt/invalid persisted JSON is treated as “no pending state”
(self-healing; never crashes startup).
---
<details>
<summary>📋 Implementation Plan</summary>
# Stabilize “Post-Compaction Context” (graduate from experiment)
## Context / Why
The `POST_COMPACTION_CONTEXT` experiment re-injects **plan file
content** and **edited-file diffs** (and currently also a **TODO list**)
after history compaction so the model still has the critical context
that compaction removed.
To remove the experiment gate and make this feature always-on, we need
to ensure:
- Injection is **bounded** (can’t blow up prompts / trigger context
errors by itself).
- We don’t create a **context_exceeded → auto-compact → context_exceeded
loop**.
- The “pending post-compaction state” (especially the **pre-compaction
diffs**) survives **app restarts/crashes**.
- We have tests that make the behavior safe to refactor.
## Evidence (repo pointers)
- `src/node/services/agentSession.ts`:
`getPostCompactionAttachmentsIfNeeded()` injects immediately after
compaction, then every `TURNS_BETWEEN_ATTACHMENTS` turns.
- `src/node/services/compactionHandler.ts`: stores pending
post-compaction diffs **in-memory only** (`cachedFileDiffs` +
`postCompactionAttachmentsPending`).
- `src/node/services/aiService.ts` →
`src/browser/utils/messages/modelMessageTransform.ts`:
`injectPostCompactionAttachments()` inserts a synthetic user message
after the compaction summary.
- `src/browser/utils/messages/attachmentRenderer.ts`: renders
plan/diffs/todos with **no total size cap**.
- `src/common/constants/attachments.ts`: per-file diff truncation
(`MAX_FILE_CONTENT_SIZE = 50_000`) + `MAX_EDITED_FILES = 10`, but **no
global cap**.
## Goals
1. **Hard budget** for post-compaction injection content (deterministic,
provider-agnostic).
2. **Loop breaker**: if injection causes `context_exceeded`, retry once
without (or with smaller) injection instead of triggering repeated
compactions.
3. **Persistence**: pending pre-compaction diffs survive
restarts/crashes until they’ve been successfully injected.
4. **Test coverage** for injection/budget/persistence.
## Non-goals
- Perfect, tokenizer-accurate “remaining token budget” accounting per
provider (start with a cheap heuristic).
- Changing compaction summarization quality (only the post-compaction
*reinjection* behavior).
---
## Plan (recommended approach)
### 1) Persist pending post-compaction diffs across restarts/crashes
(net **+~220 LoC** product code)
**Problem:** After compaction, the chat history is replaced by a
summary, so pre-compaction `file_edit_*` diffs no longer exist in
history. Today they’re only recoverable from
`CompactionHandler.cachedFileDiffs` in memory, so a restart loses them.
**Approach:** Persist “pending post-compaction diffs” into the workspace
session dir and load them on startup.
**Implementation steps**
- **Define a persisted schema** (versioned) for pending post-compaction
state:
- New file: `~/.mux/sessions/<workspaceId>/post-compaction.json` (exact
name can vary; keep it stable).
- Contents: `{ version: 1, createdAt: number, diffs: FileEditDiff[] }`.
- **Write pending diffs during compaction**:
- In `src/node/services/compactionHandler.ts`, after
`extractEditedFileDiffs(messages)` but before clearing history, write
the JSON file (best-effort; failure must not fail compaction).
- This may require threading `Config` (or `getSessionDir(workspaceId)`
callback) into `CompactionHandler`.
- **Load pending diffs on session start**:
- In `CompactionHandler` constructor (or a dedicated `init()` called
from `AgentSession`), read and validate the JSON.
- If valid and non-empty, set `cachedFileDiffs` and
`postCompactionAttachmentsPending = true`.
- **Change “consume” semantics to be crash-safe**:
- Replace `consumePendingDiffs()` with a two-phase API:
- `peekPendingDiffs(): FileEditDiff[] | null` (does not clear)
- `ackPendingDiffsConsumed(): void` (clears in-memory + deletes
`post-compaction.json`)
- In `AgentSession.getPostCompactionAttachmentsIfNeeded()`, use
`peekPendingDiffs()`.
- **Keep the sidebar state accurate after restart**:
- In `src/node/services/workspaceService.ts` `getPostCompactionState()`,
if there’s no active session (or no pending paths), fall back to reading
`post-compaction.json` and exposing `diffs[].path` (filtered to exclude
plan paths) as `trackedFilePaths`.
- Only call `ackPendingDiffsConsumed()` **after the next non-compaction
stream finishes successfully** (e.g., in the `stream-end` handler), so a
crash mid-stream does not lose the pending diffs.
**Notes / defensive programming**
- Treat bad JSON as “no pending diffs” (self-healing).
- Assert basic invariants when reading (array shapes, `diff` is string,
etc.), but never crash startup.
<details>
<summary>Alternative (not recommended): persist a synthetic “diff
snapshot” message into history</summary>
This would survive restarts “for free”, but it defeats the purpose of
compaction by reintroducing lots of tokens into history. It also risks
prompt-cache churn and UI clutter.
Net LoC estimate: **+~80 LoC** product code, but high UX/token cost.
</details>
---
### 2) Enforce a hard budget for injected post-compaction context (net
**+~140 LoC** product code)
**Problem:** Even after compaction, injection can be huge (up to `10 ×
50k` chars of diffs plus unbounded plan file), risking
`context_exceeded`.
**Approach:** Add a deterministic *character-budget* based limiter that
renders a bounded subset of attachments.
**Implementation steps**
- Add budget constants in `src/common/constants/attachments.ts` (or a
new constants file if preferred):
- `MAX_POST_COMPACTION_INJECTION_CHARS` (e.g., `80_000`)
- `MAX_POST_COMPACTION_PLAN_CHARS` (e.g., `30_000`)
- (Optional) `MAX_POST_COMPACTION_DIFFS_CHARS` (e.g., `50_000`) to
ensure diffs don’t crowd out everything else.
- Cap plan content at read time:
- In `src/node/services/attachmentService.ts`, slice `planContent` to
`MAX_POST_COMPACTION_PLAN_CHARS` and append a short “(truncated)” note.
- (Optional follow-up) implement a streaming read helper that stops at N
bytes to avoid reading massive files into memory.
- Budgeted rendering:
- In `src/browser/utils/messages/attachmentRenderer.ts`, add
`renderAttachmentsToContentWithBudget(attachments, { maxChars })`.
- Priority order: plan reference → todo list → diffs (most recent first)
until budget is exhausted.
- If truncating/omitting, add an explicit note like “(post-compaction
context truncated; omitted N file diffs)”.
- Update injection site:
- In `src/browser/utils/messages/modelMessageTransform.ts`, have
`injectPostCompactionAttachments()` use the budgeted renderer.
---
### 3) Loop breaker: auto-retry once when injection triggers
`context_exceeded` (net **+~180 LoC** product code)
**Problem:** If the injected context triggers `context_exceeded`, the
UI’s auto-compaction can repeatedly compact an already-compacted
history.
**Approach:** On `context_exceeded` failures where we know
post-compaction injection was included, retry once with injection
suppressed (or with a much smaller budget) before emitting
`stream-error`.
**Implementation steps**
- Track whether the *current stream* included post-compaction injection:
- In `src/node/services/agentSession.ts` `streamWithHistory()`, store a
boolean like `this.activeStreamHadPostCompactionInjection` when
`postCompactionAttachments?.length > 0`.
- Also track a retry guard (e.g., a `Set<string>` keyed by the failing
assistant `messageId`, or a simple “already retried once” flag).
- In `handleStreamError()` (or wherever `context_exceeded` is surfaced):
- If `errorType === "context_exceeded"` and
`activeStreamHadPostCompactionInjection` and not yet retried:
- Clear/disable pending post-compaction diffs for this compaction event
**or** retry with a smaller budget.
- Retry `streamWithHistory()` once with injection suppressed.
- Only emit the `stream-error` event if retry fails.
- Guardrails:
- Only auto-retry when the failure happens before meaningful deltas/tool
calls were emitted (to avoid polluting history with partial assistant
content).
- Log a structured debug line so we can diagnose if this ever triggers
unexpectedly.
---
### 4) Add tests for injection, budgets, persistence (net **+0 LoC**
product code; tests only)
**Unit tests**
- `src/browser/utils/messages/attachmentRenderer.test.ts` (or adjacent):
- budget truncation behavior; stable ordering; “omitted N” note.
- `src/browser/utils/messages/modelMessageTransform.test.ts`:
- `injectPostCompactionAttachments()` inserts after compaction summary
and uses budgeted output.
**Node/service tests**
- New test around `AgentSession` + `CompactionHandler` integration:
- “compaction produces pending diffs persisted to disk; next send
injects; stream-end acks and deletes persistence file.”
- “restart”: create a new session/handler that loads persisted diffs and
injects.
- “context_exceeded loop breaker”: simulate context_exceeded and ensure
one retry occurs without injection.
---
### 5) Graduate the feature out of the experiment system (follow-up
after the above is green) (net **+~120 LoC** product code)
- Remove the experiment gate:
- Backend: stop passing/reading
`options.experiments.postCompactionContext` for injection.
- Frontend: always show Post-Compaction section (or hide entirely if UX
decision is to keep it internal).
- Remove the experiment toggle from Settings and remove
`EXPERIMENT_IDS.POST_COMPACTION_CONTEXT` plumbing (including schema
fields).
- Keep exclusions (`exclusions.json`) working as the user-facing opt-out
mechanism for specific items.
---
## Acceptance criteria
- Post-compaction injection is bounded by a clear, test-backed budget.
- A single oversized plan/diff cannot trap a workspace in repeated
auto-compaction loops.
- Pre-compaction diffs survive restart/crash until successfully injected
once.
- Tests cover: budget truncation, persistence load/ack, and retry guard
behavior.
</details>
---
_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `high` •
Cost: $
<!-- mux-attribution: model=openai:gpt-5.2 thinking=high costs=8.58 -->
---------
Signed-off-by: Thomas Kosiewski <[email protected]>
Summary
- Stabilizes post-compaction context injection so it can be always-on:
crash-safe persistence, hard size budgeting, and a context_exceeded loop
breaker.
Background
- After compaction, we inject plan + edited-file diffs back into the
prompt so the model retains critical context. Previously this was
experiment-gated and unsafe:
- pre-compaction diffs were stored in-memory only (lost on restart)
- injected context had no global cap (could trigger context_exceeded)
- context_exceeded could lead to repeated auto-compaction loops
Implementation
- Persist pending post-compaction diffs to
`~/.mux/sessions/<workspaceId>/post-compaction.json` and load them on
restart.
- Add a deterministic character budget for post-compaction injection
(plan truncated; diffs omitted once budget is hit), with explicit
truncation notes.
- If a stream that included post-compaction injection fails with
`context_exceeded` before any deltas, retry once without injection (and
discard pending state) to break loops.
- Graduate the feature out of the `POST_COMPACTION_CONTEXT` experiment:
remove schema/flag plumbing and always show the sidebar section.
Validation
- `bun test` (targeted post-compaction + experiment/telemetry tests)
- `make static-check`
Risks
- On retry, the model loses injected post-compaction context for that
turn (by design to avoid infinite loops). Pending state is discarded
with a log+sidebar refresh.
- Corrupt/invalid persisted JSON is treated as “no pending state”
(self-healing; never crashes startup).
---
<details>
<summary>📋 Implementation Plan</summary>
# Stabilize “Post-Compaction Context” (graduate from experiment)
## Context / Why
The `POST_COMPACTION_CONTEXT` experiment re-injects **plan file
content** and **edited-file diffs** (and currently also a **TODO list**)
after history compaction so the model still has the critical context
that compaction removed.
To remove the experiment gate and make this feature always-on, we need
to ensure:
- Injection is **bounded** (can’t blow up prompts / trigger context
errors by itself).
- We don’t create a **context_exceeded → auto-compact → context_exceeded
loop**.
- The “pending post-compaction state” (especially the **pre-compaction
diffs**) survives **app restarts/crashes**.
- We have tests that make the behavior safe to refactor.
## Evidence (repo pointers)
- `src/node/services/agentSession.ts`:
`getPostCompactionAttachmentsIfNeeded()` injects immediately after
compaction, then every `TURNS_BETWEEN_ATTACHMENTS` turns.
- `src/node/services/compactionHandler.ts`: stores pending
post-compaction diffs **in-memory only** (`cachedFileDiffs` +
`postCompactionAttachmentsPending`).
- `src/node/services/aiService.ts` →
`src/browser/utils/messages/modelMessageTransform.ts`:
`injectPostCompactionAttachments()` inserts a synthetic user message
after the compaction summary.
- `src/browser/utils/messages/attachmentRenderer.ts`: renders
plan/diffs/todos with **no total size cap**.
- `src/common/constants/attachments.ts`: per-file diff truncation
(`MAX_FILE_CONTENT_SIZE = 50_000`) + `MAX_EDITED_FILES = 10`, but **no
global cap**.
## Goals
1. **Hard budget** for post-compaction injection content (deterministic,
provider-agnostic).
2. **Loop breaker**: if injection causes `context_exceeded`, retry once
without (or with smaller) injection instead of triggering repeated
compactions.
3. **Persistence**: pending pre-compaction diffs survive
restarts/crashes until they’ve been successfully injected.
4. **Test coverage** for injection/budget/persistence.
## Non-goals
- Perfect, tokenizer-accurate “remaining token budget” accounting per
provider (start with a cheap heuristic).
- Changing compaction summarization quality (only the post-compaction
*reinjection* behavior).
---
## Plan (recommended approach)
### 1) Persist pending post-compaction diffs across restarts/crashes
(net **+~220 LoC** product code)
**Problem:** After compaction, the chat history is replaced by a
summary, so pre-compaction `file_edit_*` diffs no longer exist in
history. Today they’re only recoverable from
`CompactionHandler.cachedFileDiffs` in memory, so a restart loses them.
**Approach:** Persist “pending post-compaction diffs” into the workspace
session dir and load them on startup.
**Implementation steps**
- **Define a persisted schema** (versioned) for pending post-compaction
state:
- New file: `~/.mux/sessions/<workspaceId>/post-compaction.json` (exact
name can vary; keep it stable).
- Contents: `{ version: 1, createdAt: number, diffs: FileEditDiff[] }`.
- **Write pending diffs during compaction**:
- In `src/node/services/compactionHandler.ts`, after
`extractEditedFileDiffs(messages)` but before clearing history, write
the JSON file (best-effort; failure must not fail compaction).
- This may require threading `Config` (or `getSessionDir(workspaceId)`
callback) into `CompactionHandler`.
- **Load pending diffs on session start**:
- In `CompactionHandler` constructor (or a dedicated `init()` called
from `AgentSession`), read and validate the JSON.
- If valid and non-empty, set `cachedFileDiffs` and
`postCompactionAttachmentsPending = true`.
- **Change “consume” semantics to be crash-safe**:
- Replace `consumePendingDiffs()` with a two-phase API:
- `peekPendingDiffs(): FileEditDiff[] | null` (does not clear)
- `ackPendingDiffsConsumed(): void` (clears in-memory + deletes
`post-compaction.json`)
- In `AgentSession.getPostCompactionAttachmentsIfNeeded()`, use
`peekPendingDiffs()`.
- **Keep the sidebar state accurate after restart**:
- In `src/node/services/workspaceService.ts` `getPostCompactionState()`,
if there’s no active session (or no pending paths), fall back to reading
`post-compaction.json` and exposing `diffs[].path` (filtered to exclude
plan paths) as `trackedFilePaths`.
- Only call `ackPendingDiffsConsumed()` **after the next non-compaction
stream finishes successfully** (e.g., in the `stream-end` handler), so a
crash mid-stream does not lose the pending diffs.
**Notes / defensive programming**
- Treat bad JSON as “no pending diffs” (self-healing).
- Assert basic invariants when reading (array shapes, `diff` is string,
etc.), but never crash startup.
<details>
<summary>Alternative (not recommended): persist a synthetic “diff
snapshot” message into history</summary>
This would survive restarts “for free”, but it defeats the purpose of
compaction by reintroducing lots of tokens into history. It also risks
prompt-cache churn and UI clutter.
Net LoC estimate: **+~80 LoC** product code, but high UX/token cost.
</details>
---
### 2) Enforce a hard budget for injected post-compaction context (net
**+~140 LoC** product code)
**Problem:** Even after compaction, injection can be huge (up to `10 ×
50k` chars of diffs plus unbounded plan file), risking
`context_exceeded`.
**Approach:** Add a deterministic *character-budget* based limiter that
renders a bounded subset of attachments.
**Implementation steps**
- Add budget constants in `src/common/constants/attachments.ts` (or a
new constants file if preferred):
- `MAX_POST_COMPACTION_INJECTION_CHARS` (e.g., `80_000`)
- `MAX_POST_COMPACTION_PLAN_CHARS` (e.g., `30_000`)
- (Optional) `MAX_POST_COMPACTION_DIFFS_CHARS` (e.g., `50_000`) to
ensure diffs don’t crowd out everything else.
- Cap plan content at read time:
- In `src/node/services/attachmentService.ts`, slice `planContent` to
`MAX_POST_COMPACTION_PLAN_CHARS` and append a short “(truncated)” note.
- (Optional follow-up) implement a streaming read helper that stops at N
bytes to avoid reading massive files into memory.
- Budgeted rendering:
- In `src/browser/utils/messages/attachmentRenderer.ts`, add
`renderAttachmentsToContentWithBudget(attachments, { maxChars })`.
- Priority order: plan reference → todo list → diffs (most recent first)
until budget is exhausted.
- If truncating/omitting, add an explicit note like “(post-compaction
context truncated; omitted N file diffs)”.
- Update injection site:
- In `src/browser/utils/messages/modelMessageTransform.ts`, have
`injectPostCompactionAttachments()` use the budgeted renderer.
---
### 3) Loop breaker: auto-retry once when injection triggers
`context_exceeded` (net **+~180 LoC** product code)
**Problem:** If the injected context triggers `context_exceeded`, the
UI’s auto-compaction can repeatedly compact an already-compacted
history.
**Approach:** On `context_exceeded` failures where we know
post-compaction injection was included, retry once with injection
suppressed (or with a much smaller budget) before emitting
`stream-error`.
**Implementation steps**
- Track whether the *current stream* included post-compaction injection:
- In `src/node/services/agentSession.ts` `streamWithHistory()`, store a
boolean like `this.activeStreamHadPostCompactionInjection` when
`postCompactionAttachments?.length > 0`.
- Also track a retry guard (e.g., a `Set<string>` keyed by the failing
assistant `messageId`, or a simple “already retried once” flag).
- In `handleStreamError()` (or wherever `context_exceeded` is surfaced):
- If `errorType === "context_exceeded"` and
`activeStreamHadPostCompactionInjection` and not yet retried:
- Clear/disable pending post-compaction diffs for this compaction event
**or** retry with a smaller budget.
- Retry `streamWithHistory()` once with injection suppressed.
- Only emit the `stream-error` event if retry fails.
- Guardrails:
- Only auto-retry when the failure happens before meaningful deltas/tool
calls were emitted (to avoid polluting history with partial assistant
content).
- Log a structured debug line so we can diagnose if this ever triggers
unexpectedly.
---
### 4) Add tests for injection, budgets, persistence (net **+0 LoC**
product code; tests only)
**Unit tests**
- `src/browser/utils/messages/attachmentRenderer.test.ts` (or adjacent):
- budget truncation behavior; stable ordering; “omitted N” note.
- `src/browser/utils/messages/modelMessageTransform.test.ts`:
- `injectPostCompactionAttachments()` inserts after compaction summary
and uses budgeted output.
**Node/service tests**
- New test around `AgentSession` + `CompactionHandler` integration:
- “compaction produces pending diffs persisted to disk; next send
injects; stream-end acks and deletes persistence file.”
- “restart”: create a new session/handler that loads persisted diffs and
injects.
- “context_exceeded loop breaker”: simulate context_exceeded and ensure
one retry occurs without injection.
---
### 5) Graduate the feature out of the experiment system (follow-up
after the above is green) (net **+~120 LoC** product code)
- Remove the experiment gate:
- Backend: stop passing/reading
`options.experiments.postCompactionContext` for injection.
- Frontend: always show Post-Compaction section (or hide entirely if UX
decision is to keep it internal).
- Remove the experiment toggle from Settings and remove
`EXPERIMENT_IDS.POST_COMPACTION_CONTEXT` plumbing (including schema
fields).
- Keep exclusions (`exclusions.json`) working as the user-facing opt-out
mechanism for specific items.
---
## Acceptance criteria
- Post-compaction injection is bounded by a clear, test-backed budget.
- A single oversized plan/diff cannot trap a workspace in repeated
auto-compaction loops.
- Pre-compaction diffs survive restart/crash until successfully injected
once.
- Tests cover: budget truncation, persistence load/ack, and retry guard
behavior.
</details>
---
_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `high` •
Cost: $
<!-- mux-attribution: model=openai:gpt-5.2 thinking=high costs=8.58 -->
---------
Signed-off-by: Thomas Kosiewski <[email protected]>
Summary
Background
Implementation
~/.mux/sessions/<workspaceId>/post-compaction.jsonand load them on restart.context_exceededbefore any deltas, retry once without injection (and discard pending state) to break loops.POST_COMPACTION_CONTEXTexperiment: remove schema/flag plumbing and always show the sidebar section.Validation
bun test(targeted post-compaction + experiment/telemetry tests)make static-checkRisks
📋 Implementation Plan
Stabilize “Post-Compaction Context” (graduate from experiment)
Context / Why
The
POST_COMPACTION_CONTEXTexperiment re-injects plan file content and edited-file diffs (and currently also a TODO list) after history compaction so the model still has the critical context that compaction removed.To remove the experiment gate and make this feature always-on, we need to ensure:
Evidence (repo pointers)
src/node/services/agentSession.ts:getPostCompactionAttachmentsIfNeeded()injects immediately after compaction, then everyTURNS_BETWEEN_ATTACHMENTSturns.src/node/services/compactionHandler.ts: stores pending post-compaction diffs in-memory only (cachedFileDiffs+postCompactionAttachmentsPending).src/node/services/aiService.ts→src/browser/utils/messages/modelMessageTransform.ts:injectPostCompactionAttachments()inserts a synthetic user message after the compaction summary.src/browser/utils/messages/attachmentRenderer.ts: renders plan/diffs/todos with no total size cap.src/common/constants/attachments.ts: per-file diff truncation (MAX_FILE_CONTENT_SIZE = 50_000) +MAX_EDITED_FILES = 10, but no global cap.Goals
context_exceeded, retry once without (or with smaller) injection instead of triggering repeated compactions.Non-goals
Plan (recommended approach)
1) Persist pending post-compaction diffs across restarts/crashes (net +~220 LoC product code)
Problem: After compaction, the chat history is replaced by a summary, so pre-compaction
file_edit_*diffs no longer exist in history. Today they’re only recoverable fromCompactionHandler.cachedFileDiffsin memory, so a restart loses them.Approach: Persist “pending post-compaction diffs” into the workspace session dir and load them on startup.
Implementation steps
~/.mux/sessions/<workspaceId>/post-compaction.json(exact name can vary; keep it stable).{ version: 1, createdAt: number, diffs: FileEditDiff[] }.src/node/services/compactionHandler.ts, afterextractEditedFileDiffs(messages)but before clearing history, write the JSON file (best-effort; failure must not fail compaction).Config(orgetSessionDir(workspaceId)callback) intoCompactionHandler.CompactionHandlerconstructor (or a dedicatedinit()called fromAgentSession), read and validate the JSON.cachedFileDiffsandpostCompactionAttachmentsPending = true.consumePendingDiffs()with a two-phase API:peekPendingDiffs(): FileEditDiff[] | null(does not clear)ackPendingDiffsConsumed(): void(clears in-memory + deletespost-compaction.json)AgentSession.getPostCompactionAttachmentsIfNeeded(), usepeekPendingDiffs().src/node/services/workspaceService.tsgetPostCompactionState(), if there’s no active session (or no pending paths), fall back to readingpost-compaction.jsonand exposingdiffs[].path(filtered to exclude plan paths) astrackedFilePaths.ackPendingDiffsConsumed()after the next non-compaction stream finishes successfully (e.g., in thestream-endhandler), so a crash mid-stream does not lose the pending diffs.Notes / defensive programming
diffis string, etc.), but never crash startup.Alternative (not recommended): persist a synthetic “diff snapshot” message into history
This would survive restarts “for free”, but it defeats the purpose of compaction by reintroducing lots of tokens into history. It also risks prompt-cache churn and UI clutter.Net LoC estimate: +~80 LoC product code, but high UX/token cost.
2) Enforce a hard budget for injected post-compaction context (net +~140 LoC product code)
Problem: Even after compaction, injection can be huge (up to
10 × 50kchars of diffs plus unbounded plan file), riskingcontext_exceeded.Approach: Add a deterministic character-budget based limiter that renders a bounded subset of attachments.
Implementation steps
src/common/constants/attachments.ts(or a new constants file if preferred):MAX_POST_COMPACTION_INJECTION_CHARS(e.g.,80_000)MAX_POST_COMPACTION_PLAN_CHARS(e.g.,30_000)MAX_POST_COMPACTION_DIFFS_CHARS(e.g.,50_000) to ensure diffs don’t crowd out everything else.src/node/services/attachmentService.ts, sliceplanContenttoMAX_POST_COMPACTION_PLAN_CHARSand append a short “(truncated)” note.src/browser/utils/messages/attachmentRenderer.ts, addrenderAttachmentsToContentWithBudget(attachments, { maxChars }).src/browser/utils/messages/modelMessageTransform.ts, haveinjectPostCompactionAttachments()use the budgeted renderer.3) Loop breaker: auto-retry once when injection triggers
context_exceeded(net +~180 LoC product code)Problem: If the injected context triggers
context_exceeded, the UI’s auto-compaction can repeatedly compact an already-compacted history.Approach: On
context_exceededfailures where we know post-compaction injection was included, retry once with injection suppressed (or with a much smaller budget) before emittingstream-error.Implementation steps
src/node/services/agentSession.tsstreamWithHistory(), store a boolean likethis.activeStreamHadPostCompactionInjectionwhenpostCompactionAttachments?.length > 0.Set<string>keyed by the failing assistantmessageId, or a simple “already retried once” flag).handleStreamError()(or wherevercontext_exceededis surfaced):errorType === "context_exceeded"andactiveStreamHadPostCompactionInjectionand not yet retried:streamWithHistory()once with injection suppressed.stream-errorevent if retry fails.4) Add tests for injection, budgets, persistence (net +0 LoC product code; tests only)
Unit tests
src/browser/utils/messages/attachmentRenderer.test.ts(or adjacent):src/browser/utils/messages/modelMessageTransform.test.ts:injectPostCompactionAttachments()inserts after compaction summary and uses budgeted output.Node/service tests
AgentSession+CompactionHandlerintegration:5) Graduate the feature out of the experiment system (follow-up after the above is green) (net +~120 LoC product code)
options.experiments.postCompactionContextfor injection.EXPERIMENT_IDS.POST_COMPACTION_CONTEXTplumbing (including schema fields).exclusions.json) working as the user-facing opt-out mechanism for specific items.Acceptance criteria
_Generated with
mux• Model:openai:gpt-5.2• Thinking:high• Cost: $