Skip to content

Think: Is "one agent, many concurrent chat sessions" supposed to work as a product model in Think? #1349

@Konan69

Description

@Konan69

I'm building a multi-thread chat product on Think where users have one (or more) persistent agents and multiple conversation threads open at once, like a desktop chat app. Think's session model and SessionManager seem designed for exactly this. I'm asking this as a question rather than filing a bug, because I think I might be missing the intended identity model.

On the client side, useAgentChat keys chat state off agent URL path and name, not the Think sessionId. I gave each chat panel its own path with a distinct sessionId, which seemed right, and the hook did generate different URLs per panel. But the keying still feels like it's pushing me toward encoding session identity into routes or agent names, which from a product perspective feels like work that should belong to the session layer. I spent a while thinking this was a client cache key issue before I realized a flaw?

From @cloudflare/ai-chat/dist/react.js:

// useAgentChat cache key — keyed by origin + pathname + agent class + instance name
const initialMessagesCacheKey = `${agentUrl.origin}${agentUrl.pathname}|${agent.agent ?? ""}|${agent.name ?? ""}`;

No sessionId in the key. Two panels pointing at the same agent with different sessions share the same cache entry.

I looked into the server side, and got confused. Think serves /get-messages from this.messages, which reads this.session.getHistory(). My agent switches sessions by assigning this.session = this.sessions.getSession(sessionId), which is the pattern the API suggests. But that mutates a shared pointer on the DO instance. With two chat panels open against the same agent, requests race on that pointer and whichever one calls selectSession() last wins. I verified this end to end: two panels, two distinct sessionIds, two separate get-messages requests returning 200, both coming back with the exact same 14-message history. The collision isn't client caching or URL deduplication. It's the shared mutable this.session inside the DO responding to concurrent requests.

From @cloudflare/think/dist/think.js:

// this.messages is a getter that reads from the single active session
get messages() {
    return this.session.getHistory();
}
// /get-messages always returns the one active session's history
this.onRequest = async (request) => {
    const url = new URL(request.url);
    if (url.pathname === "/get-messages" || url.pathname.endsWith("/get-messages"))
        return Response.json(this.messages);
    return _onRequest(request);
};
// onStart sets up a single session — no per-request session resolution
this.onStart = async () => {
    // ...
    const baseSession = Session$1.create(this);
    this.session = await this.configureSession(baseSession);
    this.session.getHistory();
    // ...
};

What's confusing is that the storage layer seems to get this right. SessionManager supports multiple named sessions in one DO, sessions store their own history, and the API shape maps naturally to "one persistent agent, many threads." But the runtime layer still funnels everything through a single active session pointer that isn't safe to switch under concurrent requests. So the model the API advertises and the model the runtime actually supports seem to disagree.

I'm left wondering whether I'm using this wrong. Is "one agent, many concurrently open sessions" actually intended as a first-class pattern? If so, is there a supported way to resolve session context per request without mutating shared state, something where Think's built-in endpoints like /get-messages and turn handling are session-aware on each request instead of reading from one mutable field? Or is the intended boundary closer to one chat identity per agent route or name, meaning one DO per ai agent, kind of locking you out of using shared memory or linking sessions, and SessionManager is meant for switching between sessions sequentially rather than serving them in parallel? I can build around either answer. I just can't tell which one Think is designed for right now, and the gap had me nerd sniped for a few hours before I found the actual collision point.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions