Skip to content

Commit 8cd9791

Browse files
fix(dev): recheck cancellation after epoch probe (review follow-up #134) (#163)
1 parent 44e31b5 commit 8cd9791

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎packages/agent-bundle/src/dev/mcp-session/mcp-session.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ export class McpSession {
315315

316316
#callToolEffect(options: McpSessionToolCallOptions): Effect.Effect<CallToolResult, unknown> {
317317
return this.#assertEpochCurrentEffect().pipe(Effect.andThen(Effect.suspend(() => {
318+
if (options.signal?.aborted) {
319+
return Effect.fail(options.signal.reason ?? new Error('MCP session tool call was aborted.'));
320+
}
318321
const requestId = options.requestId ?? randomUUID();
319322
if (requestId.trim().length === 0) {
320323
return Effect.fail(new Error('MCP session requestId must be nonempty.'));

‎packages/agent-bundle/tests/mcp-session-service.test.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,66 @@ it('rejects an already-aborted tool call without invoking the MCP SDK', async ()
864864
}
865865
}, 30_000);
866866

867+
it('rejects a tool call aborted while its epoch availability probe is pending', async () => {
868+
const root = await mkdtemp(join(tmpdir(), 'agent-bundle-persistent-mcp-probe-abort-'));
869+
const pluginData = await mkdtemp(join(tmpdir(), 'agent-bundle-persistent-mcp-probe-abort-data-'));
870+
try {
871+
let allowProbe: (() => void) | undefined;
872+
const probeBlocked = new Promise<void>((resolvePromise) => {
873+
allowProbe = resolvePromise;
874+
});
875+
let probeStarted: (() => void) | undefined;
876+
const probeStartedPromise = new Promise<void>((resolvePromise) => {
877+
probeStarted = resolvePromise;
878+
});
879+
let calls = 0;
880+
const session = new McpSession({
881+
assertEpochAvailable: async () => {
882+
probeStarted?.();
883+
await probeBlocked;
884+
},
885+
binding: { epochId: 'epoch-probe-abort', serverName: 'fixture', target: 'portable' },
886+
createClient: () => ({
887+
callTool: async () => {
888+
calls += 1;
889+
return { content: [] };
890+
},
891+
close: async () => undefined,
892+
connect: async () => undefined,
893+
...mcpCatalogStub(),
894+
}),
895+
createStdioTransport: () => stdioTransportStub() as never,
896+
createStreamableHttpTransport: () => ({}) as never,
897+
epochReference: { close: async () => undefined, root } as never,
898+
id: 'session-probe-abort',
899+
onClose: () => undefined,
900+
pluginData,
901+
resolved: {
902+
runtime: runtimeFor('portable'),
903+
server: { args: [], command: 'node', kind: 'stdio' },
904+
target: 'portable',
905+
targetRoot: root,
906+
},
907+
workspaceRoot: root,
908+
});
909+
await session.initialize();
910+
const aborted = new AbortController();
911+
const reason = new Error('cancelled during epoch probe');
912+
913+
const pending = session.callTool({ arguments: {}, name: 'fixture', signal: aborted.signal });
914+
await probeStartedPromise;
915+
aborted.abort(reason);
916+
allowProbe?.();
917+
918+
await expect(pending).rejects.toBe(reason);
919+
expect(calls).toBe(0);
920+
await session.close();
921+
} finally {
922+
await rm(root, { force: true, recursive: true });
923+
await rm(pluginData, { force: true, recursive: true });
924+
}
925+
}, 30_000);
926+
867927
it('bounds frame and event retention with an explicit replay overflow cursor', async () => {
868928
const root = await mkdtemp(join(tmpdir(), 'agent-bundle-persistent-mcp-retention-'));
869929
try {

0 commit comments

Comments
 (0)