From 5a8ccacebdb793d3936f3caeb25fb2d663302785 Mon Sep 17 00:00:00 2001 From: nvtoan0201-swe Date: Wed, 30 Sep 2026 11:46:35 +0700 Subject: [PATCH] fix: never call the provider for a task cancelled during model setup executeModelTask reads the agent identity and memories before registering its abort listener, and addEventListener never fires for an already-aborted signal. A task paused or cancelled inside that window still started a full model run. Check the signal after the setup reads so the run is rejected before any provider request. The new regression test cancels while the identity read is pending and asserts the fixture provider saw zero requests. --- apps/server/src/engine/model.ts | 3 +++ tests/model-worker.test.ts | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/apps/server/src/engine/model.ts b/apps/server/src/engine/model.ts index d7c43330a..c45f13e5e 100644 --- a/apps/server/src/engine/model.ts +++ b/apps/server/src/engine/model.ts @@ -293,6 +293,9 @@ export async function executeModelTask( "identity", ); const memories = await service.db.list<{ text: string; source: string }>(owner, "memories"); + // The abort listener below cannot fire for an already-aborted signal. A task + // paused or cancelled during these reads must not start a model run. + ctx.signal.throwIfAborted(); const agent = tanstackAgent({ model: config.model, maxSteps: 16, diff --git a/tests/model-worker.test.ts b/tests/model-worker.test.ts index b4e566c6e..8e32890a8 100644 --- a/tests/model-worker.test.ts +++ b/tests/model-worker.test.ts @@ -291,3 +291,46 @@ test("browser reads keep observation identity distinct while reusing one session assert.equal(sessionIds.size, 1, "both reads should reuse the same browser session"); assert.equal(new Set(webEvidence.map((item) => item.id)).size, 2); }); + +test("a task cancelled while the model run is being prepared never calls the provider", async (t) => { + const directory = await mkdtemp(join(tmpdir(), "openmuse-model-cancel-")); + const db = await createStore(); + const { requests } = await modelFixture(t, () => undefined); + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + const get = db.get.bind(db); + db.get = async (owner, kind, id) => { + if (kind === "agent-settings" && id === "identity") await delay(100); + return get(owner, kind, id); + }; + const server = await createApp(db, { + mode: "sample", + port: 8787, + host: "127.0.0.1", + publicUrl: "http://localhost:8787", + dataDir: directory, + agentBackend: "model", + intelligenceApiKey: "test-project-key-never-sent", + model: "openai/fixture", + googleRedirectUri: "http://localhost:8787/api/google/callback", + allowedOrigins: [], + }); + try { + const task = await server.agent.createTask("owner", { prompt: "Plan my week in detail" }); + const tick = server.agent.worker.tick(); + for (let attempt = 0; attempt < 100; attempt++) { + if ((await server.agent.getTask("owner", task.id)).status === "running") break; + await delay(5); + } + assert.equal((await server.agent.getTask("owner", task.id)).status, "running"); + await server.agent.control("owner", task.id, "pause"); + await tick; + + const saved = await server.agent.getTask("owner", task.id); + assert.equal(saved.status, "paused"); + assert.equal(requests.length, 0); + } finally { + await server.agent.stop(); + await db.close(); + await rm(directory, { recursive: true, force: true }); + } +});