diff --git a/src/agents/clawdis-tools.sessions.test.ts b/src/agents/clawdis-tools.sessions.test.ts index cb9ac6ef7..aad46270b 100644 --- a/src/agents/clawdis-tools.sessions.test.ts +++ b/src/agents/clawdis-tools.sessions.test.ts @@ -1,54 +1,10 @@ import { describe, expect, it, vi } from "vitest"; const callGatewayMock = vi.fn(); -const nextRunId = "run-1"; -const nextRunState: "done" | "error" = "done"; - vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -vi.mock("../gateway/client.js", () => ({ - GatewayClient: class { - private opts: { - onEvent?: (evt: { - event?: string; - payload?: { - runId?: string; - stream?: string; - data?: Record; - }; - }) => void; - }; - constructor(opts: { - onEvent?: (evt: { - event?: string; - payload?: { - runId?: string; - stream?: string; - data?: Record; - }; - }) => void; - }) { - this.opts = opts; - } - start() { - setTimeout(() => { - this.opts.onEvent?.({ - event: "agent", - payload: { - runId: nextRunId, - stream: "job", - data: - nextRunState === "error" ? { state: "error" } : { state: "done" }, - }, - }); - }, 1); - } - stop() {} - }, -})); - vi.mock("../config/config.js", () => ({ loadConfig: () => ({ session: { mainKey: "main", scope: "per-sender" }, @@ -60,6 +16,7 @@ import { createClawdisTools } from "./clawdis-tools.js"; describe("sessions tools", () => { it("sessions_list filters kinds and includes messages", async () => { + callGatewayMock.mockReset(); callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string }; if (request.method === "sessions.list") { @@ -131,6 +88,7 @@ describe("sessions tools", () => { }); it("sessions_history filters tool messages by default", async () => { + callGatewayMock.mockReset(); callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string }; if (request.method === "chat.history") { @@ -164,8 +122,11 @@ describe("sessions tools", () => { }); it("sessions_send supports fire-and-forget and wait", async () => { + callGatewayMock.mockReset(); + const calls: Array<{ method?: string; expectFinal?: boolean }> = []; callGatewayMock.mockImplementation(async (opts: unknown) => { - const request = opts as { method?: string }; + const request = opts as { method?: string; expectFinal?: boolean }; + calls.push(request); if (request.method === "agent") { return { runId: "run-1", status: "accepted" }; } @@ -203,5 +164,9 @@ describe("sessions tools", () => { runId: "run-1", reply: "done", }); + + const agentCalls = calls.filter((call) => call.method === "agent"); + expect(agentCalls[0]?.expectFinal).toBeUndefined(); + expect(agentCalls[1]?.expectFinal).toBe(true); }); }); diff --git a/src/agents/clawdis-tools.ts b/src/agents/clawdis-tools.ts index 7ca44f6eb..e0c41ebd5 100644 --- a/src/agents/clawdis-tools.ts +++ b/src/agents/clawdis-tools.ts @@ -45,7 +45,6 @@ import { type ClawdisConfig, type DiscordActionConfig, loadConfig, - resolveGatewayPort, } from "../config/config.js"; import { addRoleDiscord, @@ -78,7 +77,6 @@ import { unpinMessageDiscord, } from "../discord/send.js"; import { callGateway } from "../gateway/call.js"; -import { GatewayClient } from "../gateway/client.js"; import { detectMime, imageMimeFromFormat } from "../media/mime.js"; import { sanitizeToolResultImages } from "./tool-images.js"; @@ -339,88 +337,6 @@ function extractAssistantText(message: unknown): string | undefined { return joined ? joined : undefined; } -function resolveGatewayConnection(opts: GatewayCallOptions) { - const cfg = loadConfig(); - const isRemoteMode = cfg.gateway?.mode === "remote"; - const remote = isRemoteMode ? cfg.gateway?.remote : undefined; - const localPort = resolveGatewayPort(cfg); - - const url = - normalizeKey(opts.gatewayUrl) ?? - (typeof remote?.url === "string" && remote.url.trim() - ? remote.url.trim() - : undefined) ?? - `ws://127.0.0.1:${localPort}`; - const token = - normalizeKey(opts.gatewayToken) ?? - (isRemoteMode - ? normalizeKey(remote?.token) - : (normalizeKey(process.env.CLAWDIS_GATEWAY_TOKEN) ?? - normalizeKey(cfg.gateway?.auth?.token))); - const password = - normalizeKey(process.env.CLAWDIS_GATEWAY_PASSWORD) ?? - normalizeKey(remote?.password); - return { url, token, password }; -} - -async function waitForAgentCompletion(params: { - connection: ReturnType; - runId: string; - timeoutMs: number; -}): Promise<{ status: "done" | "error" | "timeout"; error?: string }> { - const { connection, runId, timeoutMs } = params; - return await new Promise((resolve) => { - let settled = false; - const done = (status: "done" | "error" | "timeout", error?: string) => { - if (settled) return; - settled = true; - clearTimeout(timer); - client.stop(); - resolve({ status, error }); - }; - - const client = new GatewayClient({ - url: connection.url, - token: connection.token, - password: connection.password, - clientName: "agent", - clientVersion: "dev", - platform: process.platform, - mode: "agent", - instanceId: crypto.randomUUID(), - onEvent: (evt) => { - if (evt.event !== "agent") return; - const payload = evt.payload as { - runId?: unknown; - stream?: unknown; - data?: Record; - }; - if (payload?.runId !== runId) return; - if (payload.stream !== "job") return; - const state = payload.data?.state; - if (state === "done") { - done("done"); - return; - } - if (state === "error") { - done( - "error", - typeof payload.data?.error === "string" - ? payload.data.error - : undefined, - ); - } - }, - onClose: (_code, _reason) => { - done("timeout"); - }, - }); - - const timer = setTimeout(() => done("timeout"), timeoutMs); - client.start(); - }); -} - async function imageResult(params: { label: string; path: string; @@ -2775,19 +2691,51 @@ function createSessionsSendTool(): AnyAgentTool { : 30; const idempotencyKey = crypto.randomUUID(); let runId = idempotencyKey; + const displayKey = resolveDisplaySessionKey({ + key: sessionKey, + alias, + mainKey, + }); + const sendParams = { + message, + sessionKey: resolvedKey, + idempotencyKey, + deliver: false, + }; + + if (timeoutSeconds === 0) { + try { + const response = (await callGateway({ + method: "agent", + params: sendParams, + timeoutMs: 10_000, + })) as { runId?: string }; + if (typeof response?.runId === "string" && response.runId) { + runId = response.runId; + } + return jsonResult({ + runId, + status: "accepted", + sessionKey: displayKey, + }); + } catch (err) { + const message = + err instanceof Error ? err.message : String(err ?? "error"); + return jsonResult({ + runId, + status: "error", + error: message, + sessionKey: displayKey, + }); + } + } + try { const response = (await callGateway({ method: "agent", - params: { - message, - sessionKey: resolvedKey, - idempotencyKey, - deliver: false, - }, - timeoutMs: - timeoutSeconds > 0 - ? Math.min(timeoutSeconds * 1000, 10_000) - : 10_000, + params: sendParams, + expectFinal: true, + timeoutMs: timeoutSeconds * 1000, })) as { runId?: string; status?: string }; if (typeof response?.runId === "string" && response.runId) { runId = response.runId; @@ -2795,46 +2743,31 @@ function createSessionsSendTool(): AnyAgentTool { } catch (err) { const message = err instanceof Error ? err.message : String(err ?? "error"); + if (message.includes("gateway timeout")) { + try { + const cached = (await callGateway({ + method: "agent", + params: sendParams, + timeoutMs: 5_000, + })) as { runId?: string }; + if (typeof cached?.runId === "string" && cached.runId) { + runId = cached.runId; + } + } catch { + /* ignore */ + } + return jsonResult({ + runId, + status: "timeout", + error: message, + sessionKey: displayKey, + }); + } return jsonResult({ runId, status: "error", error: message, - sessionKey: resolveDisplaySessionKey({ - key: sessionKey, - alias, - mainKey, - }), - }); - } - - if (timeoutSeconds === 0) { - return jsonResult({ - runId, - status: "accepted", - sessionKey: resolveDisplaySessionKey({ - key: sessionKey, - alias, - mainKey, - }), - }); - } - - const connection = resolveGatewayConnection({}); - const wait = await waitForAgentCompletion({ - connection, - runId, - timeoutMs: timeoutSeconds * 1000, - }); - - if (wait.status === "timeout") { - return jsonResult({ - runId, - status: "timeout", - sessionKey: resolveDisplaySessionKey({ - key: sessionKey, - alias, - mainKey, - }), + sessionKey: displayKey, }); } @@ -2851,14 +2784,9 @@ function createSessionsSendTool(): AnyAgentTool { return jsonResult({ runId, - status: wait.status === "error" ? "error" : "ok", - error: wait.error, + status: "ok", reply, - sessionKey: resolveDisplaySessionKey({ - key: sessionKey, - alias, - mainKey, - }), + sessionKey: displayKey, }); }, };