diff --git a/AGENTS.md b/AGENTS.md index 1d3ed60e5..ec6233060 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,7 @@ - Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there. - When asked to open a “session” file, open the Pi/Tau session logs under `~/.tau/agent/sessions/clawdis/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. - Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdis variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item. +- Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel. - Voice wake forwarding tips: - Command template should stay `clawdis-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes. - launchd PATH is minimal; ensure the app’s launch agent sets PATH to include `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm` so `pnpm`/`clawdis` binaries resolve when invoked via `clawdis-mac`. diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index 786948c2c..05b67be44 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -154,6 +154,55 @@ describe("resolveHeartbeatRecipients", () => { }); }); +describe("partial reply gating", () => { + it("does not send partial replies for WhatsApp surface", async () => { + const reply = vi.fn().mockResolvedValue(undefined); + const sendComposing = vi.fn().mockResolvedValue(undefined); + const sendMedia = vi.fn().mockResolvedValue(undefined); + + const replyResolver = vi.fn().mockResolvedValue({ text: "final reply" }); + + const mockConfig: WarelayConfig = { + inbound: { + reply: { mode: "command" }, + allowFrom: ["*"], + }, + }; + + setLoadConfigMock(mockConfig); + + await monitorWebProvider( + false, + async ({ onMessage }) => { + await onMessage({ + id: "m1", + from: "+1000", + conversationId: "+1000", + to: "+2000", + body: "hello", + timestamp: Date.now(), + chatType: "direct", + chatId: "direct:+1000", + sendComposing, + reply, + sendMedia, + }); + return { close: vi.fn().mockResolvedValue(undefined) }; + }, + false, + replyResolver, + ); + + resetLoadConfigMock(); + + expect(replyResolver).toHaveBeenCalledTimes(1); + const resolverOptions = replyResolver.mock.calls[0]?.[1] ?? {}; + expect("onPartialReply" in resolverOptions).toBe(false); + expect(reply).toHaveBeenCalledTimes(1); + expect(reply).toHaveBeenCalledWith("final reply"); + }); +}); + describe("runWebHeartbeatOnce", () => { it("skips when heartbeat token returned", async () => { const store = await makeSessionStore(); diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index 292298ad0..4b6d74348 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -12,6 +12,7 @@ import { } from "../config/sessions.js"; import { danger, info, isVerbose, logVerbose, success } from "../globals.js"; import { emitHeartbeatEvent } from "../infra/heartbeat-events.js"; +import { enqueueSystemEvent } from "../infra/system-events.js"; import { logInfo } from "../logger.js"; import { getChildLogger } from "../logging.js"; import { getQueueSize } from "../process/command-queue.js"; @@ -29,7 +30,7 @@ import { resolveReconnectPolicy, sleepWithAbort, } from "./reconnect.js"; -import { getWebAuthAgeMs } from "./session.js"; +import { getWebAuthAgeMs, readWebSelfId } from "./session.js"; const WEB_TEXT_LIMIT = 4000; const DEFAULT_GROUP_HISTORY_LIMIT = 50; @@ -969,6 +970,12 @@ export async function monitorWebProvider( }, }); + // Surface a concise connection event for the next main-session turn/heartbeat. + const { e164: selfE164 } = readWebSelfId(); + enqueueSystemEvent( + `WhatsApp relay connected${selfE164 ? ` as ${selfE164}` : ""}.`, + ); + // Start IPC server so `clawdis send` can use this connection // instead of creating a new one (which would corrupt Signal session) if ("sendMessage" in listener && "sendComposingTo" in listener) { @@ -1339,6 +1346,10 @@ export async function monitorWebProvider( "web reconnect: connection closed", ); + enqueueSystemEvent( + `WhatsApp relay disconnected (status ${status ?? "unknown"})`, + ); + if (loggedOut) { runtime.error( danger(