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
Harness: clientContext is persisted into session.history despite being documented as ephemeral, so every turn's context block replays in all later prompts #2357
The whole reproduction is four files. No model credential required, no HITL, no approvals, no channel adapters. mockModel is used purely to report which clientContext blocks are present in the prompt eve assembled for that turn.
You are a fixture agent used to inspect the prompt eve assembles.
agent/agent.ts
import{defineAgent}from"eve"import{mockModel}from"eve/evals"exportdefaultdefineAgent({// Only needed so compaction can compile against a mock model.modelContextWindowTokens: 400_000,model: mockModel(({ messages })=>{constseen=messages.filter((m)=>m.role==="user"&&m.text.includes("CTX-")).map((m)=>m.text.match(/CTX-\d+/)?.[0]??"?")return`prompt contains ${seen.length} clientContext block(s): ${seen.join(", ")}`}),})
import{defineEval}from"eve/evals"exportdefaultdefineEval({description: "clientContext from earlier turns should not appear in later prompts",tags: ["repro"],asynctest(t){for(constnof[1,2,3]){constturn=awaitt.send(`Turn ${n}`,{clientContext: `CTX-${n}`})console.log(`turn ${n} ->`,turn.message)}},})
Current vs. expected behavior
Expected
Per dist/src/client/types.d.ts (SendTurnOptions.clientContext):
Ephemeral client/page context for the next model call only.
Strings are rendered as user-role model context messages. [...] Client context rides along with a message or HITL response; it does not dispatch a turn by itself and is never persisted to durable session history.
So turn 3's prompt should contain only CTX-3.
Actual
Turn 3's prompt contains CTX-1, CTX-2 and CTX-3. One block accumulates per turn, for the life of the session.
Root cause
The two doc comments in the package contradict each other, and the harness one matches the implementation:
dist/src/client/types.d.ts, SendTurnOptions.clientContext: "never persisted to durable session history"
dist/src/harness/types.d.ts, StepInput.context: "Each entry is appended as a role: \"user\" message to session.history before the delivery message."
In dist/src/harness/tool-loop.js the turn's prompt array is seeded from session.history, each context entry is pushed onto it as a user message, and that same array is handed to handleStepResult as promptMessages and persisted:
The next turn reseeds its messages from session.history, so the blocks compound.
Impact
For the page-context use case the field is designed for (sending the user's current editor/file/page state with each message), the model ends up holding N mutually contradictory snapshots of the same state, every one framed as current, with nothing marking which is newest.
In our case an in-product assistant repeatedly told users their file still contained a problem they had already fixed, because it answered from an earlier turn's block. It also costs prompt tokens linearly in turn count, and any compaction that fires summarizes the stale blocks rather than dropping them.
Related
#529 / #762 touched this same push site and shipped deferredContext in 0.24.2, but that fix is about ordering during approval resume. The persistence behaviour is unchanged, and this repro involves no HITL at all.
Possible resolutions
Make the implementation match the documented contract: exclude context entries when persisting history. The runtime already knows which messages they are, since it pushes them itself.
Or let context be keyed, so a new block with the same key replaces the previous one instead of appending.
Or expose a way to prune/rewrite session history between turns.
(1) alone would satisfy the documented contract. (2) would additionally cover the general case, since any per-turn injected state has this problem in an append-only history.
Happy to open a PR for (1) if that is the direction you would take.
eve version
eve@0.42.0 and past version since eve@0.20.0 confirmed
Environment
OS: macOS 15.5 (arm64)
Node: v24.15.0
Package manager: npm 11.x (also reproduced with bun 1.3.5)
ai: 7.0.71 (resolved from ^7.0.58)
zod: 4.4.3
Model: not involved (eve/evals mockModel)
Reproduced locally via `eve eval`. Originally observed in production on Vercel through the
default eve channel (POST /eve/v1/session), where the app sends page context with every turn.
Where does the bug occur?
Production (eve start / deployed), Local dev (eve dev)
Deployment
No response
Build and runtime logs
There is no error and no stack trace: the bug is silent, and the accumulating prompt is the only observable. `EVE_LOG_LEVEL=debug` adds nothing beyond the run below, since nothing fails.
The `mockModel` reply is the runtime evidence, because it reports which `clientContext` blocks were present in the prompt eve assembled for that turn.
$ EVE_LOG_LEVEL=debug npx eve eval
EVALS 1
target http://127.0.0.1:57133/
turn 1 -> prompt contains 1 clientContext block(s): CTX-1
turn 2 -> prompt contains 2 clientContext block(s): CTX-1, CTX-2
turn 3 -> prompt contains 3 clientContext block(s): CTX-1, CTX-2, CTX-3
✓ repro
Results: 1 passed (1 total)
Completed in 489ms
Expected, per the documented contract, would be `1` block on every turn.
Suggested implementation prompt
Make clientContext behave as SendTurnOptions.clientContext documents it: ephemeral, for one model call, never persisted to durable session history.
Requirements:
Context entries delivered via StepInput.context may be included in the prompt for the current model call, but must not be present in session.history once the turn settles.
The harness already pushes those entries itself in tool-loop.js, so their positions are known and can be excluded when handleStepResult builds history from promptMessages.
A session that sends clientContext every turn shows prompt token usage flat in turn count rather than growing linearly.
If you would rather keep context in history and add a replace-by-key option instead, that also resolves our use case; the important part is that the docs and the runtime agree on which one it is.
Link to a minimal reproduction
https://github.com/arielweinberger/eve-clientcontext-repro
Steps to reproduce
Output:
The whole reproduction is four files. No model credential required, no HITL, no approvals, no channel adapters.
mockModelis used purely to report whichclientContextblocks are present in the prompt eve assembled for that turn.package.json{ "name": "eve-clientcontext-repro", "private": true, "type": "module", "engines": { "node": "24.x" }, "dependencies": { "eve": "0.42.0", "ai": "^7.0.58", "zod": "^4.4.3" } }agent/instructions.mdagent/agent.tsevals/evals.config.tsevals/repro.eval.tsCurrent vs. expected behavior
Expected
Per
dist/src/client/types.d.ts(SendTurnOptions.clientContext):So turn 3's prompt should contain only
CTX-3.Actual
Turn 3's prompt contains
CTX-1,CTX-2andCTX-3. One block accumulates per turn, for the life of the session.Root cause
The two doc comments in the package contradict each other, and the harness one matches the implementation:
dist/src/client/types.d.ts,SendTurnOptions.clientContext: "never persisted to durable session history"dist/src/harness/types.d.ts,StepInput.context: "Each entry is appended as arole: \"user\"message tosession.historybefore the delivery message."In
dist/src/harness/tool-loop.jsthe turn's prompt array is seeded fromsession.history, each context entry is pushed onto it as a user message, and that same array is handed tohandleStepResultaspromptMessagesand persisted:The next turn reseeds its messages from
session.history, so the blocks compound.Impact
For the page-context use case the field is designed for (sending the user's current editor/file/page state with each message), the model ends up holding N mutually contradictory snapshots of the same state, every one framed as current, with nothing marking which is newest.
In our case an in-product assistant repeatedly told users their file still contained a problem they had already fixed, because it answered from an earlier turn's block. It also costs prompt tokens linearly in turn count, and any compaction that fires summarizes the stale blocks rather than dropping them.
Related
#529 / #762 touched this same push site and shipped
deferredContextin 0.24.2, but that fix is about ordering during approval resume. The persistence behaviour is unchanged, and this repro involves no HITL at all.Possible resolutions
history. The runtime already knows which messages they are, since it pushes them itself.(1) alone would satisfy the documented contract. (2) would additionally cover the general case, since any per-turn injected state has this problem in an append-only history.
Happy to open a PR for (1) if that is the direction you would take.
eve version
eve@0.42.0 and past version since eve@0.20.0 confirmed
Environment
Where does the bug occur?
Production (
eve start/ deployed), Local dev (eve dev)Deployment
No response
Build and runtime logs
Suggested implementation prompt
Make
clientContextbehave asSendTurnOptions.clientContextdocuments it: ephemeral, for one model call, never persisted to durable session history.Requirements:
StepInput.contextmay be included in the prompt for the current model call, but must not be present insession.historyonce the turn settles.tool-loop.js, so their positions are known and can be excluded whenhandleStepResultbuildshistoryfrompromptMessages.deferredContextfix from fix(eve): defer context during tool approval #762 keeps working.client/types.d.tsandharness/types.d.tsagree.Acceptance criteria:
clientContexton N consecutive turns results in exactly one context block in turn N's prompt, not N.1 clientContext block(s)on every turn.No tool output found for function call#236, Approved HITL tool's execution result missing from history — "tool_use ids were found without tool_result blocks" #460, Linear channel: approving a HITL tool call permanently kills the session — channel context is appended after the approval response, socollectToolApprovalsnever sees it (danglingtool_use→ Anthropic 400) #529 and Anthropic provider: pending approval-parked tool calls break every subsequent model call (tool_use without adjacent tool_result → 400 → session.failed) #533 still pass.clientContextevery turn shows prompt token usage flat in turn count rather than growing linearly.If you would rather keep context in history and add a replace-by-key option instead, that also resolves our use case; the important part is that the docs and the runtime agree on which one it is.
Additional context
No response