Skip to content

🤖 fix: keep the cached system prompt when AGENTS.md changes mid-session - #7304

Merged
ThomasK33 merged 7 commits into
mainfrom
tk/f10-custom-instructions-ledger
Oct 23, 2026
Merged

ThomasK33 merged 7 commits into
mainfrom
tk/f10-custom-instructions-ledger

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Summary

A mid-session change to a workspace's AGENTS.md no longer rewrites the whole cached Anthropic history. Inside a context segment (between compactions), the instruction-file blocks (<custom-instructions> and the <model-...>/<mode-...> sections scoped in AGENTS.md) now stay at the text the segment first sent, and Xum reads that text from the ledger (chat.jsonl), not from the live files. When the live blocks differ, Xum sends the new blocks as one system row after the newest user message, so the model follows the new instructions on that turn. Every later rebuild puts the row back in the same place, so the next turn's prefix still matches. The next compaction folds the current block into the system prompt.

Background

Measured on one host (10-22 08:12 to 10-23 01:28 UTC, 1,548 Anthropic turns, a build with #7027 and #7029): 26 cache.prefix_break events in the system section, 1.45 rewritten histories per 100 turns, the largest class left. Each break's length change equals a docs/AGENTS.md change on main (+168 chars = #7117, +378 = #7176, about +1,214 = #7234). Agents merge main into their worktree, and the next turn read the new AGENTS.md into the stable system prompt. Turn envelopes showed the volatile-tail length unchanged at every break, so the change was in the stable part.

The history already comes from the ledger. The system prompt did not: it was rebuilt from live files on every turn (turnContextAssembler.ts, buildSystemMessageFromSources). This PR makes the cached instructions come from the ledger too.

Implementation

  • New MuxMetadata.customInstructions = { text, placement: "system" | "row" } on the turn's assistant row, the same shape and rule as volatileSystemTail (🤖 fix: send a volatile system-tail change as a system row, keep the history cached #6687). Xum writes it only at a segment start and when the blocks change, not on every turn.
  • buildSystemMessageFromSources now returns the system message plus the exact instruction-file span (the text before it and the blocks). The assembler freezes that span only while the prompt still starts with the reported text, so it never parses the final prompt for tags.
  • volatileSystemTail.ts generalizes its planner to "system sections" (planSystemSection): the volatile tail and the instruction blocks are two channels with their own field, row header and row id. Instructions plan first, so their rows come first.
  • A segment that starts with no instruction blocks records an empty baseline, so a first AGENTS.md added later is a row, not a rewrite.
  • If every instruction file is deleted or emptied mid-segment, the frozen blocks stay in the system prompt and a row says the instructions were removed.
  • The row header says the blocks come from Xum, not the user, and replace the system prompt's blocks. In a live check, Haiku 5.5 ignored a bare "Update: ..." header in 2 of 3 runs and followed this wording in 4 of 4.
  • Same gating as the volatile-tail rows (Anthropic and Coder routes, models that bind thinking to the prefix). Other routes keep today's behavior.

Validation

  • Real-request prefix tests (real system prompt and tools): with AGENTS.md rewritten between turns, the tools section and cached system block are byte-identical on turns 1 to 3, turns 2 and 3 carry the same row, and the compaction turn has the new blocks. A ## Mode: edit, a removed AGENTS.md and a first AGENTS.md added mid-segment behave the same way. A </custom-instructions> line in later instructions does not widen the frozen span. Each test fails with its fix reverted.
  • tests/ipc/tokenBudgetStages.fixture.integration.test.ts grew the prompt by rewriting AGENTS.md. With this PR the request really carries both the old and the new block, so the larger estimate is correct. The fixture now grows the prompt through the workspace scratchpad, which stays live; its expected numbers are unchanged.
  • Live check on direct Anthropic (under $0.50): with a 17.2k-token block changed between turns, turn 2 read 17,167 tokens from the cache and wrote only the 17,182-token row; turns 3 and 4 read from the cache.

Risks

Medium. It changes how the system prompt is assembled for Anthropic turns.

  • Context cost: while a changed block is live, each request carries the old blocks in the cached system prompt plus the full new blocks in the row. For this repo's AGENTS.md plus a global ~/.xum/AGENTS.md that is about 10.8k extra tokens until the next compaction. They are written to the cache once and read after that.
  • Tool: <name> sections from AGENTS.md go into tool descriptions and are not frozen here: an edit to one still rewrites the cached tool block (follow-up).
  • Old histories (no record) behave as before: the first turn sends the live block and records it. A malformed record is ignored. An older build ignores the field.
  • A follow-up PR applies the same rule to the agent_skill_read skill list.
  • Known limit (untested): a model or agent switch inside a segment keeps the first model's <model-...>/<mode-...> sections in the frozen prompt and sends the new ones in the row. A likely better rule re-freezes on a model or agent change (follow-up).
  • Known limit (low): when the instruction blocks are removed after a resume whose last row is an assistant row, Xum records the marker with placement system, which costs one extra cache rewrite (follow-up).

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $585.33

Inside a context segment, the <custom-instructions> block (AGENTS.md files and
project instructions) now stays at the text the segment first sent, read from
the ledger (MuxMetadata.customInstructions on the turn's assistant row). A
changed block goes in one system row after the newest user message, and every
rebuild puts that row back in place, so the cached system block and history
stay byte-identical. The next compaction folds the current block into the
system prompt. Same mechanism, gating and old-data rules as the volatile
system tail (prompt-cache mechanism 10, #6687).

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
… CRAP floor

Moves the section-row gating and the merge of the two section plans into
small helpers, so the custom-instructions freeze does not grow either function.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T08:57:52.215136Z d9b2461 Manual request
🔒 Security Review ✅ Completed 2026-10-23T08:58:38.915217Z d9b2461 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e stories (Haiku): 14 passed

27 agent steps · 7 replayed from cache · 31 model calls · 218.4k tokens (73% cached)
Spend: $0.10 of the $1.00 cap (proxy ledger)

All 14 tests in 9 files
Test Agent Time
🟢 chatRestart.e2e.ts · 1 passed 2 steps · 4 calls 24.8s
🟢 a chat survives a restart of the server 2 steps · 4 calls 24.8s
🟢 compaction.e2e.ts · 1 passed 3 steps · 7 calls 44.8s
🟢 /compact replaces the old chat with a summary 3 steps · 7 calls 44.8s
🟢 pluginTrustRestart.e2e.ts · 1 passed 4 steps · 2 calls 11.1s
🟢 a trusted plugin hook stays trusted after the server restarts 4 steps · 2 calls 11.1s
🟢 settingsRoundTrip.e2e.ts · 1 passed 3 steps · 5 calls 29.8s
🟢 a chat survives trips through Settings 3 steps · 5 calls 29.8s
🟢 sidebarPerWorkspace.e2e.ts · 1 passed 4 steps · 1 call 7.6s
🟢 a new workspace starts with its right sidebar collapsed while another keeps it open 4 steps · 1 call 7.6s
🟢 smoke.e2e.ts · 1 passed 2 steps · 1 call 2.8s
🟢 the agent opens the playground workspace 2 steps · 1 call 2.8s
🟢 streamErrors.e2e.ts · 1 passed 1 step · 1 call 9.6s
🟢 stream errors show in the chat, and the chat recovers 1 step · 1 call 9.6s
🟢 todoNudge.e2e.ts · 1 passed 2 steps · 4 calls 23.6s
🟢 the agent finishes open TODO items after the turn-end nudge 2 steps · 4 calls 23.6s
🟢 touchAlignment.e2e.ts · 6 passed 6 steps · 6 calls 30.0s
🟢 the Settings close button is level with the title on a touch screen 390px wide 1 step · 1 call 4.7s
🟢 the Settings close button is level with the title on a touch screen 768px wide 1 step · 1 call 5.2s
🟢 Backup settings checkboxes are small and level with their labels on a touch phone 1 step · 1 call 4.7s
🟢 the heartbeat checkbox is small and level with its label on a touch phone 1 step · 1 call 6.4s
🟢 a reply's model and time stay on one line on a touch phone 1 step · 1 call 3.2s
🟢 the workspace actions menu stays on the title row on a touch phone 1 step · 1 call 5.8s

e2e 0.20.0 · 3m 7s · web · run artifacts

🟢 e2e Electron stories (Haiku): 3 passed

3 agent steps · 3 model calls · 8.1k tokens (21% cached)
Spend: $0.02 of the $1.00 cap (proxy ledger)

All 3 tests in 3 files
Test Agent Time
🟢 electron/deepLink.e2e.ts · 1 passed 1 step · 1 call 6.6s
🟢 a deep link of a second launch starts a chat in the running app 1 step · 1 call 6.6s
🟢 electron/menuBar.e2e.ts · 1 passed 1 step · 1 call 5.6s
🟢 the workspace menu bar is the desktop title bar 1 step · 1 call 5.6s
🟢 electron/smoke.e2e.ts · 1 passed 1 step · 1 call 8.7s
🟢 Xum Electron opens the playground workspace 1 step · 1 call 8.7s

e2e 0.20.0 · 23.0s · electron · run artifacts

🟢 e2e agent repros (Haiku): no tests selected

Spend: $0.00 of the $1.00 cap (proxy ledger)

e2e 0.20.0 · 5.3s · 2 targets (web, phone) · run artifacts

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

ℹ️ 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/turnContextAssembler.ts Outdated
Comment thread src/node/services/turnContextAssembler.ts Outdated
Comment thread src/node/services/turnContextAssembler.ts Outdated
Codex P2 on #7304. buildSystemMessageFromSources now returns where its
instruction-file blocks are: <custom-instructions> plus the <model-...> and
<mode-...> sections scoped in those files, one contiguous span. The assembler
freezes that span instead of parsing the final prompt for the closing tag, so
a later "</custom-instructions>" line can no longer widen the span over the
additional instructions, and a Mode:/Model: section edit in .xum/AGENTS.md
goes in the row instead of rewriting the cached system prompt. Middleware that
edits the prompt before the span end leaves the prompt as sent (no record).
Replay sends the recorded prompt as is and lets the records decide the rows.
The row header now says the blocks come from Xum and must be followed: live
on Haiku 5.5 the old "Update:" header was followed in 1 of 3 runs, this one in
4 of 4.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
…al as a row

Codex P2 on #7304. When the instruction files stop producing any block mid-
segment (the only AGENTS.md was deleted or emptied), the system prompt keeps
the frozen block and a row tells the model the custom instructions were
removed, until the next compaction. A history without a frozen record sends
no block and records nothing, as before.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
The fixture grew E by rewriting AGENTS.md between turns. Inside a context
segment that change is now sent as a row with the whole new block while the
system prompt keeps the old one, so E grew by more than the pad and the
"instruction growth" and "after a restart" cases left their bands (with an
empty HOME, as in CI). The pad and the growth go in the workspace scratchpad,
which stays in the live system prompt. Passes on main too.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->

@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: 78fcfb2b63

ℹ️ 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/systemMessage.ts
Comment thread src/node/utils/messages/volatileSystemTail.ts Outdated
… tags

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
A segment that started without an AGENTS.md (or a Model:/Mode: section)
recorded nothing, so the first block added later went straight into the
system prompt and rewrote the cached prefix. Supported routes now record
an empty baseline ({ text: "", placement: "system" }) at the segment
start, one record per segment, so the first addition becomes a
replacement row. An unchanged empty block sends no removal row.

Addresses Codex PRRT_kwDOPxxmWM6vgBAc.

---
_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

One review lane has shown "Running" on d9b2461 for over an hour, so I am requesting a fresh review at this head.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: d9b2461ac9

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

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: d9b2461ac9

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 91bb729 Oct 23, 2026
70 of 74 checks passed
@ThomasK33
ThomasK33 deleted the tk/f10-custom-instructions-ledger branch October 23, 2026 09:16
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