diff --git a/CHANGELOG.md b/CHANGELOG.md index f01cfcf17..153143b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ ### Fixes - Messages: make `/stop` clear queued followups and pending session lane work for a hard abort. - Messages: make `/stop` abort active sub-agent runs spawned from the requester session and report how many were stopped. +- Sessions: ensure `sessions.delete` clears queues, aborts embedded runs, and stops sub-agents before deletion. - WhatsApp: default response prefix only for self-chat, using identity name when set. - Signal/iMessage: bound transport readiness waits to 30s with periodic logging. (#1014) — thanks @Szpadel. - Auth: merge main auth profiles into per-agent stores for sub-agents and document inheritance. (#1013) — thanks @marcmarg. diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index e2b9f5a0c..fd4ab7163 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -3,17 +3,16 @@ import fs from "node:fs"; import { abortEmbeddedPiRun, - isEmbeddedPiRunActive, - resolveEmbeddedSessionLane, waitForEmbeddedPiRunEnd, } from "../../agents/pi-embedded.js"; +import { stopSubagentsForRequester } from "../../auto-reply/reply/abort.js"; +import { clearSessionQueues } from "../../auto-reply/reply/queue.js"; import { loadConfig } from "../../config/config.js"; import { resolveMainSessionKey, type SessionEntry, updateSessionStore, } from "../../config/sessions.js"; -import { clearCommandLane } from "../../process/command-queue.js"; import { ErrorCodes, errorShape, @@ -223,8 +222,12 @@ export const sessionsHandlers: GatewayRequestHandlers = { const { entry } = loadSessionEntry(key); const sessionId = entry?.sessionId; const existed = Boolean(entry); - clearCommandLane(resolveEmbeddedSessionLane(target.canonicalKey)); - if (sessionId && isEmbeddedPiRunActive(sessionId)) { + const queueKeys = new Set(target.storeKeys); + queueKeys.add(target.canonicalKey); + if (sessionId) queueKeys.add(sessionId); + clearSessionQueues([...queueKeys]); + stopSubagentsForRequester({ cfg, requesterSessionKey: target.canonicalKey }); + if (sessionId) { abortEmbeddedPiRun(sessionId); const ended = await waitForEmbeddedPiRunEnd(sessionId, 15_000); if (!ended) { diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index b18108d94..9396c50fd 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { connectOk, embeddedRunMock, @@ -13,9 +13,39 @@ import { } from "./test-helpers.js"; import { DEFAULT_PROVIDER } from "../agents/defaults.js"; +const sessionCleanupMocks = vi.hoisted(() => ({ + clearSessionQueues: vi.fn(() => ({ followupCleared: 0, laneCleared: 0, keys: [] })), + stopSubagentsForRequester: vi.fn(() => ({ stopped: 0 })), +})); + +vi.mock("../auto-reply/reply/queue.js", async () => { + const actual = await vi.importActual( + "../auto-reply/reply/queue.js", + ); + return { + ...actual, + clearSessionQueues: sessionCleanupMocks.clearSessionQueues, + }; +}); + +vi.mock("../auto-reply/reply/abort.js", async () => { + const actual = await vi.importActual( + "../auto-reply/reply/abort.js", + ); + return { + ...actual, + stopSubagentsForRequester: sessionCleanupMocks.stopSubagentsForRequester, + }; +}); + installGatewayTestHooks(); describe("gateway server sessions", () => { + beforeEach(() => { + sessionCleanupMocks.clearSessionQueues.mockClear(); + sessionCleanupMocks.stopSubagentsForRequester.mockClear(); + }); + test("lists and patches session store via sessions.* RPC", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-")); const storePath = path.join(dir, "sessions.json"); @@ -349,6 +379,15 @@ describe("gateway server sessions", () => { }); expect(deleted.ok).toBe(true); expect(deleted.payload?.deleted).toBe(true); + expect(sessionCleanupMocks.stopSubagentsForRequester).toHaveBeenCalledWith({ + cfg: expect.any(Object), + requesterSessionKey: "agent:main:discord:group:dev", + }); + expect(sessionCleanupMocks.clearSessionQueues).toHaveBeenCalledTimes(1); + const clearedKeys = sessionCleanupMocks.clearSessionQueues.mock.calls[0]?.[0] as string[]; + expect(clearedKeys).toEqual( + expect.arrayContaining(["discord:group:dev", "agent:main:discord:group:dev", "sess-active"]), + ); expect(embeddedRunMock.abortCalls).toEqual(["sess-active"]); expect(embeddedRunMock.waitCalls).toEqual(["sess-active"]);