What problem are you trying to solve?
I’m building a personal agent where a single Telegram chat hosts multiple
sessions and the user can run /new-session, /list-sessions, /resume 2
and the same chat switches between them (same idea as “new chat” in any
chat product, just on a messaging surface). I keep the session registry on
my side; all I need from eve is to control which session an inbound message
lands on.
Turns out that’s not possible right now. Every channel computes the
continuation token internally and there’s no way for the host to influence
it. For a private Telegram chat the token is pinned to ${chatId}::
(continuationTokenFromState drops the conversationId), so the chat is
welded to one session forever. The new chatSdkChannel has the same
property, the token is hardcoded to the serialized thread id at both
call sites (bridgeSend and receive), and ChatSdkSendOptions doesn’t
let you override it even though I’m the one writing the handler and calling
send.
Things I tried before filing this:
session.setContinuationToken() - only works on the live session while
it’s running a step. getSession(id) returns the inert Session, which
per the docs can’t mutate the token, so I can’t re-key a parked session
from the outside.
Writing a custom channel. The Telegram inbound helpers are all exported,
but buildTelegramHandle isn’t, so owning dispatch means re-implementing
~100 lines of sends/splitting/typing/HITL that you’ve already
battle-tested. Didn’t want to maintain that fork.
Passing a different thread to chatSdkChannel’s send (“thread == session,
so mint one thread per session”). Doesn’t survive contact with adapters:
@chat-adapter/telegram encodes telegram:[:], validates
the shape on decode, and parses the delivery target out of the id — DMs
have exactly one possible thread.
What I ended up doing is patching eve via pnpm patchedDependencies (in
production since 0.12.0): a resolveContinuationToken config hook on
telegramChannel, consulted at both dispatch send-sites, null falls back
to the default token. It’s tiny and it’s been solid, but I’d love to delete
the patch.
Proposed solution
Two options, happy to PR either or both:
- continuationToken on ChatSdkSendOptions. In the chat-sdk channel
I already author the handler, so an explicit override is the natural shape:
bot.onDirectMessage(async (thread, message) => {
const sessionKey = await controlPlane.activeSession(thread.id); // mine
await send(messageToUserContent(message), { thread, continuationToken: sessionKey });
});
Implementation is basically options.continuationToken ?? serialized.id in
bridgeSend, plus the same optional field on receive. The thread still
drives delivery/state/HITL rendering — this only selects the session, so
replies keep landing on the originating thread.
- resolveContinuationToken on the first-class channels (Telegram,
Slack, Discord, …), same style as the existing resolveInputAuth hook:
telegramChannel({
resolveContinuationToken: async (state) => {
// state has chatId/chatType/conversationId/messageThreadId
return await controlPlane.activeSession(state.chatId); // or null → default
},
});
This is exactly what my patch does today:
async function resolveToken(config, state) {
const token = await config.resolveContinuationToken?.(state);
return token != null ? token : continuationTokenFromState(state);
}
Both are opt-in with zero behavior change when unset — returning null
falls back to today’s derivation, so existing conversations keep their
sessions. The host passes channel-local raw tokens and the framework still
namespaces them, so this doesn’t touch cross-channel semantics. send’s
existing contract does the rest: unknown token starts a session, known
token resumes it.
Alternatives considered
- A mode: "new"-style option — handles /new-session but can’t address an arbitrary existing session for /resume, and mode is already taken by RunMode.
- Sessions as provider threads (forum topics) — works where real threads exist, admits I may use it anyway, but doesn’t exist for DMs/WhatsApp/SMS.
What problem are you trying to solve?
I’m building a personal agent where a single Telegram chat hosts multiple
sessions and the user can run /new-session, /list-sessions, /resume 2
and the same chat switches between them (same idea as “new chat” in any
chat product, just on a messaging surface). I keep the session registry on
my side; all I need from eve is to control which session an inbound message
lands on.
Turns out that’s not possible right now. Every channel computes the
continuation token internally and there’s no way for the host to influence
it. For a private Telegram chat the token is pinned to ${chatId}::
(continuationTokenFromState drops the conversationId), so the chat is
welded to one session forever. The new chatSdkChannel has the same
property, the token is hardcoded to the serialized thread id at both
call sites (bridgeSend and receive), and ChatSdkSendOptions doesn’t
let you override it even though I’m the one writing the handler and calling
send.
Things I tried before filing this:
session.setContinuationToken() - only works on the live session while
it’s running a step. getSession(id) returns the inert Session, which
per the docs can’t mutate the token, so I can’t re-key a parked session
from the outside.
Writing a custom channel. The Telegram inbound helpers are all exported,
but buildTelegramHandle isn’t, so owning dispatch means re-implementing
~100 lines of sends/splitting/typing/HITL that you’ve already
battle-tested. Didn’t want to maintain that fork.
Passing a different thread to chatSdkChannel’s send (“thread == session,
so mint one thread per session”). Doesn’t survive contact with adapters:
@chat-adapter/telegram encodes telegram:[:], validates
the shape on decode, and parses the delivery target out of the id — DMs
have exactly one possible thread.
What I ended up doing is patching eve via pnpm patchedDependencies (in
production since 0.12.0): a resolveContinuationToken config hook on
telegramChannel, consulted at both dispatch send-sites, null falls back
to the default token. It’s tiny and it’s been solid, but I’d love to delete
the patch.
Proposed solution
Two options, happy to PR either or both:
I already author the handler, so an explicit override is the natural shape:
bot.onDirectMessage(async (thread, message) => {
const sessionKey = await controlPlane.activeSession(thread.id); // mine
await send(messageToUserContent(message), { thread, continuationToken: sessionKey });
});
Implementation is basically options.continuationToken ?? serialized.id in
bridgeSend, plus the same optional field on receive. The thread still
drives delivery/state/HITL rendering — this only selects the session, so
replies keep landing on the originating thread.
Slack, Discord, …), same style as the existing resolveInputAuth hook:
telegramChannel({
resolveContinuationToken: async (state) => {
// state has chatId/chatType/conversationId/messageThreadId
return await controlPlane.activeSession(state.chatId); // or null → default
},
});
This is exactly what my patch does today:
async function resolveToken(config, state) {
const token = await config.resolveContinuationToken?.(state);
return token != null ? token : continuationTokenFromState(state);
}
Both are opt-in with zero behavior change when unset — returning null
falls back to today’s derivation, so existing conversations keep their
sessions. The host passes channel-local raw tokens and the framework still
namespaces them, so this doesn’t touch cross-channel semantics. send’s
existing contract does the rest: unknown token starts a session, known
token resumes it.
Alternatives considered