fix(tools): stop cancelled calls before execution and discard late results

This commit is contained in:
Rudy Celekli committed 2026-10-14 14:17:54 -04:00
1 parent 6dd4d0156f
commit 691be39c1b
4 files changed
+50

No files matched your search

+2
View File
@@ -15,6 +15,8 @@
the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is
unchanged; extra cameras share it round-robin with the other packs. unchanged; extra cameras share it round-robin with the other packs.
- Cancelled tool calls refuse actions before execution and discard results that settle after cancellation.
- Treat explicitly current, complete empty vessel coverage as a successful update, - Treat explicitly current, complete empty vessel coverage as a successful update,
clearing obsolete contacts while retaining stale or incomplete snapshots. clearing obsolete contacts while retaining stale or incomplete snapshots.
+5
View File
@@ -5310,3 +5310,8 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye
Military fragments stitch by source identity. Different mapped parcels merge only Military fragments stitch by source identity. Different mapped parcels merge only
when their footprints touch or contain one another, never across a seam tolerance gap. when their footprints touch or contain one another, never across a seam tolerance gap.
### fix(tools): stop cancelled calls before execution and discard late results
Check cancellation before invoking the tool and after it resolves, including nested calls that share the same signal. This does not promise rollback of an action already completed.
+41
View File
@@ -0,0 +1,41 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { composeCatalog, defineTool } from './catalog.js';
test('cancelled catalog calls cannot execute or return late results', async () => {
let calls = 0,
release,
block = false;
const action = defineTool({
name: 'fixture_action',
kind: 'action',
title: 'Fixture',
description: 'Offline fixture',
inputSchema: { type: 'object' },
run: async () => {
calls++;
return block
? new Promise((r) => (release = r))
: { summary: 'fixture', data: {} };
},
});
const catalog = composeCatalog({ tools: [action] });
const c = new AbortController();
c.abort();
await assert.rejects(
catalog.call('fixture_action', {}, { signal: c.signal }),
{ name: 'AbortError' },
);
assert.equal(calls, 0);
block = true;
const running = new AbortController();
const pending = catalog.call(
'fixture_action',
{},
{ signal: running.signal },
);
while (!release) await Promise.resolve();
running.abort();
release({ summary: 'late', data: {} });
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(calls, 1);
});
+2
View File
@@ -178,6 +178,7 @@ export function composeCatalog({
return chain({ tool, args, signal, ...(parent ? { parent } : {}) }); return chain({ tool, args, signal, ...(parent ? { parent } : {}) });
}; };
const execute = async ({ tool, args, signal }) => { const execute = async ({ tool, args, signal }) => {
signal?.throwIfAborted();
const problems = validateValue(tool.inputSchema, args); const problems = validateValue(tool.inputSchema, args);
if (problems.length) if (problems.length)
throw new ToolError('invalid_arguments', problems.join('; ')); throw new ToolError('invalid_arguments', problems.join('; '));
@@ -194,6 +195,7 @@ export function composeCatalog({
} catch (error) { } catch (error) {
throw fromSourceError(error); throw fromSourceError(error);
} }
signal?.throwIfAborted();
if (!result || typeof result.summary !== 'string' || !result.data) if (!result || typeof result.summary !== 'string' || !result.data)
throw new TypeError(`${tool.name} returned no summary or data`); throw new TypeError(`${tool.name} returned no summary or data`);
if ( if (