Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13a332ac8 | ||
|
|
6aaa302be5 |
No files matched your search
+1
-4
@@ -27,7 +27,6 @@
|
||||
- Tools: add tmux-style `process send-keys` and bracketed paste helpers for PTY sessions.
|
||||
- Tools: add `process submit` helper to send CR for PTY sessions.
|
||||
- Tools: respond to PTY cursor position queries to unblock interactive TUIs.
|
||||
- TUI: refresh session token counts after runs complete or fail. (#1079) — thanks @d-ploutarchos.
|
||||
- Status: trim `/status` to current-provider usage only and drop the OAuth/token block.
|
||||
- Directory: unify `clawdbot directory` across channels and plugin channels.
|
||||
- UI: allow deleting sessions from the Control UI.
|
||||
@@ -44,13 +43,12 @@
|
||||
- Plugins: add zip installs and `--link` to avoid copying local paths.
|
||||
|
||||
### Fixes
|
||||
- Telegram: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z.
|
||||
- Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt.
|
||||
- Sessions: include deliveryContext in sessions.list and reuse normalized delivery routing for announce/restart fallbacks. (#1058)
|
||||
- Sessions: propagate deliveryContext into last-route updates to keep account/channel routing stable. (#1058)
|
||||
- Memory: prevent unhandled rejections when watch/interval sync fails. (#1076) — thanks @roshanasingh4.
|
||||
- Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing.
|
||||
- Gateway: avoid reusing last-to/accountId when the requested channel differs; sync deliveryContext with last route fields.
|
||||
- Skills: narrow skills watcher ignores to `.git` so hidden workspaces still refresh. (#1074) — thanks @roshanasingh4.
|
||||
- Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea.
|
||||
- Messages: `/stop` now hard-aborts queued followups and sub-agent runs; suppress zero-count stop notes.
|
||||
- Messages: include sender labels for live group messages across channels, matching queued/history formatting. (#1059)
|
||||
@@ -61,7 +59,6 @@
|
||||
- CLI: speed up `clawdbot sandbox-explain` by avoiding heavy plugin imports when normalizing channel ids.
|
||||
- Browser: remote profile tab operations prefer persistent Playwright and avoid silent HTTP fallbacks. (#1057) — thanks @mukhtharcm.
|
||||
- Browser: remote profile tab ops follow-up: shared Playwright loader, Playwright-based focus, and more coverage (incl. opt-in live Browserless test). (follow-up to #1057) — thanks @mukhtharcm.
|
||||
- Browser: refresh extension relay tab metadata after navigation so `/json/list` stays current. (#1073) — thanks @roshanasingh4.
|
||||
- WhatsApp: scope self-chat response prefix; inject pending-only group history and clear after any processed message.
|
||||
- Agents: drop unsigned Gemini tool calls and avoid JSON Schema `format` keyword collisions.
|
||||
- Agents: avoid duplicate sends by replying with `NO_REPLY` after `message` tool sends.
|
||||
|
||||
@@ -16,14 +16,14 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
|
||||
if (roomId.toLowerCase().startsWith("room:")) {
|
||||
roomId = roomId.slice("room:".length).trim();
|
||||
}
|
||||
const groupChannel = params.groupChannel?.trim() ?? "";
|
||||
const aliases = groupChannel ? [groupChannel] : [];
|
||||
const groupRoom = params.groupRoom?.trim() ?? "";
|
||||
const aliases = groupRoom ? [groupRoom] : [];
|
||||
const cfg = params.cfg as CoreConfig;
|
||||
const resolved = resolveMatrixRoomConfig({
|
||||
rooms: cfg.channels?.matrix?.rooms,
|
||||
roomId,
|
||||
aliases,
|
||||
name: groupChannel || undefined,
|
||||
name: groupRoom || undefined,
|
||||
}).config;
|
||||
if (resolved) {
|
||||
if (resolved.autoReply === true) return false;
|
||||
|
||||
@@ -379,7 +379,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
SenderId: senderId,
|
||||
SenderUsername: senderId.split(":")[0]?.replace(/^@/, ""),
|
||||
GroupSubject: isRoom ? (roomName ?? roomId) : undefined,
|
||||
GroupChannel: isRoom ? (room.getCanonicalAlias?.() ?? roomId) : undefined,
|
||||
GroupRoom: isRoom ? (room.getCanonicalAlias?.() ?? roomId) : undefined,
|
||||
GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined,
|
||||
Provider: "matrix" as const,
|
||||
Surface: "matrix" as const,
|
||||
|
||||
@@ -10,7 +10,7 @@ afterEach(() => {
|
||||
test("exec supports pty output", async () => {
|
||||
const tool = createExecTool({ allowBackground: false });
|
||||
const result = await tool.execute("toolcall", {
|
||||
command: 'node -e "process.stdout.write(String.fromCharCode(111,107))"',
|
||||
command: "node -e 'process.stdout.write(\"ok\")'",
|
||||
pty: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ test("process send-keys encodes Enter for pty sessions", async () => {
|
||||
const processTool = createProcessTool();
|
||||
const result = await execTool.execute("toolcall", {
|
||||
command:
|
||||
'node -e "const dataEvent=String.fromCharCode(100,97,116,97);process.stdin.on(dataEvent,d=>{process.stdout.write(d);if(d.includes(10)||d.includes(13))process.exit(0);});"',
|
||||
"node -e \"process.stdin.on('data', d => { process.stdout.write(d); if (d.includes(10) || d.includes(13)) process.exit(0); });\"",
|
||||
pty: true,
|
||||
background: true,
|
||||
});
|
||||
@@ -45,12 +45,12 @@ test("process send-keys encodes Enter for pty sessions", async () => {
|
||||
throw new Error("PTY session did not exit after send-keys");
|
||||
});
|
||||
|
||||
test("process submit sends Enter for pty sessions", async () => {
|
||||
test("process submit sends CR for pty sessions", async () => {
|
||||
const execTool = createExecTool();
|
||||
const processTool = createProcessTool();
|
||||
const result = await execTool.execute("toolcall", {
|
||||
command:
|
||||
'node -e "const dataEvent=String.fromCharCode(100,97,116,97);const submitted=String.fromCharCode(115,117,98,109,105,116,116,101,100);process.stdin.on(dataEvent,d=>{if(d.includes(10)||d.includes(13)){process.stdout.write(submitted);process.exit(0);}});"',
|
||||
"node -e \"process.stdin.on('data', d => { if (d.includes(10) || d.includes(13)) { process.stdout.write('submitted'); process.exit(0); } });\"",
|
||||
pty: true,
|
||||
background: true,
|
||||
});
|
||||
@@ -64,8 +64,7 @@ test("process submit sends Enter for pty sessions", async () => {
|
||||
sessionId,
|
||||
});
|
||||
|
||||
const deadline = Date.now() + (process.platform === "win32" ? 4000 : 2000);
|
||||
while (Date.now() < deadline) {
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
await wait(50);
|
||||
const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
|
||||
const details = poll.details as { status?: string; aggregated?: string };
|
||||
|
||||
@@ -33,7 +33,9 @@ const processSchema = Type.Object({
|
||||
keys: Type.Optional(
|
||||
Type.Array(Type.String(), { description: "Key tokens to send for send-keys" }),
|
||||
),
|
||||
hex: Type.Optional(Type.Array(Type.String(), { description: "Hex bytes to send for send-keys" })),
|
||||
hex: Type.Optional(
|
||||
Type.Array(Type.String(), { description: "Hex bytes to send for send-keys" }),
|
||||
),
|
||||
literal: Type.Optional(Type.String({ description: "Literal string for send-keys" })),
|
||||
text: Type.Optional(Type.String({ description: "Text to paste for paste" })),
|
||||
bracketed: Type.Optional(Type.Boolean({ description: "Wrap paste in bracketed mode" })),
|
||||
@@ -119,7 +121,10 @@ export function createProcessTool(
|
||||
.sort((a, b) => b.startedAt - a.startedAt)
|
||||
.map((s) => {
|
||||
const label = s.name ? truncateMiddle(s.name, 80) : truncateMiddle(s.command, 120);
|
||||
return `${s.sessionId} ${pad(s.status, 9)} ${formatDuration(s.runtimeMs)} :: ${label}`;
|
||||
return `${s.sessionId} ${pad(
|
||||
s.status,
|
||||
9,
|
||||
)} ${formatDuration(s.runtimeMs)} :: ${label}`;
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const ESC = String.fromCharCode(0x1b);
|
||||
const ESC = "\u001b";
|
||||
const DSR_PATTERN = new RegExp(`${ESC}\\[\\??6n`, "g");
|
||||
|
||||
export function stripDsrRequests(input: string): { cleaned: string; requests: number } {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
BRACKETED_PASTE_END,
|
||||
BRACKETED_PASTE_START,
|
||||
encodeKeySequence,
|
||||
encodePaste,
|
||||
} from "./pty-keys.js";
|
||||
import { BRACKETED_PASTE_END, BRACKETED_PASTE_START, encodeKeySequence, encodePaste } from "./pty-keys.js";
|
||||
|
||||
test("encodeKeySequence maps common keys and modifiers", () => {
|
||||
const enter = encodeKeySequence({ keys: ["Enter"] });
|
||||
|
||||
@@ -19,8 +19,8 @@ describe("ensureSkillsWatcher", () => {
|
||||
expect(watchMock).toHaveBeenCalledTimes(1);
|
||||
const opts = watchMock.mock.calls[0]?.[1] as { ignored?: unknown };
|
||||
|
||||
expect(opts.ignored).toBe(mod.DEFAULT_SKILLS_WATCH_IGNORED);
|
||||
const ignored = mod.DEFAULT_SKILLS_WATCH_IGNORED;
|
||||
expect(Array.isArray(opts.ignored)).toBe(true);
|
||||
const ignored = opts.ignored as RegExp[];
|
||||
expect(ignored.some((re) => re.test("/tmp/workspace/skills/node_modules/pkg/index.js"))).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -26,12 +26,6 @@ const workspaceVersions = new Map<string, number>();
|
||||
const watchers = new Map<string, SkillsWatchState>();
|
||||
let globalVersion = 0;
|
||||
|
||||
export const DEFAULT_SKILLS_WATCH_IGNORED: RegExp[] = [
|
||||
/(^|[\\/])\.git([\\/]|$)/,
|
||||
/(^|[\\/])node_modules([\\/]|$)/,
|
||||
/(^|[\\/])dist([\\/]|$)/,
|
||||
];
|
||||
|
||||
function bumpVersion(current: number): number {
|
||||
const now = Date.now();
|
||||
return now <= current ? current + 1 : now;
|
||||
@@ -133,7 +127,11 @@ export function ensureSkillsWatcher(params: { workspaceDir: string; config?: Cla
|
||||
},
|
||||
// Avoid FD exhaustion on macOS when a workspace contains huge trees.
|
||||
// This watcher only needs to react to skill changes.
|
||||
ignored: DEFAULT_SKILLS_WATCH_IGNORED,
|
||||
ignored: [
|
||||
/(^|[\\/])\.git([\\/]|$)/,
|
||||
/(^|[\\/])node_modules([\\/]|$)/,
|
||||
/(^|[\\/])dist([\\/]|$)/,
|
||||
],
|
||||
});
|
||||
|
||||
const state: SkillsWatchState = { watcher, pathsKey, debounceMs };
|
||||
|
||||
@@ -150,12 +150,12 @@ export function createSessionsListTool(opts?: {
|
||||
: undefined;
|
||||
const deliveryChannel =
|
||||
typeof deliveryContext?.channel === "string" ? deliveryContext.channel : undefined;
|
||||
const deliveryTo = typeof deliveryContext?.to === "string" ? deliveryContext.to : undefined;
|
||||
const deliveryTo =
|
||||
typeof deliveryContext?.to === "string" ? deliveryContext.to : undefined;
|
||||
const deliveryAccountId =
|
||||
typeof deliveryContext?.accountId === "string" ? deliveryContext.accountId : undefined;
|
||||
const lastChannel =
|
||||
deliveryChannel ??
|
||||
(typeof entry.lastChannel === "string" ? entry.lastChannel : undefined);
|
||||
deliveryChannel ?? (typeof entry.lastChannel === "string" ? entry.lastChannel : undefined);
|
||||
const lastAccountId =
|
||||
deliveryAccountId ??
|
||||
(typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined);
|
||||
|
||||
@@ -382,42 +382,6 @@ describe("handleTelegramAction", () => {
|
||||
).rejects.toThrow(/inline buttons are limited to DMs/i);
|
||||
});
|
||||
|
||||
it("allows inline buttons in DMs with tg: prefixed targets", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { botToken: "tok", capabilities: { inlineButtons: "dm" } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "tg:5232990709",
|
||||
content: "Choose",
|
||||
buttons: [[{ text: "Ok", callback_data: "cmd:ok" }]],
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows inline buttons in groups with topic targets", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { botToken: "tok", capabilities: { inlineButtons: "group" } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to: "telegram:group:-1001234567890:topic:456",
|
||||
content: "Choose",
|
||||
buttons: [[{ text: "Ok", callback_data: "cmd:ok" }]],
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends messages with inline keyboard buttons when enabled", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -297,7 +297,9 @@ export async function runPreparedReply(
|
||||
abortKey: command.abortKey,
|
||||
messageId: sessionCtx.MessageSid,
|
||||
});
|
||||
const isGroupSession = sessionEntry?.chatType === "group" || sessionEntry?.chatType === "channel";
|
||||
const isGroupSession =
|
||||
sessionEntry?.chatType === "group" ||
|
||||
sessionEntry?.chatType === "channel";
|
||||
const isMainSession = !isGroupSession && sessionKey === normalizeMainKey(sessionCfg?.mainKey);
|
||||
prefixedBodyBase = await prependSystemEvents({
|
||||
cfg,
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("resolveGroupRequireMention", () => {
|
||||
const ctx: TemplateContext = {
|
||||
Provider: "discord",
|
||||
From: "group:123",
|
||||
GroupChannel: "#general",
|
||||
GroupRoom: "#general",
|
||||
GroupSpace: "145",
|
||||
};
|
||||
const groupResolution: GroupKeyResolution = {
|
||||
|
||||
@@ -16,12 +16,12 @@ export function resolveGroupRequireMention(params: {
|
||||
const channel = normalizeChannelId(rawChannel);
|
||||
if (!channel) return true;
|
||||
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
||||
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
|
||||
const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim();
|
||||
const groupSpace = ctx.GroupSpace?.trim();
|
||||
const requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({
|
||||
cfg,
|
||||
groupId,
|
||||
groupChannel,
|
||||
groupRoom,
|
||||
groupSpace,
|
||||
accountId: ctx.AccountId,
|
||||
});
|
||||
@@ -62,13 +62,13 @@ export function buildGroupIntro(params: {
|
||||
? "Activation: always-on (you receive every group message)."
|
||||
: "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included).";
|
||||
const groupId = params.sessionCtx.From?.replace(/^group:/, "");
|
||||
const groupChannel = params.sessionCtx.GroupChannel?.trim() ?? subject;
|
||||
const groupRoom = params.sessionCtx.GroupRoom?.trim() ?? subject;
|
||||
const groupSpace = params.sessionCtx.GroupSpace?.trim();
|
||||
const providerIdsLine = providerId
|
||||
? getChannelDock(providerId)?.groups?.resolveGroupIntroHint?.({
|
||||
cfg: params.cfg,
|
||||
groupId,
|
||||
groupChannel,
|
||||
groupRoom,
|
||||
groupSpace,
|
||||
accountId: params.sessionCtx.AccountId,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { applyQueueDropPolicy, shouldSkipQueueItem } from "../../../utils/queue-helpers.js";
|
||||
import {
|
||||
applyQueueDropPolicy,
|
||||
shouldSkipQueueItem,
|
||||
} from "../../../utils/queue-helpers.js";
|
||||
import { FOLLOWUP_QUEUES, getFollowupQueue } from "./state.js";
|
||||
import type { FollowupRun, QueueDedupeMode, QueueSettings } from "./types.js";
|
||||
|
||||
|
||||
@@ -193,9 +193,11 @@ export async function initSessionState(params: {
|
||||
|
||||
const baseEntry = !isNewSession && freshEntry ? entry : undefined;
|
||||
// Track the originating channel/to for announce routing (subagent announce-back).
|
||||
const lastChannelRaw = (ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel;
|
||||
const lastChannelRaw =
|
||||
(ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel;
|
||||
const lastToRaw = (ctx.OriginatingTo as string | undefined) || ctx.To || baseEntry?.lastTo;
|
||||
const lastAccountIdRaw = (ctx.AccountId as string | undefined) || baseEntry?.lastAccountId;
|
||||
const lastAccountIdRaw =
|
||||
(ctx.AccountId as string | undefined) || baseEntry?.lastAccountId;
|
||||
const deliveryFields = normalizeSessionDeliveryFields({
|
||||
deliveryContext: {
|
||||
channel: lastChannelRaw,
|
||||
@@ -240,19 +242,14 @@ export async function initSessionState(params: {
|
||||
const channel = groupResolution.channel;
|
||||
const subject = ctx.GroupSubject?.trim();
|
||||
const space = ctx.GroupSpace?.trim();
|
||||
const explicitChannel = ctx.GroupChannel?.trim();
|
||||
const explicitRoom = ctx.GroupRoom?.trim();
|
||||
const normalizedChannel = normalizeChannelId(channel);
|
||||
const isChannelProvider = Boolean(
|
||||
const isRoomProvider = Boolean(
|
||||
normalizedChannel &&
|
||||
getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"),
|
||||
);
|
||||
const nextRoom =
|
||||
explicitChannel ??
|
||||
((groupResolution.chatType === "channel" || isChannelProvider) &&
|
||||
subject &&
|
||||
subject.startsWith("#")
|
||||
? subject
|
||||
: undefined);
|
||||
explicitRoom ?? (isRoomProvider && subject && subject.startsWith("#") ? subject : undefined);
|
||||
const nextSubject = nextRoom ? undefined : subject;
|
||||
sessionEntry.chatType = groupResolution.chatType ?? "group";
|
||||
sessionEntry.channel = channel;
|
||||
|
||||
@@ -63,8 +63,7 @@ export type MsgContext = {
|
||||
/** Human label for envelope headers (conversation label, not sender). */
|
||||
ConversationLabel?: string;
|
||||
GroupSubject?: string;
|
||||
/** Human label for channel-like group conversations (e.g. #general, #support). */
|
||||
GroupChannel?: string;
|
||||
GroupRoom?: string;
|
||||
GroupSpace?: string;
|
||||
GroupMembers?: string;
|
||||
GroupSystemPrompt?: string;
|
||||
|
||||
@@ -90,23 +90,6 @@ function createMessageQueue(ws: WebSocket) {
|
||||
return { next };
|
||||
}
|
||||
|
||||
async function waitForListMatch<T>(
|
||||
fetchList: () => Promise<T>,
|
||||
predicate: (value: T) => boolean,
|
||||
timeoutMs = 2000,
|
||||
intervalMs = 50,
|
||||
): Promise<T> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (true) {
|
||||
const value = await fetchList();
|
||||
if (predicate(value)) return value;
|
||||
if (Date.now() >= deadline) {
|
||||
throw new Error("timeout waiting for list update");
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
describe("chrome extension relay server", () => {
|
||||
let cdpUrl = "";
|
||||
|
||||
@@ -169,48 +152,9 @@ describe("chrome extension relay server", () => {
|
||||
const list = (await fetch(`${cdpUrl}/json/list`).then((r) => r.json())) as Array<{
|
||||
id?: string;
|
||||
url?: string;
|
||||
title?: string;
|
||||
}>;
|
||||
expect(list.some((t) => t.id === "t1" && t.url === "https://example.com")).toBe(true);
|
||||
|
||||
// Simulate navigation updating tab metadata.
|
||||
ext.send(
|
||||
JSON.stringify({
|
||||
method: "forwardCDPEvent",
|
||||
params: {
|
||||
method: "Target.targetInfoChanged",
|
||||
params: {
|
||||
targetInfo: {
|
||||
targetId: "t1",
|
||||
type: "page",
|
||||
title: "DER STANDARD",
|
||||
url: "https://www.derstandard.at/",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const list2 = await waitForListMatch(
|
||||
async () =>
|
||||
(await fetch(`${cdpUrl}/json/list`).then((r) => r.json())) as Array<{
|
||||
id?: string;
|
||||
url?: string;
|
||||
title?: string;
|
||||
}>,
|
||||
(list) =>
|
||||
list.some(
|
||||
(t) =>
|
||||
t.id === "t1" && t.url === "https://www.derstandard.at/" && t.title === "DER STANDARD",
|
||||
),
|
||||
);
|
||||
expect(
|
||||
list2.some(
|
||||
(t) =>
|
||||
t.id === "t1" && t.url === "https://www.derstandard.at/" && t.title === "DER STANDARD",
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`);
|
||||
await waitForOpen(cdp);
|
||||
const q = createMessageQueue(cdp);
|
||||
|
||||
@@ -497,23 +497,6 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep cached tab metadata fresh for /json/list.
|
||||
// After navigation, Chrome updates URL/title via Target.targetInfoChanged.
|
||||
if (method === "Target.targetInfoChanged") {
|
||||
const changed = (params ?? {}) as { targetInfo?: { targetId?: string; type?: string } };
|
||||
const targetInfo = changed?.targetInfo;
|
||||
const targetId = targetInfo?.targetId;
|
||||
if (targetId && (targetInfo?.type ?? "page") === "page") {
|
||||
for (const [sid, target] of connectedTargets) {
|
||||
if (target.targetId !== targetId) continue;
|
||||
connectedTargets.set(sid, {
|
||||
...target,
|
||||
targetInfo: { ...target.targetInfo, ...(targetInfo as object) },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcastToCdpClients({ method, params, sessionId });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -70,3 +70,4 @@ describe("resolveCommandAuthorizedFromAuthorizers", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,3 +21,4 @@ export function resolveCommandAuthorizedFromAuthorizers(params: {
|
||||
}
|
||||
return authorizers.some((entry) => entry.configured && entry.allowed);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function resolveConversationLabel(ctx: MsgContext): string | undefined {
|
||||
}
|
||||
|
||||
const base =
|
||||
ctx.GroupChannel?.trim() ||
|
||||
ctx.GroupRoom?.trim() ||
|
||||
ctx.GroupSubject?.trim() ||
|
||||
ctx.GroupSpace?.trim() ||
|
||||
ctx.From?.trim() ||
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveDiscordGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { looksLikeDiscordTargetId, normalizeDiscordMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeDiscordTargetId,
|
||||
normalizeDiscordMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { discordOnboardingAdapter } from "./onboarding/discord.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||
type GroupMentionParams = {
|
||||
cfg: ClawdbotConfig;
|
||||
groupId?: string | null;
|
||||
groupChannel?: string | null;
|
||||
groupRoom?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
};
|
||||
@@ -133,14 +133,13 @@ export function resolveDiscordGroupRequireMention(params: GroupMentionParams): b
|
||||
);
|
||||
const channelEntries = guildEntry?.channels;
|
||||
if (channelEntries && Object.keys(channelEntries).length > 0) {
|
||||
const groupChannel = params.groupChannel;
|
||||
const channelSlug = normalizeDiscordSlug(groupChannel);
|
||||
const channelSlug = normalizeDiscordSlug(params.groupRoom);
|
||||
const entry =
|
||||
(params.groupId ? channelEntries[params.groupId] : undefined) ??
|
||||
(channelSlug
|
||||
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
|
||||
: undefined) ??
|
||||
(groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined);
|
||||
(params.groupRoom ? channelEntries[normalizeDiscordSlug(params.groupRoom)] : undefined);
|
||||
if (entry && typeof entry.requireMention === "boolean") {
|
||||
return entry.requireMention;
|
||||
}
|
||||
@@ -160,8 +159,7 @@ export function resolveSlackGroupRequireMention(params: GroupMentionParams): boo
|
||||
const keys = Object.keys(channels);
|
||||
if (keys.length === 0) return true;
|
||||
const channelId = params.groupId?.trim();
|
||||
const groupChannel = params.groupChannel;
|
||||
const channelName = groupChannel?.replace(/^#/, "");
|
||||
const channelName = params.groupRoom?.replace(/^#/, "");
|
||||
const normalizedName = normalizeSlackSlug(channelName);
|
||||
const candidates = [
|
||||
channelId ?? "",
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { resolveChannelMediaMaxBytes } from "./media-limits.js";
|
||||
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeSignalTargetId,
|
||||
normalizeSignalMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { signalOnboardingAdapter } from "./onboarding/signal.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
} from "./config-helpers.js";
|
||||
import { resolveTelegramGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeTelegramTargetId,
|
||||
normalizeTelegramMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { telegramOnboardingAdapter } from "./onboarding/telegram.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import {
|
||||
|
||||
@@ -133,8 +133,7 @@ export type ChannelLogSink = {
|
||||
export type ChannelGroupContext = {
|
||||
cfg: ClawdbotConfig;
|
||||
groupId?: string | null;
|
||||
/** Human label for channel-like group conversations (e.g. #general). */
|
||||
groupChannel?: string | null;
|
||||
groupRoom?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
};
|
||||
|
||||
@@ -26,7 +26,10 @@ import { buildChannelConfigSchema } from "./config-schema.js";
|
||||
import { createWhatsAppLoginTool } from "./agent-tools/whatsapp-login.js";
|
||||
import { resolveWhatsAppGroupRequireMention } from "./group-mentions.js";
|
||||
import { formatPairingApproveHint } from "./helpers.js";
|
||||
import { looksLikeWhatsAppTargetId, normalizeWhatsAppMessagingTarget } from "./normalize-target.js";
|
||||
import {
|
||||
looksLikeWhatsAppTargetId,
|
||||
normalizeWhatsAppMessagingTarget,
|
||||
} from "./normalize-target.js";
|
||||
import { whatsappOnboardingAdapter } from "./onboarding/whatsapp.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
|
||||
@@ -129,9 +129,7 @@ export function normalizeAnyChannelId(raw?: string | null): ChannelId | null {
|
||||
if (!registry) return null;
|
||||
|
||||
const hit = registry.channels.find((entry) => {
|
||||
const id = String(entry.plugin.id ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const id = String(entry.plugin.id ?? "").trim().toLowerCase();
|
||||
if (id && id === key) return true;
|
||||
return (entry.plugin.meta.aliases ?? []).some((alias) => alias.trim().toLowerCase() === key);
|
||||
});
|
||||
|
||||
@@ -12,11 +12,7 @@ import {
|
||||
type HookStatusReport,
|
||||
} from "../hooks/hooks-status.js";
|
||||
import { loadConfig, writeConfigFile } from "../config/io.js";
|
||||
import {
|
||||
installHooksFromNpmSpec,
|
||||
installHooksFromPath,
|
||||
resolveHookInstallDir,
|
||||
} from "../hooks/install.js";
|
||||
import { installHooksFromNpmSpec, installHooksFromPath, resolveHookInstallDir } from "../hooks/install.js";
|
||||
import { recordHookInstall } from "../hooks/installs.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
@@ -736,7 +732,8 @@ export function registerHooksCli(program: Command): void {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir));
|
||||
const nextVersion =
|
||||
result.version ?? (await readInstalledPackageVersion(result.targetDir));
|
||||
nextCfg = recordHookInstall(nextCfg, {
|
||||
hookId,
|
||||
source: "npm",
|
||||
|
||||
@@ -51,9 +51,7 @@ export async function deliverAgentCommandResult(params: {
|
||||
isInternalMessageChannel(deliveryChannel) || Boolean(deliveryPlugin);
|
||||
|
||||
const targetMode =
|
||||
opts.deliveryTargetMode ??
|
||||
deliveryPlan.deliveryTargetMode ??
|
||||
(opts.to ? "explicit" : "implicit");
|
||||
opts.deliveryTargetMode ?? deliveryPlan.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit");
|
||||
const resolvedAccountId = deliveryPlan.resolvedAccountId;
|
||||
const resolved =
|
||||
deliver && isDeliveryChannelKnown && deliveryChannel
|
||||
|
||||
@@ -243,7 +243,10 @@ export function formatMessageCliText(result: MessageActionRunResult): string[] {
|
||||
const results = result.payload.results ?? [];
|
||||
const rows = results.map((entry) => ({
|
||||
Channel: resolveChannelLabel(entry.channel),
|
||||
Target: shortenText(formatTargetDisplay({ channel: entry.channel, target: entry.to }), 36),
|
||||
Target: shortenText(
|
||||
formatTargetDisplay({ channel: entry.channel, target: entry.to }),
|
||||
36,
|
||||
),
|
||||
Status: entry.ok ? "ok" : "error",
|
||||
Error: entry.ok ? "" : shortenText(entry.error ?? "unknown error", 48),
|
||||
}));
|
||||
|
||||
@@ -16,8 +16,8 @@ describe("resolveChannelCapabilities", () => {
|
||||
|
||||
it("returns undefined for missing inputs", () => {
|
||||
expect(resolveChannelCapabilities({})).toBeUndefined();
|
||||
expect(resolveChannelCapabilities({ cfg: {} })).toBeUndefined();
|
||||
expect(resolveChannelCapabilities({ cfg: {}, channel: "" })).toBeUndefined();
|
||||
expect(resolveChannelCapabilities({ cfg: {} as ClawdbotConfig })).toBeUndefined();
|
||||
expect(resolveChannelCapabilities({ cfg: {} as ClawdbotConfig, channel: "" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes and prefers per-account capabilities", () => {
|
||||
@@ -36,7 +36,7 @@ describe("resolveChannelCapabilities", () => {
|
||||
|
||||
expect(
|
||||
resolveChannelCapabilities({
|
||||
cfg,
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
}),
|
||||
@@ -57,7 +57,7 @@ describe("resolveChannelCapabilities", () => {
|
||||
|
||||
expect(
|
||||
resolveChannelCapabilities({
|
||||
cfg,
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
}),
|
||||
@@ -77,7 +77,7 @@ describe("resolveChannelCapabilities", () => {
|
||||
|
||||
expect(
|
||||
resolveChannelCapabilities({
|
||||
cfg,
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
channel: "slack",
|
||||
accountId: "family",
|
||||
}),
|
||||
@@ -100,7 +100,7 @@ describe("resolveChannelCapabilities", () => {
|
||||
|
||||
expect(
|
||||
resolveChannelCapabilities({
|
||||
cfg,
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
channel: "msteams",
|
||||
}),
|
||||
).toEqual(["polls"]);
|
||||
@@ -120,7 +120,7 @@ describe("resolveChannelCapabilities", () => {
|
||||
// Should return undefined (not crash), allowing channel-specific handlers to process it.
|
||||
expect(
|
||||
resolveChannelCapabilities({
|
||||
cfg,
|
||||
cfg: cfg as ClawdbotConfig,
|
||||
channel: "telegram",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import type { ClawdbotConfig } from "./config.js";
|
||||
import type { TelegramCapabilitiesConfig } from "./types.telegram.js";
|
||||
|
||||
type CapabilitiesConfig = TelegramCapabilitiesConfig;
|
||||
|
||||
const isStringArray = (value: unknown): value is string[] =>
|
||||
Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
||||
|
||||
function normalizeCapabilities(capabilities: CapabilitiesConfig | undefined): string[] | undefined {
|
||||
function normalizeCapabilities(capabilities: string[] | undefined): string[] | undefined {
|
||||
if (!capabilities) return undefined;
|
||||
// Handle object-format capabilities (e.g., { inlineButtons: "dm" }) gracefully.
|
||||
// Channel-specific handlers (like resolveTelegramInlineButtonsScope) process these separately.
|
||||
if (!isStringArray(capabilities)) return undefined;
|
||||
if (!Array.isArray(capabilities)) return undefined;
|
||||
const normalized = capabilities.map((entry) => entry.trim()).filter(Boolean);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function resolveAccountCapabilities(params: {
|
||||
cfg?: { accounts?: Record<string, { capabilities?: CapabilitiesConfig }> } & {
|
||||
capabilities?: CapabilitiesConfig;
|
||||
cfg?: { accounts?: Record<string, { capabilities?: string[] }> } & {
|
||||
capabilities?: string[];
|
||||
};
|
||||
accountId?: string | null;
|
||||
}): string[] | undefined {
|
||||
@@ -45,7 +40,7 @@ function resolveAccountCapabilities(params: {
|
||||
}
|
||||
|
||||
export function resolveChannelCapabilities(params: {
|
||||
cfg?: Partial<ClawdbotConfig>;
|
||||
cfg?: ClawdbotConfig;
|
||||
channel?: string | null;
|
||||
accountId?: string | null;
|
||||
}): string[] | undefined {
|
||||
@@ -56,8 +51,8 @@ export function resolveChannelCapabilities(params: {
|
||||
const channelsConfig = cfg.channels as Record<string, unknown> | undefined;
|
||||
const channelConfig = (channelsConfig?.[channel] ?? (cfg as Record<string, unknown>)[channel]) as
|
||||
| {
|
||||
accounts?: Record<string, { capabilities?: CapabilitiesConfig }>;
|
||||
capabilities?: CapabilitiesConfig;
|
||||
accounts?: Record<string, { capabilities?: string[] }>;
|
||||
capabilities?: string[];
|
||||
}
|
||||
| undefined;
|
||||
return resolveAccountCapabilities({
|
||||
|
||||
@@ -251,7 +251,11 @@ export const MediaUnderstandingScopeSchema = z
|
||||
.object({
|
||||
channel: z.string().optional(),
|
||||
chatType: z
|
||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||
.union([
|
||||
z.literal("direct"),
|
||||
z.literal("group"),
|
||||
z.literal("channel"),
|
||||
])
|
||||
.optional(),
|
||||
keyPrefix: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -39,7 +39,11 @@ export const SessionSchema = z
|
||||
.object({
|
||||
channel: z.string().optional(),
|
||||
chatType: z
|
||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||
.union([
|
||||
z.literal("direct"),
|
||||
z.literal("group"),
|
||||
z.literal("channel"),
|
||||
])
|
||||
.optional(),
|
||||
keyPrefix: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -319,38 +319,38 @@ export async function preflightDiscordMessage(
|
||||
surface: "discord",
|
||||
});
|
||||
|
||||
if (!isDirectMessage) {
|
||||
const ownerAllowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
|
||||
const ownerOk = ownerAllowList
|
||||
? allowListMatches(ownerAllowList, {
|
||||
if (!isDirectMessage) {
|
||||
const ownerAllowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
|
||||
const ownerOk = ownerAllowList
|
||||
? allowListMatches(ownerAllowList, {
|
||||
id: author.id,
|
||||
name: author.username,
|
||||
tag: formatDiscordUserTag(author),
|
||||
})
|
||||
: false;
|
||||
const channelUsers = channelConfig?.users ?? guildInfo?.users;
|
||||
const usersOk =
|
||||
Array.isArray(channelUsers) && channelUsers.length > 0
|
||||
? resolveDiscordUserAllowed({
|
||||
allowList: channelUsers,
|
||||
userId: author.id,
|
||||
userName: author.username,
|
||||
userTag: formatDiscordUserTag(author),
|
||||
})
|
||||
: false;
|
||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||
commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: ownerAllowList != null, allowed: ownerOk },
|
||||
{ configured: Array.isArray(channelUsers) && channelUsers.length > 0, allowed: usersOk },
|
||||
],
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
});
|
||||
const channelUsers = channelConfig?.users ?? guildInfo?.users;
|
||||
const usersOk =
|
||||
Array.isArray(channelUsers) && channelUsers.length > 0
|
||||
? resolveDiscordUserAllowed({
|
||||
allowList: channelUsers,
|
||||
userId: author.id,
|
||||
userName: author.username,
|
||||
userTag: formatDiscordUserTag(author),
|
||||
})
|
||||
: false;
|
||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||
commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: ownerAllowList != null, allowed: ownerOk },
|
||||
{ configured: Array.isArray(channelUsers) && channelUsers.length > 0, allowed: usersOk },
|
||||
],
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
});
|
||||
|
||||
if (allowTextCommands && hasControlCommand(baseText, params.cfg) && !commandAuthorized) {
|
||||
logVerbose(`Blocked discord control command from unauthorized sender ${author.id}`);
|
||||
return null;
|
||||
if (allowTextCommands && hasControlCommand(baseText, params.cfg) && !commandAuthorized) {
|
||||
logVerbose(`Blocked discord control command from unauthorized sender ${author.id}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,8 +124,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
senderDisplay && senderTag && senderDisplay !== senderTag
|
||||
? `${senderDisplay} (${senderTag})`
|
||||
: (senderDisplay ?? senderTag ?? author.id);
|
||||
const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
|
||||
const groupSubject = isDirectMessage ? undefined : groupChannel;
|
||||
const groupRoom = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
|
||||
const groupSubject = isDirectMessage ? undefined : groupRoom;
|
||||
const channelDescription = channelInfo?.topic?.trim();
|
||||
const systemPromptParts = [
|
||||
channelDescription ? `Channel topic: ${channelDescription}` : null,
|
||||
@@ -245,7 +245,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
SenderUsername: author.username,
|
||||
SenderTag: formatDiscordUserTag(author),
|
||||
GroupSubject: groupSubject,
|
||||
GroupChannel: groupChannel,
|
||||
GroupRoom: groupRoom,
|
||||
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
||||
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
||||
Provider: "discord" as const,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
||||
import { getAgentRunContext, registerAgentRunContext } from "../infra/agent-events.js";
|
||||
import { toAgentRequestSessionKey } from "../routing/session-key.js";
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
|
||||
export function resolveSessionKeyForRun(runId: string) {
|
||||
const cached = getAgentRunContext(runId)?.sessionKey;
|
||||
@@ -12,7 +12,8 @@ export function resolveSessionKeyForRun(runId: string) {
|
||||
const found = Object.entries(store).find(([, entry]) => entry?.sessionId === runId);
|
||||
const storeKey = found?.[0];
|
||||
if (storeKey) {
|
||||
const sessionKey = toAgentRequestSessionKey(storeKey) ?? storeKey;
|
||||
const parsed = parseAgentSessionKey(storeKey);
|
||||
const sessionKey = parsed?.rest ?? storeKey;
|
||||
registerAgentRunContext(runId, { sessionKey });
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
rpcReq,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks();
|
||||
@@ -27,16 +26,22 @@ describe("gateway server agent", () => {
|
||||
testState.allowFrom = ["+436769770569"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-stale",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-stale",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -65,14 +70,20 @@ describe("gateway server agent", () => {
|
||||
test("agent forwards sessionKey to agentCommand", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
"agent:main:subagent:abc": {
|
||||
sessionId: "sess-sub",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:subagent:abc": {
|
||||
sessionId: "sess-sub",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -100,17 +111,23 @@ describe("gateway server agent", () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-account",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "default",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-account",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -139,17 +156,23 @@ describe("gateway server agent", () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-explicit",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "legacy",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-explicit",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "legacy",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -178,17 +201,23 @@ describe("gateway server agent", () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-explicit-account",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "legacy",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-explicit-account",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "legacy",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -218,17 +247,23 @@ describe("gateway server agent", () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-implicit",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "kev",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-implicit",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "kev",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -255,14 +290,20 @@ describe("gateway server agent", () => {
|
||||
test("agent forwards image attachments as images[]", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-images",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-images",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -302,14 +343,20 @@ describe("gateway server agent", () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-missing-provider",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-missing-provider",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -337,16 +384,22 @@ describe("gateway server agent", () => {
|
||||
test("agent routes main last-channel whatsapp", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-whatsapp",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-whatsapp",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -376,16 +429,22 @@ describe("gateway server agent", () => {
|
||||
test("agent routes main last-channel telegram", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "telegram",
|
||||
lastTo: "123",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "telegram",
|
||||
lastTo: "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -414,16 +473,22 @@ describe("gateway server agent", () => {
|
||||
test("agent routes main last-channel discord", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-discord",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "discord",
|
||||
lastTo: "channel:discord-123",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-discord",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "discord",
|
||||
lastTo: "channel:discord-123",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -452,16 +517,22 @@ describe("gateway server agent", () => {
|
||||
test("agent routes main last-channel slack", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-slack",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "slack",
|
||||
lastTo: "channel:slack-123",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-slack",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "slack",
|
||||
lastTo: "channel:slack-123",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -490,16 +561,22 @@ describe("gateway server agent", () => {
|
||||
test("agent routes main last-channel signal", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-signal",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "signal",
|
||||
lastTo: "+15551234567",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-signal",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "signal",
|
||||
lastTo: "+15551234567",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
startGatewayServer,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks();
|
||||
@@ -81,16 +80,22 @@ describe("gateway server agent", () => {
|
||||
setActivePluginRegistry(registry);
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-teams",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "msteams",
|
||||
lastTo: "conversation:teams-123",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-teams",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "msteams",
|
||||
lastTo: "conversation:teams-123",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -128,16 +133,22 @@ describe("gateway server agent", () => {
|
||||
setActivePluginRegistry(registry);
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-alias",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "imessage",
|
||||
lastTo: "chat_id:123",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-alias",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "imessage",
|
||||
lastTo: "chat_id:123",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -195,16 +206,22 @@ describe("gateway server agent", () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-webchat",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "webchat",
|
||||
lastTo: "+1555",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-webchat",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "webchat",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -233,16 +250,22 @@ describe("gateway server agent", () => {
|
||||
test("agent uses webchat for internal runs when last provider is webchat", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main-webchat-internal",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "webchat",
|
||||
lastTo: "+1555",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main-webchat-internal",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "webchat",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -385,14 +408,20 @@ describe("gateway server agent", () => {
|
||||
test("agent events stream to webchat clients when run context is registered", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws, {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
rpcReq,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks();
|
||||
@@ -67,15 +66,21 @@ describe("gateway server agent", () => {
|
||||
test("suppresses tool stream events when verbose is off", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
verboseLevel: "off",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
verboseLevel: "off",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws, {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
sessionStoreSaveDelayMs,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks();
|
||||
@@ -29,14 +28,20 @@ describe("gateway server chat", () => {
|
||||
test("chat.history caps payload bytes", { timeout: 15_000 }, async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -73,16 +78,22 @@ describe("gateway server chat", () => {
|
||||
test("chat.send does not overwrite last delivery route", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -94,12 +105,11 @@ describe("gateway server chat", () => {
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const stored = JSON.parse(await fs.readFile(testState.sessionStorePath, "utf-8")) as Record<
|
||||
string,
|
||||
{ lastChannel?: string; lastTo?: string } | undefined
|
||||
>;
|
||||
expect(stored["agent:main:main"]?.lastChannel).toBe("whatsapp");
|
||||
expect(stored["agent:main:main"]?.lastTo).toBe("+1555");
|
||||
const stored = JSON.parse(await fs.readFile(testState.sessionStorePath, "utf-8")) as {
|
||||
main?: { lastChannel?: string; lastTo?: string };
|
||||
};
|
||||
expect(stored.main?.lastChannel).toBe("whatsapp");
|
||||
expect(stored.main?.lastTo).toBe("+1555");
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
@@ -108,14 +118,20 @@ describe("gateway server chat", () => {
|
||||
test("chat.abort cancels an in-flight chat.send", { timeout: 15000 }, async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
let inFlight: Promise<unknown> | undefined;
|
||||
@@ -194,14 +210,20 @@ describe("gateway server chat", () => {
|
||||
test("chat.abort cancels while saving the session store", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
sessionStoreSaveDelayMs.value = 120;
|
||||
|
||||
@@ -266,11 +288,11 @@ describe("gateway server chat", () => {
|
||||
test("chat.send treats /stop as an out-of-band abort", { timeout: 15000 }, async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: { sessionId: "sess-main", updatedAt: Date.now() },
|
||||
},
|
||||
});
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify({ main: { sessionId: "sess-main", updatedAt: Date.now() } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
rpcReq,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks();
|
||||
@@ -97,7 +96,7 @@ describe("gateway server chat", () => {
|
||||
test("chat.abort returns aborted=false for unknown runId", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({ entries: {} });
|
||||
await fs.writeFile(testState.sessionStorePath, JSON.stringify({}, null, 2), "utf-8");
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -117,14 +116,20 @@ describe("gateway server chat", () => {
|
||||
test("chat.abort rejects mismatched sessionKey", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -184,14 +189,20 @@ describe("gateway server chat", () => {
|
||||
test("chat.abort is a no-op after chat.send completes", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -248,14 +259,20 @@ describe("gateway server chat", () => {
|
||||
test("chat.send preserves run ordering for queued runs", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
rpcReq,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks();
|
||||
@@ -107,16 +106,22 @@ describe("gateway server chat", () => {
|
||||
},
|
||||
};
|
||||
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
"discord:group:dev": {
|
||||
sessionId: "sess-discord",
|
||||
updatedAt: Date.now(),
|
||||
chatType: "group",
|
||||
channel: "discord",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"discord:group:dev": {
|
||||
sessionId: "sess-discord",
|
||||
updatedAt: Date.now(),
|
||||
chatType: "group",
|
||||
channel: "discord",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -143,14 +148,20 @@ describe("gateway server chat", () => {
|
||||
},
|
||||
};
|
||||
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
"cron:job-1": {
|
||||
sessionId: "sess-cron",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"cron:job-1": {
|
||||
sessionId: "sess-cron",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -225,14 +236,20 @@ describe("gateway server chat", () => {
|
||||
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < 300; i += 1) {
|
||||
@@ -332,15 +349,21 @@ describe("gateway server chat", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: forkedPath,
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: forkedPath,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -374,14 +397,20 @@ describe("gateway server chat", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -418,14 +447,20 @@ describe("gateway server chat", () => {
|
||||
];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-main.jsonl"),
|
||||
JSON.stringify({
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
startGatewayServer,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
const _decodeWsData = (data: unknown): string => {
|
||||
@@ -218,14 +217,20 @@ describe("gateway server node/bridge", () => {
|
||||
test("bridge RPC chat.history returns session messages", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-main.jsonl"),
|
||||
@@ -269,14 +274,20 @@ describe("gateway server node/bridge", () => {
|
||||
test("bridge RPC sessions.list returns session rows", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port);
|
||||
@@ -320,14 +331,20 @@ describe("gateway server node/bridge", () => {
|
||||
test("bridge chat events are pushed to subscribed nodes", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port);
|
||||
@@ -391,14 +408,20 @@ describe("gateway server node/bridge", () => {
|
||||
test("bridge chat.send forwards image attachments to agentCommand", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port);
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
startGatewayServer,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
const decodeWsData = (data: unknown): string => {
|
||||
@@ -43,16 +42,22 @@ describe("gateway server node/bridge", () => {
|
||||
test("bridge voice transcript defaults to main session", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port);
|
||||
@@ -87,14 +92,20 @@ describe("gateway server node/bridge", () => {
|
||||
test("bridge voice transcript triggers chat events for webchat clients", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws, {
|
||||
@@ -172,14 +183,20 @@ describe("gateway server node/bridge", () => {
|
||||
test("bridge chat.abort cancels while saving the session store", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
sessionStoreSaveDelayMs.value = 120;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
rpcReq,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
|
||||
@@ -66,35 +65,41 @@ describe("gateway server sessions", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: now - 30_000,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
thinkingLevel: "low",
|
||||
verboseLevel: "on",
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "work",
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: now - 30_000,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
thinkingLevel: "low",
|
||||
verboseLevel: "on",
|
||||
lastProvider: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
lastAccountId: "work",
|
||||
},
|
||||
"agent:main:discord:group:dev": {
|
||||
sessionId: "sess-group",
|
||||
updatedAt: now - 120_000,
|
||||
totalTokens: 50,
|
||||
},
|
||||
"agent:main:subagent:one": {
|
||||
sessionId: "sess-subagent",
|
||||
updatedAt: now - 120_000,
|
||||
spawnedBy: "agent:main:main",
|
||||
},
|
||||
global: {
|
||||
sessionId: "sess-global",
|
||||
updatedAt: now - 10_000,
|
||||
},
|
||||
},
|
||||
"discord:group:dev": {
|
||||
sessionId: "sess-group",
|
||||
updatedAt: now - 120_000,
|
||||
totalTokens: 50,
|
||||
},
|
||||
"agent:main:subagent:one": {
|
||||
sessionId: "sess-subagent",
|
||||
updatedAt: now - 120_000,
|
||||
spawnedBy: "agent:main:main",
|
||||
},
|
||||
global: {
|
||||
sessionId: "sess-global",
|
||||
updatedAt: now - 10_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
const hello = await connectOk(ws);
|
||||
@@ -350,15 +355,21 @@ describe("gateway server sessions", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: { sessionId: "sess-main", updatedAt: Date.now() },
|
||||
"discord:group:dev": {
|
||||
sessionId: "sess-active",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:main:main": { sessionId: "sess-main", updatedAt: Date.now() },
|
||||
"agent:main:discord:group:dev": {
|
||||
sessionId: "sess-active",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
embeddedRunMock.activeIds.add("sess-active");
|
||||
embeddedRunMock.waitResults.set("sess-active", true);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
rpcReq,
|
||||
startServerWithClient,
|
||||
testState,
|
||||
writeSessionStore,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks();
|
||||
@@ -26,30 +25,38 @@ describe("gateway server sessions", () => {
|
||||
const workDir = path.join(dir, "work");
|
||||
await fs.mkdir(homeDir, { recursive: true });
|
||||
await fs.mkdir(workDir, { recursive: true });
|
||||
await writeSessionStore({
|
||||
storePath: path.join(homeDir, "sessions.json"),
|
||||
agentId: "home",
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-home-main",
|
||||
updatedAt: Date.now(),
|
||||
await fs.writeFile(
|
||||
path.join(homeDir, "sessions.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:home:main": {
|
||||
sessionId: "sess-home-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
"agent:home:discord:group:dev": {
|
||||
sessionId: "sess-home-group",
|
||||
updatedAt: Date.now() - 1000,
|
||||
},
|
||||
},
|
||||
"discord:group:dev": {
|
||||
sessionId: "sess-home-group",
|
||||
updatedAt: Date.now() - 1000,
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(workDir, "sessions.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:work:main": {
|
||||
sessionId: "sess-work-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await writeSessionStore({
|
||||
storePath: path.join(workDir, "sessions.json"),
|
||||
agentId: "work",
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-work-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
@@ -85,17 +92,20 @@ describe("gateway server sessions", () => {
|
||||
testState.agentsConfig = { list: [{ id: "ops", default: true }] };
|
||||
testState.sessionConfig = { mainKey: "work" };
|
||||
|
||||
await writeSessionStore({
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
agentId: "ops",
|
||||
mainKey: "work",
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-ops-main",
|
||||
updatedAt: Date.now(),
|
||||
JSON.stringify(
|
||||
{
|
||||
"agent:ops:work": {
|
||||
sessionId: "sess-ops-main",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
@@ -6,12 +6,11 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, expect } from "vitest";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
import { resolveMainSessionKeyFromConfig, type SessionEntry } from "../config/sessions.js";
|
||||
import { resolveMainSessionKeyFromConfig } from "../config/sessions.js";
|
||||
import { resetAgentRunContextForTest } from "../infra/agent-events.js";
|
||||
import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import { resetLogger, setLoggerOverride } from "../logging.js";
|
||||
import { DEFAULT_AGENT_ID, toAgentStoreSessionKey } from "../routing/session-key.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
@@ -32,32 +31,6 @@ let previousHome: string | undefined;
|
||||
let tempHome: string | undefined;
|
||||
let tempConfigRoot: string | undefined;
|
||||
|
||||
export async function writeSessionStore(params: {
|
||||
entries: Record<string, Partial<SessionEntry>>;
|
||||
storePath?: string;
|
||||
agentId?: string;
|
||||
mainKey?: string;
|
||||
}): Promise<void> {
|
||||
const storePath = params.storePath ?? testState.sessionStorePath;
|
||||
if (!storePath) throw new Error("writeSessionStore requires testState.sessionStorePath");
|
||||
const agentId = params.agentId ?? DEFAULT_AGENT_ID;
|
||||
const store: Record<string, Partial<SessionEntry>> = {};
|
||||
for (const [requestKey, entry] of Object.entries(params.entries)) {
|
||||
const rawKey = requestKey.trim();
|
||||
const storeKey =
|
||||
rawKey === "global" || rawKey === "unknown"
|
||||
? rawKey
|
||||
: toAgentStoreSessionKey({
|
||||
agentId,
|
||||
requestKey,
|
||||
mainKey: params.mainKey,
|
||||
});
|
||||
store[storeKey] = entry;
|
||||
}
|
||||
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
||||
await fs.writeFile(storePath, JSON.stringify(store, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
export function installGatewayTestHooks() {
|
||||
beforeEach(async () => {
|
||||
setLoggerOverride({ level: "silent", consoleLevel: "silent" });
|
||||
|
||||
@@ -97,8 +97,9 @@ describe("hooks install (e2e)", () => {
|
||||
expect(installResult.ok).toBe(true);
|
||||
if (!installResult.ok) return;
|
||||
|
||||
const { clearInternalHooks, createInternalHookEvent, triggerInternalHook } =
|
||||
await import("./internal-hooks.js");
|
||||
const { clearInternalHooks, createInternalHookEvent, triggerInternalHook } = await import(
|
||||
"./internal-hooks.js"
|
||||
);
|
||||
const { loadInternalHooks } = await import("./loader.js");
|
||||
|
||||
clearInternalHooks();
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("installHooksFromArchive", () => {
|
||||
"---",
|
||||
"name: zip-hook",
|
||||
"description: Zip hook",
|
||||
'metadata: {"clawdbot":{"events":["command:new"]}}',
|
||||
"metadata: {\"clawdbot\":{\"events\":[\"command:new\"]}}",
|
||||
"---",
|
||||
"",
|
||||
"# Zip Hook",
|
||||
@@ -82,7 +82,9 @@ describe("installHooksFromArchive", () => {
|
||||
expect(result.hookPackId).toBe("zip-hooks");
|
||||
expect(result.hooks).toContain("zip-hook");
|
||||
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "zip-hooks"));
|
||||
expect(fs.existsSync(path.join(result.targetDir, "hooks", "zip-hook", "HOOK.md"))).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(result.targetDir, "hooks", "zip-hook", "HOOK.md")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("installs hook packs from tar archives", async () => {
|
||||
@@ -107,7 +109,7 @@ describe("installHooksFromArchive", () => {
|
||||
"---",
|
||||
"name: tar-hook",
|
||||
"description: Tar hook",
|
||||
'metadata: {"clawdbot":{"events":["command:new"]}}',
|
||||
"metadata: {\"clawdbot\":{\"events\":[\"command:new\"]}}",
|
||||
"---",
|
||||
"",
|
||||
"# Tar Hook",
|
||||
@@ -146,7 +148,7 @@ describe("installHooksFromPath", () => {
|
||||
"---",
|
||||
"name: my-hook",
|
||||
"description: My hook",
|
||||
'metadata: {"clawdbot":{"events":["command:new"]}}',
|
||||
"metadata: {\"clawdbot\":{\"events\":[\"command:new\"]}}",
|
||||
"---",
|
||||
"",
|
||||
"# My Hook",
|
||||
|
||||
@@ -134,9 +134,7 @@ async function installHookPackageFromDir(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const hooksDir = params.hooksDir
|
||||
? resolveUserPath(params.hooksDir)
|
||||
: path.join(CONFIG_DIR, "hooks");
|
||||
const hooksDir = params.hooksDir ? resolveUserPath(params.hooksDir) : path.join(CONFIG_DIR, "hooks");
|
||||
await fs.mkdir(hooksDir, { recursive: true });
|
||||
|
||||
const targetDir = resolveHookInstallDir(hookPackId, hooksDir);
|
||||
@@ -234,9 +232,7 @@ async function installHookFromDir(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const hooksDir = params.hooksDir
|
||||
? resolveUserPath(params.hooksDir)
|
||||
: path.join(CONFIG_DIR, "hooks");
|
||||
const hooksDir = params.hooksDir ? resolveUserPath(params.hooksDir) : path.join(CONFIG_DIR, "hooks");
|
||||
await fs.mkdir(hooksDir, { recursive: true });
|
||||
|
||||
const targetDir = resolveHookInstallDir(hookName, hooksDir);
|
||||
|
||||
+6
-2
@@ -79,7 +79,9 @@ export async function loadInternalHooks(
|
||||
// Register for all events listed in metadata
|
||||
const events = entry.clawdbot?.events ?? [];
|
||||
if (events.length === 0) {
|
||||
console.warn(`Hook warning: Hook '${entry.hook.name}' has no events defined in metadata`);
|
||||
console.warn(
|
||||
`Hook warning: Hook '${entry.hook.name}' has no events defined in metadata`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -124,7 +126,9 @@ export async function loadInternalHooks(
|
||||
const handler = mod[exportName];
|
||||
|
||||
if (typeof handler !== "function") {
|
||||
console.error(`Hook error: Handler '${exportName}' from ${modulePath} is not a function`);
|
||||
console.error(
|
||||
`Hook error: Handler '${exportName}' from ${modulePath} is not a function`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,7 @@ function resolvePackageHooks(manifest: HookPackageManifest): string[] {
|
||||
return raw.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
||||
}
|
||||
|
||||
function loadHookFromDir(params: {
|
||||
hookDir: string;
|
||||
source: string;
|
||||
nameHint?: string;
|
||||
}): Hook | null {
|
||||
function loadHookFromDir(params: { hookDir: string; source: string; nameHint?: string }): Hook | null {
|
||||
const hookMdPath = path.join(params.hookDir, "HOOK.md");
|
||||
if (!fs.existsSync(hookMdPath)) return null;
|
||||
|
||||
|
||||
@@ -61,7 +61,10 @@ export async function withTimeout<T>(
|
||||
}
|
||||
}
|
||||
|
||||
async function extractZip(params: { archivePath: string; destDir: string }): Promise<void> {
|
||||
async function extractZip(params: {
|
||||
archivePath: string;
|
||||
destDir: string;
|
||||
}): Promise<void> {
|
||||
const buffer = await fs.readFile(params.archivePath);
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const entries = Object.values(zip.files);
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
normalizeMessageChannel,
|
||||
type GatewayMessageChannel,
|
||||
} from "../../utils/message-channel.js";
|
||||
import {
|
||||
resolveOutboundTarget,
|
||||
resolveSessionDeliveryTarget,
|
||||
type SessionDeliveryTarget,
|
||||
} from "./targets.js";
|
||||
import { resolveOutboundTarget, resolveSessionDeliveryTarget, type SessionDeliveryTarget } from "./targets.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { OutboundTargetResolution } from "./targets.js";
|
||||
|
||||
|
||||
@@ -121,7 +121,9 @@ describe("applyMediaUnderstanding", () => {
|
||||
|
||||
expect(result.appliedAudio).toBe(true);
|
||||
expect(ctx.Transcript).toBe("transcribed text");
|
||||
expect(ctx.Body).toBe("[Audio]\nUser text:\n/capture status\nTranscript:\ntranscribed text");
|
||||
expect(ctx.Body).toBe(
|
||||
"[Audio]\nUser text:\n/capture status\nTranscript:\ntranscribed text",
|
||||
);
|
||||
expect(ctx.CommandBody).toBe("/capture status");
|
||||
expect(ctx.RawBody).toBe("/capture status");
|
||||
expect(ctx.BodyForCommands).toBe("/capture status");
|
||||
|
||||
@@ -78,7 +78,10 @@ export async function applyMediaUnderstanding(params: {
|
||||
}
|
||||
|
||||
if (decisions.length > 0) {
|
||||
ctx.MediaUnderstandingDecisions = [...(ctx.MediaUnderstandingDecisions ?? []), ...decisions];
|
||||
ctx.MediaUnderstandingDecisions = [
|
||||
...(ctx.MediaUnderstandingDecisions ?? []),
|
||||
...decisions,
|
||||
];
|
||||
}
|
||||
|
||||
if (outputs.length > 0) {
|
||||
|
||||
@@ -104,7 +104,7 @@ export function resolveAttachmentKind(
|
||||
attachment: MediaAttachment,
|
||||
): "image" | "audio" | "video" | "document" | "unknown" {
|
||||
const kind = kindFromMime(attachment.mime);
|
||||
if (kind === "image" || kind === "audio" || kind === "video") return kind;
|
||||
if (kind !== "unknown") return kind;
|
||||
|
||||
const ext = getFileExtension(attachment.path ?? attachment.url);
|
||||
if (!ext) return "unknown";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveEntriesWithActiveFallback, resolveModelEntries } from "./resolve.js";
|
||||
import {
|
||||
resolveEntriesWithActiveFallback,
|
||||
resolveModelEntries,
|
||||
} from "./resolve.js";
|
||||
|
||||
const providerRegistry = new Map([
|
||||
["openai", { capabilities: ["image"] }],
|
||||
|
||||
@@ -100,9 +100,7 @@ function buildModelDecision(params: {
|
||||
|
||||
function formatDecisionSummary(decision: MediaUnderstandingDecision): string {
|
||||
const total = decision.attachments.length;
|
||||
const success = decision.attachments.filter(
|
||||
(entry) => entry.chosen?.outcome === "success",
|
||||
).length;
|
||||
const success = decision.attachments.filter((entry) => entry.chosen?.outcome === "success").length;
|
||||
const chosen = decision.attachments.find((entry) => entry.chosen)?.chosen;
|
||||
const provider = chosen?.provider?.trim();
|
||||
const model = chosen?.model?.trim();
|
||||
@@ -357,10 +355,7 @@ async function runAttachmentEntries(params: {
|
||||
cache: MediaAttachmentCache;
|
||||
entries: MediaUnderstandingModelConfig[];
|
||||
config?: MediaUnderstandingConfig;
|
||||
}): Promise<{
|
||||
output: MediaUnderstandingOutput | null;
|
||||
attempts: MediaUnderstandingModelDecision[];
|
||||
}> {
|
||||
}): Promise<{ output: MediaUnderstandingOutput | null; attempts: MediaUnderstandingModelDecision[] }> {
|
||||
const { entries, capability } = params;
|
||||
const attempts: MediaUnderstandingModelDecision[] = [];
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
|
||||
|
||||
vi.mock("chokidar", () => ({
|
||||
default: {
|
||||
watch: vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
close: vi.fn(async () => undefined),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./embeddings.js", () => {
|
||||
return {
|
||||
createEmbeddingProvider: async () => ({
|
||||
requestedProvider: "openai",
|
||||
provider: {
|
||||
id: "mock",
|
||||
model: "mock-embed",
|
||||
embedQuery: async () => [0, 0, 0],
|
||||
embedBatch: async () => {
|
||||
throw new Error("openai embeddings failed: 429 insufficient_quota");
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("memory manager sync failures", () => {
|
||||
let workspaceDir: string;
|
||||
let indexPath: string;
|
||||
let manager: MemoryIndexManager | null = null;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers();
|
||||
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-mem-"));
|
||||
indexPath = path.join(workspaceDir, "index.sqlite");
|
||||
await fs.mkdir(path.join(workspaceDir, "memory"));
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "Hello");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
if (manager) {
|
||||
await manager.close();
|
||||
manager = null;
|
||||
}
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("does not raise unhandledRejection when watch-triggered sync fails", async () => {
|
||||
const unhandled: unknown[] = [];
|
||||
const handler = (reason: unknown) => {
|
||||
unhandled.push(reason);
|
||||
};
|
||||
process.on("unhandledRejection", handler);
|
||||
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
memorySearch: {
|
||||
provider: "openai",
|
||||
model: "mock-embed",
|
||||
store: { path: indexPath },
|
||||
sync: { watch: true, watchDebounceMs: 1, onSessionStart: false, onSearch: false },
|
||||
},
|
||||
},
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await getMemorySearchManager({ cfg, agentId: "main" });
|
||||
expect(result.manager).not.toBeNull();
|
||||
if (!result.manager) throw new Error("manager missing");
|
||||
manager = result.manager;
|
||||
|
||||
// Call the internal scheduler directly; it uses fire-and-forget sync.
|
||||
(manager as unknown as { scheduleWatchSync: () => void }).scheduleWatchSync();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await Promise.resolve();
|
||||
|
||||
process.off("unhandledRejection", handler);
|
||||
expect(unhandled).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope
|
||||
import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
import { resolveUserPath, truncateUtf16Safe } from "../utils.js";
|
||||
import {
|
||||
createEmbeddingProvider,
|
||||
@@ -47,8 +46,6 @@ type MemoryIndexMeta = {
|
||||
const META_KEY = "memory_index_meta_v1";
|
||||
const SNIPPET_MAX_CHARS = 700;
|
||||
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
const INDEX_CACHE = new Map<string, MemoryIndexManager>();
|
||||
|
||||
export class MemoryIndexManager {
|
||||
@@ -317,9 +314,7 @@ export class MemoryIndexManager {
|
||||
if (!minutes || minutes <= 0 || this.intervalTimer) return;
|
||||
const ms = minutes * 60 * 1000;
|
||||
this.intervalTimer = setInterval(() => {
|
||||
void this.sync({ reason: "interval" }).catch((err) => {
|
||||
log.warn(`memory sync failed (interval): ${String(err)}`);
|
||||
});
|
||||
void this.sync({ reason: "interval" });
|
||||
}, ms);
|
||||
}
|
||||
|
||||
@@ -328,9 +323,7 @@ export class MemoryIndexManager {
|
||||
if (this.watchTimer) clearTimeout(this.watchTimer);
|
||||
this.watchTimer = setTimeout(() => {
|
||||
this.watchTimer = null;
|
||||
void this.sync({ reason: "watch" }).catch((err) => {
|
||||
log.warn(`memory sync failed (watch): ${String(err)}`);
|
||||
});
|
||||
void this.sync({ reason: "watch" });
|
||||
}, this.settings.sync.watchDebounceMs);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,25 +16,6 @@ export type ParsedAgentSessionKey = {
|
||||
rest: string;
|
||||
};
|
||||
|
||||
export function toAgentRequestSessionKey(storeKey: string | undefined | null): string | undefined {
|
||||
const raw = (storeKey ?? "").trim();
|
||||
if (!raw) return undefined;
|
||||
return parseAgentSessionKey(raw)?.rest ?? raw;
|
||||
}
|
||||
|
||||
export function toAgentStoreSessionKey(params: {
|
||||
agentId: string;
|
||||
requestKey: string | undefined | null;
|
||||
mainKey?: string | undefined;
|
||||
}): string {
|
||||
const raw = (params.requestKey ?? "").trim();
|
||||
if (!raw || raw === DEFAULT_MAIN_KEY) {
|
||||
return buildAgentMainSessionKey({ agentId: params.agentId, mainKey: params.mainKey });
|
||||
}
|
||||
if (raw.startsWith("agent:")) return raw;
|
||||
return `agent:${normalizeAgentId(params.agentId)}:${raw}`;
|
||||
}
|
||||
|
||||
export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | null): string {
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
||||
|
||||
+10
-14
@@ -474,8 +474,10 @@ async function collectChannelSecurityFindings(params: {
|
||||
|
||||
if (plugin.id === "discord") {
|
||||
const discordCfg =
|
||||
(account as { config?: Record<string, unknown> } | null)?.config ??
|
||||
({} as Record<string, unknown>);
|
||||
(account as { config?: Record<string, unknown> } | null)?.config ?? ({} as Record<
|
||||
string,
|
||||
unknown
|
||||
>);
|
||||
const nativeEnabled = resolveNativeCommandsEnabled({
|
||||
providerId: "discord",
|
||||
providerSetting: coerceNativeSetting(
|
||||
@@ -514,20 +516,15 @@ async function collectChannelSecurityFindings(params: {
|
||||
normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
|
||||
|
||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||
if (
|
||||
!useAccessGroups &&
|
||||
groupPolicy !== "disabled" &&
|
||||
guildsConfigured &&
|
||||
!hasAnyUserAllowlist
|
||||
) {
|
||||
if (!useAccessGroups && groupPolicy !== "disabled" && guildsConfigured && !hasAnyUserAllowlist) {
|
||||
findings.push({
|
||||
checkId: "channels.discord.commands.native.unrestricted",
|
||||
severity: "critical",
|
||||
title: "Discord slash commands are unrestricted",
|
||||
detail:
|
||||
"commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.",
|
||||
'commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.',
|
||||
remediation:
|
||||
"Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users).",
|
||||
'Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users).',
|
||||
});
|
||||
} else if (
|
||||
useAccessGroups &&
|
||||
@@ -543,7 +540,7 @@ async function collectChannelSecurityFindings(params: {
|
||||
detail:
|
||||
"Discord slash commands are enabled, but neither an owner allowFrom list nor any per-guild/channel users allowlist is configured; /… commands will be rejected for everyone.",
|
||||
remediation:
|
||||
"Add your user id to channels.discord.dm.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users.",
|
||||
'Add your user id to channels.discord.dm.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users.',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -570,7 +567,7 @@ async function collectChannelSecurityFindings(params: {
|
||||
const slashCommandEnabled =
|
||||
nativeEnabled ||
|
||||
nativeSkillsEnabled ||
|
||||
(slackCfg.slashCommand as { enabled?: unknown } | undefined)?.enabled === true;
|
||||
((slackCfg.slashCommand as { enabled?: unknown } | undefined)?.enabled === true);
|
||||
if (slashCommandEnabled) {
|
||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||
if (!useAccessGroups) {
|
||||
@@ -583,8 +580,7 @@ async function collectChannelSecurityFindings(params: {
|
||||
remediation: "Set commands.useAccessGroups=true (recommended).",
|
||||
});
|
||||
} else {
|
||||
const dmAllowFromRaw = (account as { dm?: { allowFrom?: unknown } } | null)?.dm
|
||||
?.allowFrom;
|
||||
const dmAllowFromRaw = (account as { dm?: { allowFrom?: unknown } } | null)?.dm?.allowFrom;
|
||||
const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
|
||||
const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []);
|
||||
const ownerAllowFromConfigured =
|
||||
|
||||
@@ -2,14 +2,14 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildSlackSlashCommandMatcher,
|
||||
isSlackChannelAllowedByPolicy,
|
||||
isSlackRoomAllowedByPolicy,
|
||||
resolveSlackThreadTs,
|
||||
} from "./monitor.js";
|
||||
|
||||
describe("slack groupPolicy gating", () => {
|
||||
it("allows when policy is open", () => {
|
||||
expect(
|
||||
isSlackChannelAllowedByPolicy({
|
||||
isSlackRoomAllowedByPolicy({
|
||||
groupPolicy: "open",
|
||||
channelAllowlistConfigured: false,
|
||||
channelAllowed: false,
|
||||
@@ -19,7 +19,7 @@ describe("slack groupPolicy gating", () => {
|
||||
|
||||
it("blocks when policy is disabled", () => {
|
||||
expect(
|
||||
isSlackChannelAllowedByPolicy({
|
||||
isSlackRoomAllowedByPolicy({
|
||||
groupPolicy: "disabled",
|
||||
channelAllowlistConfigured: true,
|
||||
channelAllowed: true,
|
||||
@@ -29,7 +29,7 @@ describe("slack groupPolicy gating", () => {
|
||||
|
||||
it("blocks allowlist when no channel allowlist configured", () => {
|
||||
expect(
|
||||
isSlackChannelAllowedByPolicy({
|
||||
isSlackRoomAllowedByPolicy({
|
||||
groupPolicy: "allowlist",
|
||||
channelAllowlistConfigured: false,
|
||||
channelAllowed: true,
|
||||
@@ -39,7 +39,7 @@ describe("slack groupPolicy gating", () => {
|
||||
|
||||
it("allows allowlist when channel is allowed", () => {
|
||||
expect(
|
||||
isSlackChannelAllowedByPolicy({
|
||||
isSlackRoomAllowedByPolicy({
|
||||
groupPolicy: "allowlist",
|
||||
channelAllowlistConfigured: true,
|
||||
channelAllowed: true,
|
||||
@@ -49,7 +49,7 @@ describe("slack groupPolicy gating", () => {
|
||||
|
||||
it("blocks allowlist when channel is not allowed", () => {
|
||||
expect(
|
||||
isSlackChannelAllowedByPolicy({
|
||||
isSlackRoomAllowedByPolicy({
|
||||
groupPolicy: "allowlist",
|
||||
channelAllowlistConfigured: true,
|
||||
channelAllowed: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { buildSlackSlashCommandMatcher } from "./monitor/commands.js";
|
||||
export { isSlackChannelAllowedByPolicy } from "./monitor/policy.js";
|
||||
export { isSlackRoomAllowedByPolicy } from "./monitor/policy.js";
|
||||
export { monitorSlackProvider } from "./monitor/provider.js";
|
||||
export { resolveSlackThreadTs } from "./monitor/replies.js";
|
||||
export type { MonitorSlackOpts } from "./monitor/types.js";
|
||||
@@ -11,7 +11,7 @@ import type { SlackMessageEvent } from "../types.js";
|
||||
|
||||
import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
||||
import { resolveSlackChannelConfig } from "./channel-config.js";
|
||||
import { isSlackChannelAllowedByPolicy } from "./policy.js";
|
||||
import { isSlackRoomAllowedByPolicy } from "./policy.js";
|
||||
|
||||
export function inferSlackChannelType(
|
||||
channelId?: string | null,
|
||||
@@ -314,7 +314,7 @@ export function createSlackMonitorContext(params: {
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0;
|
||||
if (
|
||||
!isSlackChannelAllowedByPolicy({
|
||||
!isSlackRoomAllowedByPolicy({
|
||||
groupPolicy: params.groupPolicy,
|
||||
channelAllowlistConfigured,
|
||||
channelAllowed,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function isSlackChannelAllowedByPolicy(params: {
|
||||
export function isSlackRoomAllowedByPolicy(params: {
|
||||
groupPolicy: "open" | "disabled" | "allowlist";
|
||||
channelAllowlistConfigured: boolean;
|
||||
channelAllowed: boolean;
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./channel-config.js";
|
||||
import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js";
|
||||
import type { SlackMonitorContext } from "./context.js";
|
||||
import { isSlackChannelAllowedByPolicy } from "./policy.js";
|
||||
import { isSlackRoomAllowedByPolicy } from "./policy.js";
|
||||
import { deliverSlackSlashReplies } from "./replies.js";
|
||||
|
||||
type SlackBlock = { type: string; [key: string]: unknown };
|
||||
@@ -247,7 +247,7 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
Boolean(ctx.channelsConfig) && Object.keys(ctx.channelsConfig ?? {}).length > 0;
|
||||
const channelAllowed = channelConfig?.allowed !== false;
|
||||
if (
|
||||
!isSlackChannelAllowedByPolicy({
|
||||
!isSlackRoomAllowedByPolicy({
|
||||
groupPolicy: ctx.groupPolicy,
|
||||
channelAllowlistConfigured,
|
||||
channelAllowed,
|
||||
|
||||
@@ -110,8 +110,8 @@ export const registerTelegramNativeCommands = ({
|
||||
|
||||
if (allCommands.length > 0) {
|
||||
bot.api.setMyCommands(allCommands).catch((err) => {
|
||||
runtime.error?.(danger(`telegram setMyCommands failed: ${String(err)}`));
|
||||
});
|
||||
runtime.error?.(danger(`telegram setMyCommands failed: ${String(err)}`));
|
||||
});
|
||||
|
||||
if (typeof (bot as unknown as { command?: unknown }).command !== "function") {
|
||||
logVerbose("telegram: bot.command unavailable; skipping native handlers");
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveTelegramTargetChatType } from "./inline-buttons.js";
|
||||
|
||||
describe("resolveTelegramTargetChatType", () => {
|
||||
it("returns 'direct' for positive numeric IDs", () => {
|
||||
expect(resolveTelegramTargetChatType("5232990709")).toBe("direct");
|
||||
expect(resolveTelegramTargetChatType("123456789")).toBe("direct");
|
||||
});
|
||||
|
||||
it("returns 'group' for negative numeric IDs", () => {
|
||||
expect(resolveTelegramTargetChatType("-123456789")).toBe("group");
|
||||
expect(resolveTelegramTargetChatType("-1001234567890")).toBe("group");
|
||||
});
|
||||
|
||||
it("handles telegram: prefix from normalizeTelegramMessagingTarget", () => {
|
||||
expect(resolveTelegramTargetChatType("telegram:5232990709")).toBe("direct");
|
||||
expect(resolveTelegramTargetChatType("telegram:-123456789")).toBe("group");
|
||||
expect(resolveTelegramTargetChatType("TELEGRAM:5232990709")).toBe("direct");
|
||||
});
|
||||
|
||||
it("handles tg/group prefixes and topic suffixes", () => {
|
||||
expect(resolveTelegramTargetChatType("tg:5232990709")).toBe("direct");
|
||||
expect(resolveTelegramTargetChatType("group:-123456789")).toBe("group");
|
||||
expect(resolveTelegramTargetChatType("telegram:group:-1001234567890")).toBe("group");
|
||||
expect(resolveTelegramTargetChatType("telegram:group:-1001234567890:topic:456")).toBe("group");
|
||||
expect(resolveTelegramTargetChatType("-1001234567890:456")).toBe("group");
|
||||
});
|
||||
|
||||
it("returns 'unknown' for usernames", () => {
|
||||
expect(resolveTelegramTargetChatType("@username")).toBe("unknown");
|
||||
expect(resolveTelegramTargetChatType("telegram:@username")).toBe("unknown");
|
||||
});
|
||||
|
||||
it("returns 'unknown' for empty strings", () => {
|
||||
expect(resolveTelegramTargetChatType("")).toBe("unknown");
|
||||
expect(resolveTelegramTargetChatType(" ")).toBe("unknown");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { TelegramInlineButtonsScope } from "../config/types.telegram.js";
|
||||
import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js";
|
||||
import { parseTelegramTarget } from "./targets.js";
|
||||
|
||||
const DEFAULT_INLINE_BUTTONS_SCOPE: TelegramInlineButtonsScope = "allowlist";
|
||||
|
||||
@@ -62,12 +61,10 @@ export function isTelegramInlineButtonsEnabled(params: {
|
||||
}
|
||||
|
||||
export function resolveTelegramTargetChatType(target: string): "direct" | "group" | "unknown" {
|
||||
if (!target.trim()) return "unknown";
|
||||
const parsed = parseTelegramTarget(target);
|
||||
const chatId = parsed.chatId.trim();
|
||||
if (!chatId) return "unknown";
|
||||
if (/^-?\d+$/.test(chatId)) {
|
||||
return chatId.startsWith("-") ? "group" : "direct";
|
||||
const trimmed = target.trim();
|
||||
if (!trimmed) return "unknown";
|
||||
if (/^-?\d+$/.test(trimmed)) {
|
||||
return trimmed.startsWith("-") ? "group" : "direct";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
@@ -8,11 +8,10 @@ type EventHandlerContext = {
|
||||
tui: TUI;
|
||||
state: TuiStateAccess;
|
||||
setActivityStatus: (text: string) => void;
|
||||
refreshSessionInfo?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function createEventHandlers(context: EventHandlerContext) {
|
||||
const { chatLog, tui, state, setActivityStatus, refreshSessionInfo } = context;
|
||||
const { chatLog, tui, state, setActivityStatus } = context;
|
||||
const finalizedRuns = new Map<string, number>();
|
||||
|
||||
const noteFinalizedRun = (runId: string) => {
|
||||
@@ -65,20 +64,16 @@ export function createEventHandlers(context: EventHandlerContext) {
|
||||
noteFinalizedRun(evt.runId);
|
||||
state.activeChatRunId = null;
|
||||
setActivityStatus(stopReason === "error" ? "error" : "idle");
|
||||
// Refresh session info to update token counts in footer
|
||||
void refreshSessionInfo?.();
|
||||
}
|
||||
if (evt.state === "aborted") {
|
||||
chatLog.addSystem("run aborted");
|
||||
state.activeChatRunId = null;
|
||||
setActivityStatus("aborted");
|
||||
void refreshSessionInfo?.();
|
||||
}
|
||||
if (evt.state === "error") {
|
||||
chatLog.addSystem(`run error: ${evt.errorMessage ?? "unknown"}`);
|
||||
state.activeChatRunId = null;
|
||||
setActivityStatus("error");
|
||||
void refreshSessionInfo?.();
|
||||
}
|
||||
tui.requestRender();
|
||||
};
|
||||
|
||||
@@ -41,7 +41,6 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
updateAutocompleteProvider,
|
||||
setActivityStatus,
|
||||
} = context;
|
||||
let refreshSessionInfoPromise: Promise<void> | null = null;
|
||||
|
||||
const applyAgentsResult = (result: GatewayAgentsList) => {
|
||||
state.agentDefaultId = normalizeAgentId(result.defaultId);
|
||||
@@ -96,51 +95,43 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
};
|
||||
|
||||
const refreshSessionInfo = async () => {
|
||||
if (refreshSessionInfoPromise) return refreshSessionInfoPromise;
|
||||
refreshSessionInfoPromise = (async () => {
|
||||
try {
|
||||
const listAgentId =
|
||||
state.currentSessionKey === "global" || state.currentSessionKey === "unknown"
|
||||
? undefined
|
||||
: state.currentAgentId;
|
||||
const result = await client.listSessions({
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
agentId: listAgentId,
|
||||
});
|
||||
const entry = result.sessions.find((row) => {
|
||||
// Exact match
|
||||
if (row.key === state.currentSessionKey) return true;
|
||||
// Also match canonical keys like "agent:default:main" against "main"
|
||||
const parsed = parseAgentSessionKey(row.key);
|
||||
return parsed?.rest === state.currentSessionKey;
|
||||
});
|
||||
state.sessionInfo = {
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
model: entry?.model ?? result.defaults?.model ?? undefined,
|
||||
modelProvider: entry?.modelProvider ?? result.defaults?.modelProvider ?? undefined,
|
||||
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
|
||||
inputTokens: entry?.inputTokens ?? null,
|
||||
outputTokens: entry?.outputTokens ?? null,
|
||||
totalTokens: entry?.totalTokens ?? null,
|
||||
responseUsage: entry?.responseUsage,
|
||||
updatedAt: entry?.updatedAt ?? null,
|
||||
displayName: entry?.displayName,
|
||||
};
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`sessions list failed: ${String(err)}`);
|
||||
}
|
||||
updateAutocompleteProvider();
|
||||
updateFooter();
|
||||
tui.requestRender();
|
||||
})();
|
||||
try {
|
||||
await refreshSessionInfoPromise;
|
||||
} finally {
|
||||
refreshSessionInfoPromise = null;
|
||||
const listAgentId =
|
||||
state.currentSessionKey === "global" || state.currentSessionKey === "unknown"
|
||||
? undefined
|
||||
: state.currentAgentId;
|
||||
const result = await client.listSessions({
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
agentId: listAgentId,
|
||||
});
|
||||
const entry = result.sessions.find((row) => {
|
||||
// Exact match
|
||||
if (row.key === state.currentSessionKey) return true;
|
||||
// Also match canonical keys like "agent:default:main" against "main"
|
||||
const parsed = parseAgentSessionKey(row.key);
|
||||
return parsed?.rest === state.currentSessionKey;
|
||||
});
|
||||
state.sessionInfo = {
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
model: entry?.model ?? result.defaults?.model ?? undefined,
|
||||
modelProvider: entry?.modelProvider ?? result.defaults?.modelProvider ?? undefined,
|
||||
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
|
||||
inputTokens: entry?.inputTokens ?? null,
|
||||
outputTokens: entry?.outputTokens ?? null,
|
||||
totalTokens: entry?.totalTokens ?? null,
|
||||
responseUsage: entry?.responseUsage,
|
||||
updatedAt: entry?.updatedAt ?? null,
|
||||
displayName: entry?.displayName,
|
||||
};
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`sessions list failed: ${String(err)}`);
|
||||
}
|
||||
updateAutocompleteProvider();
|
||||
updateFooter();
|
||||
tui.requestRender();
|
||||
};
|
||||
|
||||
const loadHistory = async () => {
|
||||
|
||||
@@ -399,7 +399,6 @@ export async function runTui(opts: TuiOptions) {
|
||||
tui,
|
||||
state,
|
||||
setActivityStatus,
|
||||
refreshSessionInfo,
|
||||
});
|
||||
|
||||
const { handleCommand, sendMessage, openModelSelector, openAgentSelector, openSessionSelector } =
|
||||
|
||||
@@ -19,7 +19,7 @@ export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryCon
|
||||
if (!context) return undefined;
|
||||
const channel =
|
||||
typeof context.channel === "string"
|
||||
? (normalizeMessageChannel(context.channel) ?? context.channel.trim())
|
||||
? normalizeMessageChannel(context.channel) ?? context.channel.trim()
|
||||
: undefined;
|
||||
const to = typeof context.to === "string" ? context.to.trim() : undefined;
|
||||
const accountId = normalizeAccountId(context.accountId);
|
||||
@@ -31,7 +31,9 @@ export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryCon
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeSessionDeliveryFields(source?: DeliveryContextSessionSource): {
|
||||
export function normalizeSessionDeliveryFields(
|
||||
source?: DeliveryContextSessionSource,
|
||||
): {
|
||||
deliveryContext?: DeliveryContext;
|
||||
lastChannel?: string;
|
||||
lastTo?: string;
|
||||
|
||||
@@ -78,15 +78,9 @@ async function resolveWhatsAppCommandAuthorized(params: {
|
||||
}
|
||||
|
||||
const storeAllowFrom = await readChannelAllowFromStore("whatsapp").catch(() => []);
|
||||
const combinedAllowFrom = Array.from(
|
||||
new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]),
|
||||
);
|
||||
const combinedAllowFrom = Array.from(new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]));
|
||||
const allowFrom =
|
||||
combinedAllowFrom.length > 0
|
||||
? combinedAllowFrom
|
||||
: params.msg.selfE164
|
||||
? [params.msg.selfE164]
|
||||
: [];
|
||||
combinedAllowFrom.length > 0 ? combinedAllowFrom : params.msg.selfE164 ? [params.msg.selfE164] : [];
|
||||
if (allowFrom.some((v) => String(v).trim() === "*")) return true;
|
||||
return normalizeAllowFromE164(allowFrom).includes(senderE164);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user