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
Think: expose immutable per-turn messenger/thread context on beforeTurn ctx (concurrent turns race on _activeMessengerContext) #1894
beforeTurn(ctx) exposes no immutable, per-turn identity for which messenger thread/session the turn belongs to. The only way to recover it from inside beforeTurn is getMessengerContext(), which reads the mutable _activeMessengerContext field — and that field is set outside the TurnQueue, so it races under concurrent turns on one DO. This makes per-thread turn logic (e.g. scoping the model-visible transcript to the triggering thread) impossible to implement correctly from the public hook. Related to (but distinct from) the closed #1349 — that was the this.session / useAgentChat cache-key layer; this is the beforeTurn/ctx layer.
TurnQueue.enqueue runs the turn body only after await previousTurn, and the user-message append happens inside that serialized body.
The ctx passed to beforeTurn is { system, messages, tools, model, continuation, body } — the messages are already assembled model messages (no per-message messenger metadata), and there is no thread/session id.
The race
Same DO handles two near-simultaneous events for threads A then B (DOs interleave at await points):
Event B interleaves during A's awaits: _activeMessengerContext = B (overwrite); enqueues turn B behind A.
Turn A's body runs; its beforeTurn reads getMessengerContext() → B. Turn A now operates with thread B's context.
Anchoring on this.messages instead (newest tagged user message) is race-free for fresh turns, because appends are serialized by the queue — but continuation turns (_scheduleAutoContinuation → _admitTurn({ continuation: true })) re-enter the queue as separate turns, so a cross-thread message can interleave between a tool-call turn and its continuation and poison the anchor. There is no fully-correct fix available from beforeTurn today.
Ask
Thread an immutable per-turn messenger/thread identity through the turn and surface it on the beforeTurnctx (e.g. ctx.messenger / ctx.threadId), captured at admission time so it flows with the unit of work through TurnQueue rather than being read from a shared mutable field. (Alternatively/additionally, stamp the resolved thread on every appended message, not just the user trigger.) That would let per-thread turn logic be written correctly without reading _activeMessengerContext or inferring position in a shared history.
+1 — hitting this on think@0.13.0 / agents@0.17.4.
Use case: one Agent DO per user as the isolation boundary, with a multi-thread chat UI on top. We want threads to be independent conversations without sharding a DO per thread. Today they're cosmetic to the model — beforeTurn has no thread identity and history is assembled from the single this.session before it runs, so a turn sees the globally-latest tail rather than the active thread's.
SessionManager.getSession(id) already exists — the primitive's there; Think just binds one Session. What would unblock us: a race-free, per-turn session id resolved inside the TurnQueue so history is read from that session. Read-only ctx exposure alone wouldn't be enough without the selection seam.
Happy to test a preview build against a real per-user multi-thread workload.
Use case: one Agent DO per user as the isolation boundary, with a multi-thread chat UI on top. We want threads to be independent conversations without sharding a DO per thread.
@apercivall We generally use a separate DO for each conversation/thread. If you want to share a DO across multiple conversations, sub-agents are the current solution (see the multi-ai chat example). Is there a reason you are otherwise trying to keep multiple user conversations inside a single DO? There is a bit of a tricky situation with messengers that use a single webhook across all users and threads, because they currently resolve to a single DO. It's workaround-able by owning more of the routing at the Worker-level with getAgentByName, but I'd like to improve this story in future.
@poggersdave Regarding the original problem of accessing _activeMessengerContext -- in line with the above, it's most often the case that thread context shouldn't change from one message to the next, but there is nonetheless a race in the case that you are smushing messages from multiple threads into a single Think conversation. I think the most common failure mode for this is likely multi-user conversations with two users posting into a single thread concurrently.
Activity
apercivall commentedon Jul 16, 2026
+1 — hitting this on
think@0.13.0/agents@0.17.4.Use case: one Agent DO per user as the isolation boundary, with a multi-thread chat UI on top. We want threads to be independent conversations without sharding a DO per thread. Today they're cosmetic to the model —
beforeTurnhas no thread identity and history is assembled from the singlethis.sessionbefore it runs, so a turn sees the globally-latest tail rather than the active thread's.SessionManager.getSession(id)already exists — the primitive's there; Think just binds oneSession. What would unblock us: a race-free, per-turn session id resolved inside the TurnQueue so history is read from that session. Read-onlyctxexposure alone wouldn't be enough without the selection seam.Happy to test a preview build against a real per-user multi-thread workload.
threepointone commentedon Aug 5, 2026
ai v7's context might help here?
cjol commentedon Aug 11, 2026
@apercivall We generally use a separate DO for each conversation/thread. If you want to share a DO across multiple conversations, sub-agents are the current solution (see the multi-ai chat example). Is there a reason you are otherwise trying to keep multiple user conversations inside a single DO? There is a bit of a tricky situation with messengers that use a single webhook across all users and threads, because they currently resolve to a single DO. It's workaround-able by owning more of the routing at the Worker-level with
getAgentByName, but I'd like to improve this story in future.@poggersdave Regarding the original problem of accessing
_activeMessengerContext-- in line with the above, it's most often the case that thread context shouldn't change from one message to the next, but there is nonetheless a race in the case that you are smushing messages from multiple threads into a single Think conversation. I think the most common failure mode for this is likely multi-user conversations with two users posting into a single thread concurrently.feat(think): expose the running turn's identity to hooks and tools (#…