fix(mcp): keep the input seq strictly increasing within one process (#615 merge-time fix)

send_prompt and interrupt_session tagged input with seq = Date.now() under
one clientId. The server drops a seq at or below the last one it saw for that
clientId and still answers 200, so two inputs in one millisecond (parallel
tool calls) or a clock stepped back lost the second input while the tool
reported it sent. The seq is now max(last + 1, now).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-13 17:45:21 +02:00
1 parent 3072449c94
commit 35e21dd676
2 files changed
+18 -3

No files matched your search

+8 -3
View File
@@ -391,7 +391,7 @@ export interface ToolRunnerDeps {
request: ApiRequest;
/** Ids of sessions this process started: the default allowlist for prompting and deleting. */
started: Set<string>;
/** Exactly-once tag for this process's input (`seq` is the clock). */
/** Exactly-once tag for this process's input (`seq` is the clock, kept strictly increasing). */
clientId: string;
now?: () => number;
}
@@ -470,6 +470,11 @@ function readEnum<T extends string>(args: Record<string, unknown>, key: string,
export function createToolRunner(deps: ToolRunnerDeps) {
const { config } = deps;
// The server drops any seq at or below the last one it saw for this clientId (and
// still answers 200), so two inputs in one millisecond, or a clock stepped back,
// must not reuse a seq: keep it strictly increasing for the life of the process.
let lastSeq = 0;
const nextSeq = (): number => (lastSeq = Math.max(lastSeq + 1, (deps.now ?? Date.now)()));
function context(): AgentContext {
if (!config.apiUrl) {
@@ -664,7 +669,7 @@ export function createToolRunner(deps: ToolRunnerDeps) {
const body = buildSendBody(text, {
enter: true,
clientId: deps.clientId,
seq: (deps.now ?? Date.now)(),
seq: nextSeq(),
wait: wait ? true : undefined,
waitTimeout: wait ? timeoutMs : undefined,
});
@@ -788,7 +793,7 @@ export function createToolRunner(deps: ToolRunnerDeps) {
{
method: 'POST',
path: `/api/v1/sessions/${encodeURIComponent(row.id)}/input`,
body: buildInterruptBody(deps.clientId, (deps.now ?? Date.now)()),
body: buildInterruptBody(deps.clientId, nextSeq()),
},
signal
);