You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Provider claude-agent: system messages in history are dropped, so keyed instruction updates never reach Claude #62916
On the claude-agent provider, system messages in the conversation history never reach Claude.
The keyed session instructions API delivers every update after the first request as exactly such a
message, so a plugin's later ctx.session.instructions.entry.put or remove never reaches Claude.
ctx.session.instructions.entry.put / remove (experimental): the first value goes into the
request's system parts. Every later change is appended at the next step boundary as a stored system message, "Instructions updated: …"
(packages/core/src/session/message-updater.ts:154, instruction-state.ts, runner/to-llm-message.ts).
The connector skips system messages, both when it converts the prompt for Claude and when it
fingerprints history:
On other providers the update message is part of the request. On claude-agent the model keeps the
first value for the rest of the session.
This is related to #61248 / #61273 but not the same: those cover system parts that a plugin
pushes into event.system, which the provider snapshots too early. This one covers system-role messages in history. #61273 does not touch the conversion above.
Expected: an instruction update reaches Claude without changing the system prompt (a change
there restarts the Claude process, per the provider spec). One option is to render it into the user
content, the way other notices are.
Found by reading the source at v2.2.5 (9bbb8ce); the lines above are unchanged on dev. It was
not reproduced against a live Claude account.
Plugins
A custom server plugin (any plugin that uses ctx.session.instructions.entry)
OpenCode version
2.2.5
Steps to reproduce
In a server plugin, call ctx.session.instructions.entry.put({ sessionID, key: "repro.state", value: "A" }) before the session's first request.
After the first step, call it again with "B".
Run the session on claude-agent/<model> and on openai/<model>, and ask the model for the current value.
Expected from the source: OpenAI sees the "Instructions updated" message with B; Claude still answers A.
Thanks for the detailed report. I confirmed this at the connector level on v2.2.5, and it still happens on the latest dev (617a770ae). There's no Claude account in this sandbox, so I couldn't check what the model actually answers. What I could check is the content the connector sends to Claude.
The script below loads the claude-agent connector's own modules. It gives them the second-turn prompt OpenCode builds after an instruction update: base system prompt, user, assistant, the system message "Instructions updated: repro.state … B", then a new user question.
Fresh or rebuilt Claude session:ConvertToClaudeMessage.content() returns only the user and assistant turns. The update is missing.
Continuing a live session:Fingerprint.groups() skips the system message, so SyncDecision.decide picks start: 4, which comes after the update. CatchUp.content() then sends Claude only the new question.
Other providers get the update, because runner/to-llm-message.ts keeps it as a system message. The baseline system parts only hold the first value, so Claude keeps A.
The update mechanism covers every instruction source, not just plugin entries. So changes to things like AGENTS.md or MCP and skill instructions during a session probably don't reach Claude on this provider either. I haven't tested those separately.
Related: #61248 / #61273 (plugin system parts read too early) and #61551 (forks replaying superseded instructions).
Reproduction script (repro.ts)
// Reproduces #62916: the claude-agent connector drops system-role messages from history,// which is how OpenCode delivers "Instructions updated" notices after the first request.// Run from an OpenCode checkout: REPO=/path/to/opencode bun /workspace/repro/repro.tsconstrepo=process.env.REPO??"/workspace/repo"constbase=`${repo}/packages/core/src/plugin/provider/claude-agent`const{ ConvertToClaudeMessage }=awaitimport(`${base}/language-model/convert-to-claude-message.ts`)const{ Fingerprint }=awaitimport(`${base}/sync/fingerprint.ts`)const{ SyncDecision }=awaitimport(`${base}/sync/decide.ts`)const{ CatchUp }=awaitimport(`${base}/sync/catch-up.ts`)// The prompt OpenCode sends on the second turn after a plugin changed key repro.state from A to B.// (runner/to-llm-message.ts maps the stored SessionMessage.System to a role "system" message.)constupdate="Instructions updated: repro.state\nrepro.state = B"constprompt: any[]=[{role: "system",content: "base system prompt (repro.state = A)"},{role: "user",content: [{type: "text",text: "remember the value"}]},{role: "assistant",content: [{type: "text",text: "ok"}]},{role: "system",content: update},{role: "user",content: [{type: "text",text: "what is repro.state now?"}]},]consttext=(blocks: any[])=>blocks.map((b)=>(b.type==="text" ? b.text : `[${b.type}]`)).join("\n")console.log("== ConvertToClaudeMessage.content(full prompt) (fresh/rebuilt Claude session) ==")constfresh=text(ConvertToClaudeMessage.content(prompt))console.log(fresh)console.log("contains update:",fresh.includes("repro.state = B"))// Continuing a live Claude session whose record already has the first user+assistant turn.constgroups=Fingerprint.groups(prompt.slice(0,3))constrecord: any={history: groups.map((g: any)=>({hash: g.hash}))}constdecision=SyncDecision.decide(prompt,record)console.log("\n== continuing Claude session ==")console.log("groups:",Fingerprint.groups(prompt).map((g: any)=>`${g.role}[${g.start},${g.end})`).join(" "))console.log("decision:",JSON.stringify(decision))constcont=text(CatchUp.content(prompt,decision.start))console.log("content sent to Claude:\n"+cont)console.log("contains update:",cont.includes("repro.state = B"))
Activity
opencodejr commentedon Nov 9, 2026
Thanks for the detailed report. I confirmed this at the connector level on v2.2.5, and it still happens on the latest
dev(617a770ae). There's no Claude account in this sandbox, so I couldn't check what the model actually answers. What I could check is the content the connector sends to Claude.The script below loads the
claude-agentconnector's own modules. It gives them the second-turn prompt OpenCode builds after an instruction update: base system prompt, user, assistant, thesystemmessage "Instructions updated: repro.state … B", then a new user question.ConvertToClaudeMessage.content()returns only the user and assistant turns. The update is missing.Fingerprint.groups()skips the system message, soSyncDecision.decidepicksstart: 4, which comes after the update.CatchUp.content()then sends Claude only the new question.Other providers get the update, because
runner/to-llm-message.tskeeps it as a system message. The baseline system parts only hold the first value, so Claude keepsA.The update mechanism covers every instruction source, not just plugin entries. So changes to things like AGENTS.md or MCP and skill instructions during a session probably don't reach Claude on this provider either. I haven't tested those separately.
Related: #61248 / #61273 (plugin system parts read too early) and #61551 (forks replaying superseded instructions).
Reproduction script (repro.ts)
Run it:
bun repro.ts