diff --git a/CHANGELOG.md b/CHANGELOG.md index 89d87e2cf..d7fade07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ - Control UI: add Docs link, remove chat composer divider, and add New session button. - Control UI: link sessions list to chat view. (#471) — thanks @HazAT - Sessions: support session `label` in store/list/UI and allow `sessions_send` lookup by label. (#570) — thanks @azade-c +- Sessions: clarify `sessions_send` delivery semantics, log announce failures, and enforce Discord request timeouts. (#507) — thanks @steipete - Control UI: show/patch per-session reasoning level and render extracted reasoning in chat. - Control UI: queue outgoing chat messages, add Enter-to-send, and show queued items. (#527) — thanks @YuriNachos - Control UI: refactor chat layout with tool sidebar, grouped messages, and nav improvements. (#475) — thanks @rahthakor diff --git a/docs/concepts/session-tool.md b/docs/concepts/session-tool.md index 9a611c495..44c1f3496 100644 --- a/docs/concepts/session-tool.md +++ b/docs/concepts/session-tool.md @@ -76,6 +76,7 @@ Behavior: - `timeoutSeconds > 0`: wait up to N seconds for completion, then return `{ runId, status: "ok", reply }`. - If wait times out: `{ runId, status: "timeout", error }`. Run continues; call `sessions_history` later. - If the run fails: `{ runId, status: "error", error }`. +- Announce delivery runs after the primary run completes and is best-effort; `status: "ok"` does not guarantee the announce was delivered. - Waits via gateway `agent.wait` (server-side) so reconnects don't drop the wait. - Agent-to-agent message context is injected for the primary run. - After the primary run completes, Clawdbot runs a **reply-back loop**: diff --git a/docs/tools/index.md b/docs/tools/index.md index 09ef96ea5..7deaaf51e 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -206,6 +206,7 @@ Notes: - `main` is the canonical direct-chat key; global/unknown are hidden. - `messageLimit > 0` fetches last N messages per session (tool messages filtered). - `sessions_send` waits for final completion when `timeoutSeconds > 0`. +- Delivery/announce happens after completion and is best-effort; `status: "ok"` confirms the agent run finished, not that the announce was delivered. - `sessions_spawn` starts a sub-agent run and posts an announce reply back to the requester chat. - `sessions_spawn` is non-blocking and returns `status: "accepted"` immediately. - `sessions_send` runs a reply‑back ping‑pong (reply `REPLY_SKIP` to stop; max turns via `session.agentToAgent.maxPingPongTurns`, 0–5). diff --git a/package.json b/package.json index 72ef08b7f..9a7af4356 100644 --- a/package.json +++ b/package.json @@ -183,6 +183,7 @@ "@sinclair/typebox": "0.34.47" }, "patchedDependencies": { + "@buape/carbon": "patches/@buape__carbon.patch", "@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch", "@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch", "@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch" @@ -221,6 +222,7 @@ ] }, "patchedDependencies": { + "@buape/carbon": "patches/@buape__carbon.patch", "@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch", "@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch", "@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch", diff --git a/patches/@buape__carbon.patch b/patches/@buape__carbon.patch new file mode 100644 index 000000000..0e965e609 --- /dev/null +++ b/patches/@buape__carbon.patch @@ -0,0 +1,53 @@ +--- a/dist/src/classes/RequestClient.js ++++ b/dist/src/classes/RequestClient.js +@@ -118,6 +118,9 @@ + } + } + this.abortController = new AbortController(); ++ const timeoutMs = typeof this.options.timeout === "number" && this.options.timeout > 0 ++ ? this.options.timeout ++ : undefined; + let body; + if (data?.body && + typeof data.body === "object" && +@@ -178,12 +181,26 @@ + body = JSON.stringify(data.body); + } + } +- const response = await fetch(url, { +- method, +- headers, +- body, +- signal: this.abortController.signal +- }); ++ let timeoutId; ++ if (timeoutMs !== undefined) { ++ timeoutId = setTimeout(() => { ++ this.abortController?.abort(); ++ }, timeoutMs); ++ } ++ let response; ++ try { ++ response = await fetch(url, { ++ method, ++ headers, ++ body, ++ signal: this.abortController.signal ++ }); ++ } ++ finally { ++ if (timeoutId) { ++ clearTimeout(timeoutId); ++ } ++ } + let rawBody = ""; + let parsedBody; + try { +@@ -405,4 +422,4 @@ + } + } + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(ms, 0))); +-//# sourceMappingURL=RequestClient.js.map +\ No newline at end of file ++//# sourceMappingURL=RequestClient.js.map + diff --git a/src/agents/clawdbot-tools.sessions.test.ts b/src/agents/clawdbot-tools.sessions.test.ts index ab56274ea..2217e1f5b 100644 --- a/src/agents/clawdbot-tools.sessions.test.ts +++ b/src/agents/clawdbot-tools.sessions.test.ts @@ -243,7 +243,11 @@ describe("sessions tools", () => { message: "ping", timeoutSeconds: 0, }); - expect(fire.details).toMatchObject({ status: "accepted", runId: "run-1" }); + expect(fire.details).toMatchObject({ + status: "accepted", + runId: "run-1", + delivery: { status: "pending", mode: "announce" }, + }); await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); @@ -256,6 +260,7 @@ describe("sessions tools", () => { expect(waited.details).toMatchObject({ status: "ok", reply: "done", + delivery: { status: "pending", mode: "announce" }, }); expect(typeof (waited.details as { runId?: string }).runId).toBe("string"); await new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index 55561e947..d2687d7fd 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -4,6 +4,8 @@ import { Type } from "@sinclair/typebox"; import { loadConfig } from "../../config/config.js"; import { callGateway } from "../../gateway/call.js"; +import { formatErrorMessage } from "../../infra/errors.js"; +import { createSubsystemLogger } from "../../logging.js"; import { isSubagentSessionKey, normalizeAgentId, @@ -35,6 +37,8 @@ import { resolvePingPongTurns, } from "./sessions-send-helpers.js"; +const log = createSubsystemLogger("agents/sessions-send"); + const SessionsSendToolSchema = Type.Object({ sessionKey: Type.Optional(Type.String()), label: Type.Optional( @@ -308,11 +312,13 @@ export function createSessionsSendTool(opts?: { const requesterSessionKey = opts?.agentSessionKey; const requesterProvider = opts?.agentProvider; const maxPingPongTurns = resolvePingPongTurns(cfg); + const delivery = { status: "pending", mode: "announce" as const }; const runAgentToAgentFlow = async ( roundOneReply?: string, runInfo?: { runId: string }, ) => { + const runContextId = runInfo?.runId ?? runId; try { let primaryReply = roundOneReply; let latestReply = roundOneReply; @@ -400,20 +406,32 @@ export function createSessionsSendTool(opts?: { announceReply.trim() && !isAnnounceSkip(announceReply) ) { - await callGateway({ - method: "send", - params: { - to: announceTarget.to, - message: announceReply.trim(), + try { + await callGateway({ + method: "send", + params: { + to: announceTarget.to, + message: announceReply.trim(), + provider: announceTarget.provider, + accountId: announceTarget.accountId, + idempotencyKey: crypto.randomUUID(), + }, + timeoutMs: 10_000, + }); + } catch (err) { + log.warn("sessions_send announce delivery failed", { + runId: runContextId, provider: announceTarget.provider, - accountId: announceTarget.accountId, - idempotencyKey: crypto.randomUUID(), - }, - timeoutMs: 10_000, - }); + to: announceTarget.to, + error: formatErrorMessage(err), + }); + } } - } catch { - // Best-effort follow-ups; ignore failures to avoid breaking the caller response. + } catch (err) { + log.warn("sessions_send announce flow failed", { + runId: runContextId, + error: formatErrorMessage(err), + }); } }; @@ -432,6 +450,7 @@ export function createSessionsSendTool(opts?: { runId, status: "accepted", sessionKey: displayKey, + delivery, }); } catch (err) { const messageText = @@ -535,6 +554,7 @@ export function createSessionsSendTool(opts?: { status: "ok", reply, sessionKey: displayKey, + delivery, }); }, };