fix: block partial replies on external chat surfaces
This commit is contained in:
@@ -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.
|
- 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`.
|
- 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.
|
- 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:
|
- 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.
|
- 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`.
|
- 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`.
|
||||||
|
|||||||
@@ -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", () => {
|
describe("runWebHeartbeatOnce", () => {
|
||||||
it("skips when heartbeat token returned", async () => {
|
it("skips when heartbeat token returned", async () => {
|
||||||
const store = await makeSessionStore();
|
const store = await makeSessionStore();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { danger, info, isVerbose, logVerbose, success } from "../globals.js";
|
import { danger, info, isVerbose, logVerbose, success } from "../globals.js";
|
||||||
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
|
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
|
||||||
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||||
import { logInfo } from "../logger.js";
|
import { logInfo } from "../logger.js";
|
||||||
import { getChildLogger } from "../logging.js";
|
import { getChildLogger } from "../logging.js";
|
||||||
import { getQueueSize } from "../process/command-queue.js";
|
import { getQueueSize } from "../process/command-queue.js";
|
||||||
@@ -29,7 +30,7 @@ import {
|
|||||||
resolveReconnectPolicy,
|
resolveReconnectPolicy,
|
||||||
sleepWithAbort,
|
sleepWithAbort,
|
||||||
} from "./reconnect.js";
|
} from "./reconnect.js";
|
||||||
import { getWebAuthAgeMs } from "./session.js";
|
import { getWebAuthAgeMs, readWebSelfId } from "./session.js";
|
||||||
|
|
||||||
const WEB_TEXT_LIMIT = 4000;
|
const WEB_TEXT_LIMIT = 4000;
|
||||||
const DEFAULT_GROUP_HISTORY_LIMIT = 50;
|
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
|
// Start IPC server so `clawdis send` can use this connection
|
||||||
// instead of creating a new one (which would corrupt Signal session)
|
// instead of creating a new one (which would corrupt Signal session)
|
||||||
if ("sendMessage" in listener && "sendComposingTo" in listener) {
|
if ("sendMessage" in listener && "sendComposingTo" in listener) {
|
||||||
@@ -1339,6 +1346,10 @@ export async function monitorWebProvider(
|
|||||||
"web reconnect: connection closed",
|
"web reconnect: connection closed",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
enqueueSystemEvent(
|
||||||
|
`WhatsApp relay disconnected (status ${status ?? "unknown"})`,
|
||||||
|
);
|
||||||
|
|
||||||
if (loggedOut) {
|
if (loggedOut) {
|
||||||
runtime.error(
|
runtime.error(
|
||||||
danger(
|
danger(
|
||||||
|
|||||||
Reference in New Issue
Block a user