diff --git a/CHANGELOG.md b/CHANGELOG.md index 1844de110..a79441fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,19 +24,7 @@ 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. -- Street Level counts unique tiles before ranking so overlapping dateline halves - in polar views do not waste slots in the 16-tile overview budget. - -- Directions no longer shows a truncation notice for a route with exactly 200 - turns, and a roundabout at the 200th turn keeps its exit distance. - -- Weather records keep missing conditions unknown and refuse summaries without a measured temperature. - -- Tool string length validation counts Unicode characters correctly, including characters outside the basic multilingual plane. - -- Director timeline seeks cross the dateline along the short longitude arc. - -- Boundary capability checks recover after unsuccessful or malformed status responses, using the existing retry backoff. +- Cancelled catalog queries refuse execution when their signal is already aborted. - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 2171d6617..bd346b4f5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -101,6 +101,10 @@ lists at 25 rows by default. Voice offers the same queries next to its app actions: the session lists them, and the browser runs them through the same catalog, loaded on first use. See [tools and the MCP server](TOOLS.md). +Catalog calls check their cancellation signal immediately before executing a +tool. Already-cancelled queries do not start; cancellation after execution starts +continues to follow the existing tool and caller behavior. + ## Cyber HUD — September 30, 2026 Display > HUD > Layout includes Cyber, also available through the HUD voice diff --git a/src/tools/catalog-cancellation.test.mjs b/src/tools/catalog-cancellation.test.mjs new file mode 100644 index 000000000..0f666b262 --- /dev/null +++ b/src/tools/catalog-cancellation.test.mjs @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { composeCatalog, defineTool } from './catalog.js'; + +for (const intercept of [false, true]) { + test(`cancelled query refuses execution${intercept ? ' after an awaited interceptor' : ''}`, async () => { + let calls = 0; + const controller = new AbortController(); + const query = defineTool({ + name: 'fixture_query', + kind: 'query', + title: 'Fixture', + description: 'Offline fixture', + inputSchema: { type: 'object' }, + run: async () => { + calls++; + return { summary: 'fixture', data: {} }; + }, + }); + const catalog = composeCatalog({ + tools: [query], + interceptors: intercept + ? [ + async (call, next) => { + await Promise.resolve(); + controller.abort(); + return next(call); + }, + ] + : [], + }); + if (!intercept) controller.abort(); + await assert.rejects( + catalog.call('fixture_query', {}, { signal: controller.signal }), + { name: 'AbortError' }, + ); + assert.equal(calls, 0); + const fresh = composeCatalog({ tools: [query] }); + assert.equal( + ( + await fresh.call( + 'fixture_query', + {}, + { signal: new AbortController().signal }, + ) + ).summary, + 'fixture', + ); + assert.equal(calls, 1); + }); +} diff --git a/src/tools/catalog.js b/src/tools/catalog.js index 4d5106b6e..d617a9f28 100644 --- a/src/tools/catalog.js +++ b/src/tools/catalog.js @@ -178,6 +178,7 @@ export function composeCatalog({ return chain({ tool, args, signal, ...(parent ? { parent } : {}) }); }; const execute = async ({ tool, args, signal }) => { + signal?.throwIfAborted(); const problems = validateValue(tool.inputSchema, args); if (problems.length) throw new ToolError('invalid_arguments', problems.join('; '));