mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
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:
1 parent
3072449c94
commit
35e21dd676
2 files changed
+18
-3
No files matched your search
+8
-3
@@ -391,7 +391,7 @@ export interface ToolRunnerDeps {
|
|||||||
request: ApiRequest;
|
request: ApiRequest;
|
||||||
/** Ids of sessions this process started: the default allowlist for prompting and deleting. */
|
/** Ids of sessions this process started: the default allowlist for prompting and deleting. */
|
||||||
started: Set<string>;
|
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;
|
clientId: string;
|
||||||
now?: () => number;
|
now?: () => number;
|
||||||
}
|
}
|
||||||
@@ -470,6 +470,11 @@ function readEnum<T extends string>(args: Record<string, unknown>, key: string,
|
|||||||
|
|
||||||
export function createToolRunner(deps: ToolRunnerDeps) {
|
export function createToolRunner(deps: ToolRunnerDeps) {
|
||||||
const { config } = deps;
|
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 {
|
function context(): AgentContext {
|
||||||
if (!config.apiUrl) {
|
if (!config.apiUrl) {
|
||||||
@@ -664,7 +669,7 @@ export function createToolRunner(deps: ToolRunnerDeps) {
|
|||||||
const body = buildSendBody(text, {
|
const body = buildSendBody(text, {
|
||||||
enter: true,
|
enter: true,
|
||||||
clientId: deps.clientId,
|
clientId: deps.clientId,
|
||||||
seq: (deps.now ?? Date.now)(),
|
seq: nextSeq(),
|
||||||
wait: wait ? true : undefined,
|
wait: wait ? true : undefined,
|
||||||
waitTimeout: wait ? timeoutMs : undefined,
|
waitTimeout: wait ? timeoutMs : undefined,
|
||||||
});
|
});
|
||||||
@@ -788,7 +793,7 @@ export function createToolRunner(deps: ToolRunnerDeps) {
|
|||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: `/api/v1/sessions/${encodeURIComponent(row.id)}/input`,
|
path: `/api/v1/sessions/${encodeURIComponent(row.id)}/input`,
|
||||||
body: buildInterruptBody(deps.clientId, (deps.now ?? Date.now)()),
|
body: buildInterruptBody(deps.clientId, nextSeq()),
|
||||||
},
|
},
|
||||||
signal
|
signal
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -411,6 +411,16 @@ describe('the tools', () => {
|
|||||||
expect(result.structured).toMatchObject({ outcome: 'not_delivered' });
|
expect(result.structured).toMatchObject({ outcome: 'not_delivered' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('input seq stays strictly increasing within one millisecond, across send_prompt and interrupt_session', async () => {
|
||||||
|
// The runner's clock is frozen at 1: the server drops a repeated seq while still answering 200.
|
||||||
|
const { tools, calls } = runner(config({ allowAnySession: true }));
|
||||||
|
await tools.run('send_prompt', { session_id: USERS, text: 'one', wait: false });
|
||||||
|
await tools.run('send_prompt', { session_id: USERS, text: 'two', wait: false });
|
||||||
|
await tools.run('interrupt_session', { session_id: USERS });
|
||||||
|
const seqs = calls.filter((c) => c.path.endsWith('/input')).map((c) => c.body?.seq);
|
||||||
|
expect(seqs).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
it('wait_for_session needs exactly one of until and match, and passes a marker literally', async () => {
|
it('wait_for_session needs exactly one of until and match, and passes a marker literally', async () => {
|
||||||
const { tools, calls } = runner(config());
|
const { tools, calls } = runner(config());
|
||||||
expect((await tools.run('wait_for_session', { session_id: USERS })).isError).toBe(true);
|
expect((await tools.run('wait_for_session', { session_id: USERS })).isError).toBe(true);
|
||||||
|
|||||||
Reference in new issue
Block a user