mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 23:41:24 +00:00
chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
chunkMarkdownText,
|
||||
chunkText,
|
||||
resolveTextChunkLimit,
|
||||
} from "./chunk.js";
|
||||
import { chunkMarkdownText, chunkText, resolveTextChunkLimit } from "./chunk.js";
|
||||
|
||||
function expectFencesBalanced(chunks: string[]) {
|
||||
for (const chunk of chunks) {
|
||||
@@ -32,10 +28,7 @@ type ChunkCase = {
|
||||
expected: string[];
|
||||
};
|
||||
|
||||
function runChunkCases(
|
||||
chunker: (text: string, limit: number) => string[],
|
||||
cases: ChunkCase[],
|
||||
) {
|
||||
function runChunkCases(chunker: (text: string, limit: number) => string[], cases: ChunkCase[]) {
|
||||
for (const { name, text, limit, expected } of cases) {
|
||||
it(name, () => {
|
||||
expect(chunker(text, limit)).toEqual(expected);
|
||||
@@ -84,21 +77,15 @@ describe("chunkText", () => {
|
||||
it("prefers breaking at a newline before the limit", () => {
|
||||
const text = `paragraph one line\n\nparagraph two starts here and continues`;
|
||||
const chunks = chunkText(text, 40);
|
||||
expect(chunks).toEqual([
|
||||
"paragraph one line",
|
||||
"paragraph two starts here and continues",
|
||||
]);
|
||||
expect(chunks).toEqual(["paragraph one line", "paragraph two starts here and continues"]);
|
||||
});
|
||||
|
||||
it("otherwise breaks at the last whitespace under the limit", () => {
|
||||
const text =
|
||||
"This is a message that should break nicely near a word boundary.";
|
||||
const text = "This is a message that should break nicely near a word boundary.";
|
||||
const chunks = chunkText(text, 30);
|
||||
expect(chunks[0].length).toBeLessThanOrEqual(30);
|
||||
expect(chunks[1].length).toBeLessThanOrEqual(30);
|
||||
expect(chunks.join(" ").replace(/\s+/g, " ").trim()).toBe(
|
||||
text.replace(/\s+/g, " ").trim(),
|
||||
);
|
||||
expect(chunks.join(" ").replace(/\s+/g, " ").trim()).toBe(text.replace(/\s+/g, " ").trim());
|
||||
});
|
||||
|
||||
it("falls back to a hard break when no whitespace is present", () => {
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
findFenceSpanAt,
|
||||
isSafeFenceBreak,
|
||||
parseFenceSpans,
|
||||
} from "../markdown/fences.js";
|
||||
import { findFenceSpanAt, isSafeFenceBreak, parseFenceSpans } from "../markdown/fences.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||
|
||||
@@ -58,9 +54,7 @@ export function resolveTextChunkLimit(
|
||||
if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return undefined;
|
||||
const channelsConfig = cfg?.channels as Record<string, unknown> | undefined;
|
||||
const providerConfig = (channelsConfig?.[provider] ??
|
||||
(cfg as Record<string, unknown> | undefined)?.[provider]) as
|
||||
| ProviderChunkConfig
|
||||
| undefined;
|
||||
(cfg as Record<string, unknown> | undefined)?.[provider]) as ProviderChunkConfig | undefined;
|
||||
return resolveChunkLimitForProvider(providerConfig, accountId);
|
||||
})();
|
||||
if (typeof providerOverride === "number" && providerOverride > 0) {
|
||||
@@ -96,12 +90,8 @@ export function chunkText(text: string, limit: number): string[] {
|
||||
}
|
||||
|
||||
// If we broke on whitespace/newline, skip that separator; for hard breaks keep it.
|
||||
const brokeOnSeparator =
|
||||
breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
||||
const nextStart = Math.min(
|
||||
remaining.length,
|
||||
breakIdx + (brokeOnSeparator ? 1 : 0),
|
||||
);
|
||||
const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
||||
const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0));
|
||||
remaining = remaining.slice(nextStart).trimStart();
|
||||
}
|
||||
|
||||
@@ -145,10 +135,7 @@ export function chunkMarkdownText(text: string, limit: number): string[] {
|
||||
const maxIdxIfAlreadyNewline = limit - closeLine.length;
|
||||
|
||||
let pickedNewline = false;
|
||||
let lastNewline = remaining.lastIndexOf(
|
||||
"\n",
|
||||
Math.max(0, maxIdxIfAlreadyNewline - 1),
|
||||
);
|
||||
let lastNewline = remaining.lastIndexOf("\n", Math.max(0, maxIdxIfAlreadyNewline - 1));
|
||||
while (lastNewline !== -1) {
|
||||
const candidateBreak = lastNewline + 1;
|
||||
if (candidateBreak < minProgressIdx) break;
|
||||
@@ -173,27 +160,19 @@ export function chunkMarkdownText(text: string, limit: number): string[] {
|
||||
|
||||
const fenceAtBreak = findFenceSpanAt(spans, breakIdx);
|
||||
fenceToSplit =
|
||||
fenceAtBreak && fenceAtBreak.start === initialFence.start
|
||||
? fenceAtBreak
|
||||
: undefined;
|
||||
fenceAtBreak && fenceAtBreak.start === initialFence.start ? fenceAtBreak : undefined;
|
||||
}
|
||||
|
||||
let rawChunk = remaining.slice(0, breakIdx);
|
||||
if (!rawChunk) break;
|
||||
|
||||
const brokeOnSeparator =
|
||||
breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
||||
const nextStart = Math.min(
|
||||
remaining.length,
|
||||
breakIdx + (brokeOnSeparator ? 1 : 0),
|
||||
);
|
||||
const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
||||
const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0));
|
||||
let next = remaining.slice(nextStart);
|
||||
|
||||
if (fenceToSplit) {
|
||||
const closeLine = `${fenceToSplit.indent}${fenceToSplit.marker}`;
|
||||
rawChunk = rawChunk.endsWith("\n")
|
||||
? `${rawChunk}${closeLine}`
|
||||
: `${rawChunk}\n${closeLine}`;
|
||||
rawChunk = rawChunk.endsWith("\n") ? `${rawChunk}${closeLine}` : `${rawChunk}\n${closeLine}`;
|
||||
next = `${fenceToSplit.openLine}\n${next}`;
|
||||
} else {
|
||||
next = stripLeadingNewlines(next);
|
||||
@@ -213,13 +192,9 @@ function stripLeadingNewlines(value: string): string {
|
||||
return i > 0 ? value.slice(i) : value;
|
||||
}
|
||||
|
||||
function pickSafeBreakIndex(
|
||||
window: string,
|
||||
spans: ReturnType<typeof parseFenceSpans>,
|
||||
): number {
|
||||
const { lastNewline, lastWhitespace } = scanParenAwareBreakpoints(
|
||||
window,
|
||||
(index) => isSafeFenceBreak(spans, index),
|
||||
function pickSafeBreakIndex(window: string, spans: ReturnType<typeof parseFenceSpans>): number {
|
||||
const { lastNewline, lastWhitespace } = scanParenAwareBreakpoints(window, (index) =>
|
||||
isSafeFenceBreak(spans, index),
|
||||
);
|
||||
|
||||
if (lastNewline > 0) return lastNewline;
|
||||
|
||||
@@ -14,10 +14,7 @@ export type CommandAuthorization = {
|
||||
to?: string;
|
||||
};
|
||||
|
||||
function resolveProviderFromContext(
|
||||
ctx: MsgContext,
|
||||
cfg: ClawdbotConfig,
|
||||
): ChannelId | undefined {
|
||||
function resolveProviderFromContext(ctx: MsgContext, cfg: ClawdbotConfig): ChannelId | undefined {
|
||||
const direct =
|
||||
normalizeChannelId(ctx.Provider) ??
|
||||
normalizeChannelId(ctx.Surface) ??
|
||||
@@ -79,12 +76,9 @@ export function resolveCommandAuthorization(params: {
|
||||
allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [],
|
||||
});
|
||||
const allowAll =
|
||||
allowFromList.length === 0 ||
|
||||
allowFromList.some((entry) => entry.trim() === "*");
|
||||
allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*");
|
||||
|
||||
const ownerCandidates = allowAll
|
||||
? []
|
||||
: allowFromList.filter((entry) => entry !== "*");
|
||||
const ownerCandidates = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
|
||||
if (!allowAll && ownerCandidates.length === 0 && to) {
|
||||
const normalizedTo = formatAllowFromList({
|
||||
dock,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { listChannelDocks } from "../channels/dock.js";
|
||||
import type {
|
||||
ChatCommandDefinition,
|
||||
CommandScope,
|
||||
} from "./commands-registry.types.js";
|
||||
import type { ChatCommandDefinition, CommandScope } from "./commands-registry.types.js";
|
||||
|
||||
type DefineChatCommandInput = {
|
||||
key: string;
|
||||
@@ -14,17 +11,12 @@ type DefineChatCommandInput = {
|
||||
scope?: CommandScope;
|
||||
};
|
||||
|
||||
function defineChatCommand(
|
||||
command: DefineChatCommandInput,
|
||||
): ChatCommandDefinition {
|
||||
const aliases = (
|
||||
command.textAliases ?? (command.textAlias ? [command.textAlias] : [])
|
||||
)
|
||||
function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition {
|
||||
const aliases = (command.textAliases ?? (command.textAlias ? [command.textAlias] : []))
|
||||
.map((alias) => alias.trim())
|
||||
.filter(Boolean);
|
||||
const scope =
|
||||
command.scope ??
|
||||
(command.nativeName ? (aliases.length ? "both" : "native") : "text");
|
||||
command.scope ?? (command.nativeName ? (aliases.length ? "both" : "native") : "text");
|
||||
return {
|
||||
key: command.key,
|
||||
nativeName: command.nativeName,
|
||||
@@ -35,18 +27,12 @@ function defineChatCommand(
|
||||
};
|
||||
}
|
||||
|
||||
function registerAlias(
|
||||
commands: ChatCommandDefinition[],
|
||||
key: string,
|
||||
...aliases: string[]
|
||||
): void {
|
||||
function registerAlias(commands: ChatCommandDefinition[], key: string, ...aliases: string[]): void {
|
||||
const command = commands.find((entry) => entry.key === key);
|
||||
if (!command) {
|
||||
throw new Error(`registerAlias: unknown command key: ${key}`);
|
||||
}
|
||||
const existing = new Set(
|
||||
command.textAliases.map((alias) => alias.trim().toLowerCase()),
|
||||
);
|
||||
const existing = new Set(command.textAliases.map((alias) => alias.trim().toLowerCase()));
|
||||
for (const alias of aliases) {
|
||||
const trimmed = alias.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
@@ -95,9 +95,7 @@ describe("commands registry", () => {
|
||||
});
|
||||
|
||||
it("normalizes telegram-style command mentions for the current bot", () => {
|
||||
expect(
|
||||
normalizeCommandBody("/help@clawdbot", { botUsername: "clawdbot" }),
|
||||
).toBe("/help");
|
||||
expect(normalizeCommandBody("/help@clawdbot", { botUsername: "clawdbot" })).toBe("/help");
|
||||
expect(
|
||||
normalizeCommandBody("/help@clawdbot args", {
|
||||
botUsername: "clawdbot",
|
||||
@@ -111,8 +109,8 @@ describe("commands registry", () => {
|
||||
});
|
||||
|
||||
it("keeps telegram-style command mentions for other bots", () => {
|
||||
expect(
|
||||
normalizeCommandBody("/help@otherbot", { botUsername: "clawdbot" }),
|
||||
).toBe("/help@otherbot");
|
||||
expect(normalizeCommandBody("/help@otherbot", { botUsername: "clawdbot" })).toBe(
|
||||
"/help@otherbot",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import {
|
||||
CHAT_COMMANDS,
|
||||
getNativeCommandSurfaces,
|
||||
} from "./commands-registry.data.js";
|
||||
import { CHAT_COMMANDS, getNativeCommandSurfaces } from "./commands-registry.data.js";
|
||||
import type {
|
||||
ChatCommandDefinition,
|
||||
CommandDetection,
|
||||
@@ -56,35 +53,28 @@ export function listChatCommands(): ChatCommandDefinition[] {
|
||||
return [...CHAT_COMMANDS];
|
||||
}
|
||||
|
||||
export function isCommandEnabled(
|
||||
cfg: ClawdbotConfig,
|
||||
commandKey: string,
|
||||
): boolean {
|
||||
export function isCommandEnabled(cfg: ClawdbotConfig, commandKey: string): boolean {
|
||||
if (commandKey === "config") return cfg.commands?.config === true;
|
||||
if (commandKey === "debug") return cfg.commands?.debug === true;
|
||||
if (commandKey === "bash") return cfg.commands?.bash === true;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function listChatCommandsForConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
): ChatCommandDefinition[] {
|
||||
export function listChatCommandsForConfig(cfg: ClawdbotConfig): ChatCommandDefinition[] {
|
||||
return CHAT_COMMANDS.filter((command) => isCommandEnabled(cfg, command.key));
|
||||
}
|
||||
|
||||
export function listNativeCommandSpecs(): NativeCommandSpec[] {
|
||||
return CHAT_COMMANDS.filter(
|
||||
(command) => command.scope !== "text" && command.nativeName,
|
||||
).map((command) => ({
|
||||
name: command.nativeName ?? command.key,
|
||||
description: command.description,
|
||||
acceptsArgs: Boolean(command.acceptsArgs),
|
||||
}));
|
||||
return CHAT_COMMANDS.filter((command) => command.scope !== "text" && command.nativeName).map(
|
||||
(command) => ({
|
||||
name: command.nativeName ?? command.key,
|
||||
description: command.description,
|
||||
acceptsArgs: Boolean(command.acceptsArgs),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function listNativeCommandSpecsForConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
): NativeCommandSpec[] {
|
||||
export function listNativeCommandSpecsForConfig(cfg: ClawdbotConfig): NativeCommandSpec[] {
|
||||
return listChatCommandsForConfig(cfg)
|
||||
.filter((command) => command.scope !== "text" && command.nativeName)
|
||||
.map((command) => ({
|
||||
@@ -94,14 +84,10 @@ export function listNativeCommandSpecsForConfig(
|
||||
}));
|
||||
}
|
||||
|
||||
export function findCommandByNativeName(
|
||||
name: string,
|
||||
): ChatCommandDefinition | undefined {
|
||||
export function findCommandByNativeName(name: string): ChatCommandDefinition | undefined {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
return CHAT_COMMANDS.find(
|
||||
(command) =>
|
||||
command.scope !== "text" &&
|
||||
command.nativeName?.toLowerCase() === normalized,
|
||||
(command) => command.scope !== "text" && command.nativeName?.toLowerCase() === normalized,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,16 +96,12 @@ export function buildCommandText(commandName: string, args?: string): string {
|
||||
return trimmedArgs ? `/${commandName} ${trimmedArgs}` : `/${commandName}`;
|
||||
}
|
||||
|
||||
export function normalizeCommandBody(
|
||||
raw: string,
|
||||
options?: CommandNormalizeOptions,
|
||||
): string {
|
||||
export function normalizeCommandBody(raw: string, options?: CommandNormalizeOptions): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed.startsWith("/")) return trimmed;
|
||||
|
||||
const newline = trimmed.indexOf("\n");
|
||||
const singleLine =
|
||||
newline === -1 ? trimmed : trimmed.slice(0, newline).trim();
|
||||
const singleLine = newline === -1 ? trimmed : trimmed.slice(0, newline).trim();
|
||||
|
||||
const colonMatch = singleLine.match(/^\/([^\s:]+)\s*:(.*)$/);
|
||||
const normalized = colonMatch
|
||||
@@ -151,9 +133,7 @@ export function normalizeCommandBody(
|
||||
if (!tokenSpec) return commandBody;
|
||||
if (rest && !tokenSpec.acceptsArgs) return commandBody;
|
||||
const normalizedRest = rest?.trimStart();
|
||||
return normalizedRest
|
||||
? `${tokenSpec.canonical} ${normalizedRest}`
|
||||
: tokenSpec.canonical;
|
||||
return normalizedRest ? `${tokenSpec.canonical} ${normalizedRest}` : tokenSpec.canonical;
|
||||
}
|
||||
|
||||
export function isCommandMessage(raw: string): boolean {
|
||||
@@ -181,9 +161,7 @@ export function getCommandDetection(_cfg?: ClawdbotConfig): CommandDetection {
|
||||
}
|
||||
cachedDetection = {
|
||||
exact,
|
||||
regex: patterns.length
|
||||
? new RegExp(`^(?:${patterns.join("|")})$`, "i")
|
||||
: /$^/,
|
||||
regex: patterns.length ? new RegExp(`^(?:${patterns.join("|")})$`, "i") : /$^/,
|
||||
};
|
||||
return cachedDetection;
|
||||
}
|
||||
@@ -225,9 +203,7 @@ export function isNativeCommandSurface(surface?: string): boolean {
|
||||
return getNativeCommandSurfaces().has(surface.toLowerCase());
|
||||
}
|
||||
|
||||
export function shouldHandleTextCommands(
|
||||
params: ShouldHandleTextCommandsParams,
|
||||
): boolean {
|
||||
export function shouldHandleTextCommands(params: ShouldHandleTextCommandsParams): boolean {
|
||||
if (params.commandSource === "native") return true;
|
||||
if (params.cfg.commands?.text !== false) return true;
|
||||
return !isNativeCommandSurface(params.surface);
|
||||
|
||||
@@ -19,9 +19,7 @@ describe("formatAgentEnvelope", () => {
|
||||
|
||||
process.env.TZ = originalTz;
|
||||
|
||||
expect(body).toBe(
|
||||
"[WebChat user1 mac-mini 10.0.0.5 2025-01-02T03:04Z] hello",
|
||||
);
|
||||
expect(body).toBe("[WebChat user1 mac-mini 10.0.0.5 2025-01-02T03:04Z] hello");
|
||||
});
|
||||
|
||||
it("formats timestamps in UTC regardless of local timezone", () => {
|
||||
|
||||
@@ -2,9 +2,7 @@ import { normalizeCommandBody } from "./commands-registry.js";
|
||||
|
||||
export type GroupActivationMode = "mention" | "always";
|
||||
|
||||
export function normalizeGroupActivation(
|
||||
raw?: string | null,
|
||||
): GroupActivationMode | undefined {
|
||||
export function normalizeGroupActivation(raw?: string | null): GroupActivationMode | undefined {
|
||||
const value = raw?.trim().toLowerCase();
|
||||
if (value === "mention") return "mention";
|
||||
if (value === "always") return "always";
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
stripHeartbeatToken,
|
||||
} from "./heartbeat.js";
|
||||
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "./heartbeat.js";
|
||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||
|
||||
describe("stripHeartbeatToken", () => {
|
||||
@@ -18,26 +15,20 @@ describe("stripHeartbeatToken", () => {
|
||||
text: "",
|
||||
didStrip: false,
|
||||
});
|
||||
expect(stripHeartbeatToken(HEARTBEAT_TOKEN, { mode: "heartbeat" })).toEqual(
|
||||
{
|
||||
shouldSkip: true,
|
||||
text: "",
|
||||
didStrip: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("drops heartbeats with small junk in heartbeat mode", () => {
|
||||
expect(
|
||||
stripHeartbeatToken("HEARTBEAT_OK 🦞", { mode: "heartbeat" }),
|
||||
).toEqual({
|
||||
expect(stripHeartbeatToken(HEARTBEAT_TOKEN, { mode: "heartbeat" })).toEqual({
|
||||
shouldSkip: true,
|
||||
text: "",
|
||||
didStrip: true,
|
||||
});
|
||||
expect(
|
||||
stripHeartbeatToken(`🦞 ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }),
|
||||
).toEqual({
|
||||
});
|
||||
|
||||
it("drops heartbeats with small junk in heartbeat mode", () => {
|
||||
expect(stripHeartbeatToken("HEARTBEAT_OK 🦞", { mode: "heartbeat" })).toEqual({
|
||||
shouldSkip: true,
|
||||
text: "",
|
||||
didStrip: true,
|
||||
});
|
||||
expect(stripHeartbeatToken(`🦞 ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" })).toEqual({
|
||||
shouldSkip: true,
|
||||
text: "",
|
||||
didStrip: true,
|
||||
@@ -45,9 +36,7 @@ describe("stripHeartbeatToken", () => {
|
||||
});
|
||||
|
||||
it("drops short remainder in heartbeat mode", () => {
|
||||
expect(
|
||||
stripHeartbeatToken(`ALERT ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }),
|
||||
).toEqual({
|
||||
expect(stripHeartbeatToken(`ALERT ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" })).toEqual({
|
||||
shouldSkip: true,
|
||||
text: "",
|
||||
didStrip: true,
|
||||
@@ -56,9 +45,7 @@ describe("stripHeartbeatToken", () => {
|
||||
|
||||
it("keeps heartbeat replies when remaining content exceeds threshold", () => {
|
||||
const long = "A".repeat(DEFAULT_HEARTBEAT_ACK_MAX_CHARS + 1);
|
||||
expect(
|
||||
stripHeartbeatToken(`${long} ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }),
|
||||
).toEqual({
|
||||
expect(stripHeartbeatToken(`${long} ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" })).toEqual({
|
||||
shouldSkip: false,
|
||||
text: long,
|
||||
didStrip: true,
|
||||
@@ -66,16 +53,12 @@ describe("stripHeartbeatToken", () => {
|
||||
});
|
||||
|
||||
it("strips token at edges for normal messages", () => {
|
||||
expect(
|
||||
stripHeartbeatToken(`${HEARTBEAT_TOKEN} hello`, { mode: "message" }),
|
||||
).toEqual({
|
||||
expect(stripHeartbeatToken(`${HEARTBEAT_TOKEN} hello`, { mode: "message" })).toEqual({
|
||||
shouldSkip: false,
|
||||
text: "hello",
|
||||
didStrip: true,
|
||||
});
|
||||
expect(
|
||||
stripHeartbeatToken(`hello ${HEARTBEAT_TOKEN}`, { mode: "message" }),
|
||||
).toEqual({
|
||||
expect(stripHeartbeatToken(`hello ${HEARTBEAT_TOKEN}`, { mode: "message" })).toEqual({
|
||||
shouldSkip: false,
|
||||
text: "hello",
|
||||
didStrip: true,
|
||||
@@ -95,9 +78,7 @@ describe("stripHeartbeatToken", () => {
|
||||
});
|
||||
|
||||
it("strips HTML-wrapped heartbeat tokens", () => {
|
||||
expect(
|
||||
stripHeartbeatToken(`<b>${HEARTBEAT_TOKEN}</b>`, { mode: "heartbeat" }),
|
||||
).toEqual({
|
||||
expect(stripHeartbeatToken(`<b>${HEARTBEAT_TOKEN}</b>`, { mode: "heartbeat" })).toEqual({
|
||||
shouldSkip: true,
|
||||
text: "",
|
||||
didStrip: true,
|
||||
@@ -105,9 +86,7 @@ describe("stripHeartbeatToken", () => {
|
||||
});
|
||||
|
||||
it("strips markdown-wrapped heartbeat tokens", () => {
|
||||
expect(
|
||||
stripHeartbeatToken(`**${HEARTBEAT_TOKEN}**`, { mode: "heartbeat" }),
|
||||
).toEqual({
|
||||
expect(stripHeartbeatToken(`**${HEARTBEAT_TOKEN}**`, { mode: "heartbeat" })).toEqual({
|
||||
shouldSkip: true,
|
||||
text: "",
|
||||
didStrip: true,
|
||||
|
||||
@@ -54,9 +54,7 @@ export function stripHeartbeatToken(
|
||||
const mode: StripHeartbeatMode = opts.mode ?? "message";
|
||||
const maxAckCharsRaw = opts.maxAckChars;
|
||||
const parsedAckChars =
|
||||
typeof maxAckCharsRaw === "string"
|
||||
? Number(maxAckCharsRaw)
|
||||
: maxAckCharsRaw;
|
||||
typeof maxAckCharsRaw === "string" ? Number(maxAckCharsRaw) : maxAckCharsRaw;
|
||||
const maxAckChars = Math.max(
|
||||
0,
|
||||
typeof parsedAckChars === "number" && Number.isFinite(parsedAckChars)
|
||||
@@ -77,9 +75,7 @@ export function stripHeartbeatToken(
|
||||
.replace(/[*`~_]+$/, "");
|
||||
|
||||
const trimmedNormalized = stripMarkup(trimmed);
|
||||
const hasToken =
|
||||
trimmed.includes(HEARTBEAT_TOKEN) ||
|
||||
trimmedNormalized.includes(HEARTBEAT_TOKEN);
|
||||
const hasToken = trimmed.includes(HEARTBEAT_TOKEN) || trimmedNormalized.includes(HEARTBEAT_TOKEN);
|
||||
if (!hasToken) {
|
||||
return { shouldSkip: false, text: trimmed, didStrip: false };
|
||||
}
|
||||
@@ -87,9 +83,7 @@ export function stripHeartbeatToken(
|
||||
const strippedOriginal = stripTokenAtEdges(trimmed);
|
||||
const strippedNormalized = stripTokenAtEdges(trimmedNormalized);
|
||||
const picked =
|
||||
strippedOriginal.didStrip && strippedOriginal.text
|
||||
? strippedOriginal
|
||||
: strippedNormalized;
|
||||
strippedOriginal.didStrip && strippedOriginal.text ? strippedOriginal : strippedNormalized;
|
||||
if (!picked.didStrip) {
|
||||
return { shouldSkip: false, text: trimmed, didStrip: false };
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@ function formatMediaAttachedLine(params: {
|
||||
}
|
||||
|
||||
export function buildInboundMediaNote(ctx: MsgContext): string | undefined {
|
||||
const pathsFromArray = Array.isArray(ctx.MediaPaths)
|
||||
? ctx.MediaPaths
|
||||
: undefined;
|
||||
const pathsFromArray = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths : undefined;
|
||||
const paths =
|
||||
pathsFromArray && pathsFromArray.length > 0
|
||||
? pathsFromArray
|
||||
|
||||
@@ -17,9 +17,7 @@ export function extractModelDirective(
|
||||
/(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)?)?/i,
|
||||
);
|
||||
|
||||
const aliases = (options?.aliases ?? [])
|
||||
.map((alias) => alias.trim())
|
||||
.filter(Boolean);
|
||||
const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean);
|
||||
const aliasMatch =
|
||||
modelMatch || aliases.length === 0
|
||||
? null
|
||||
@@ -41,9 +39,7 @@ export function extractModelDirective(
|
||||
rawProfile = parts.slice(1).join("@").trim() || undefined;
|
||||
}
|
||||
|
||||
const cleaned = match
|
||||
? body.replace(match[0], " ").replace(/\s+/g, " ").trim()
|
||||
: body.trim();
|
||||
const cleaned = match ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() : body.trim();
|
||||
|
||||
return {
|
||||
cleaned,
|
||||
|
||||
@@ -6,19 +6,14 @@ import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.j
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
type RunEmbeddedPiAgent =
|
||||
typeof import("../agents/pi-embedded.js").runEmbeddedPiAgent;
|
||||
type RunEmbeddedPiAgent = typeof import("../agents/pi-embedded.js").runEmbeddedPiAgent;
|
||||
type RunEmbeddedPiAgentParams = Parameters<RunEmbeddedPiAgent>[0];
|
||||
|
||||
const piEmbeddedMock = vi.hoisted(() => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn<
|
||||
ReturnType<RunEmbeddedPiAgent>,
|
||||
Parameters<RunEmbeddedPiAgent>
|
||||
>(),
|
||||
runEmbeddedPiAgent: vi.fn<ReturnType<RunEmbeddedPiAgent>, Parameters<RunEmbeddedPiAgent>>(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -84,9 +83,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain("Thinking level set to xhigh.");
|
||||
});
|
||||
});
|
||||
@@ -113,9 +110,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain("Thinking level set to xhigh.");
|
||||
});
|
||||
});
|
||||
@@ -142,9 +137,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain(
|
||||
'Thinking level "xhigh" is only supported for openai/gpt-5.2, openai-codex/gpt-5.2-codex or openai-codex/gpt-5.1-codex.',
|
||||
);
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -98,9 +97,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain("done");
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -263,9 +262,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain("done");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -95,9 +94,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain("done");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0];
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
@@ -14,8 +14,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -96,9 +95,7 @@ describe("directive behavior", () => {
|
||||
baseUrl: "http://127.0.0.1:1234/v1",
|
||||
apiKey: "lmstudio",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{ id: "kimi-k2-0905-preview", name: "Kimi K2 (Local)" },
|
||||
],
|
||||
models: [{ id: "kimi-k2-0905-preview", name: "Kimi K2 (Local)" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -188,9 +185,7 @@ describe("directive behavior", () => {
|
||||
);
|
||||
|
||||
const events = drainSystemEvents(MAIN_SESSION_KEY);
|
||||
expect(events).toContain(
|
||||
"Model switched to Opus (anthropic/claude-opus-4-5).",
|
||||
);
|
||||
expect(events).toContain("Model switched to Opus (anthropic/claude-opus-4-5).");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -95,9 +94,7 @@ describe("directive behavior", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Elevated mode disabled.");
|
||||
expect(text).toContain("Session: agent:main:main");
|
||||
const optionsLine = text
|
||||
?.split("\n")
|
||||
.find((line) => line.trim().startsWith("⚙️"));
|
||||
const optionsLine = text?.split("\n").find((line) => line.trim().startsWith("⚙️"));
|
||||
expect(optionsLine).toBeTruthy();
|
||||
expect(optionsLine).not.toContain("elevated");
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -183,9 +182,7 @@ describe("directive behavior", () => {
|
||||
);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
const optionsLine = text
|
||||
?.split("\n")
|
||||
.find((line) => line.trim().startsWith("⚙️"));
|
||||
const optionsLine = text?.split("\n").find((line) => line.trim().startsWith("⚙️"));
|
||||
expect(optionsLine).toBeTruthy();
|
||||
expect(optionsLine).toContain("elevated");
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -181,9 +180,7 @@ describe("directive behavior", () => {
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Elevated mode disabled.");
|
||||
const optionsLine = text
|
||||
?.split("\n")
|
||||
.find((line) => line.trim().startsWith("⚙️"));
|
||||
const optionsLine = text?.split("\n").find((line) => line.trim().startsWith("⚙️"));
|
||||
expect(optionsLine).toBeTruthy();
|
||||
expect(optionsLine).not.toContain("elevated");
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -217,9 +216,7 @@ describe("directive behavior", () => {
|
||||
baseUrl: "http://127.0.0.1:1234/v1",
|
||||
apiKey: "lmstudio",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{ id: "minimax-m2.1-gs32", name: "MiniMax M2.1 GS32" },
|
||||
],
|
||||
models: [{ id: "minimax-m2.1-gs32", name: "MiniMax M2.1 GS32" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,11 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionKey,
|
||||
saveSessionStore,
|
||||
} from "../config/sessions.js";
|
||||
import { loadSessionStore, resolveSessionKey, saveSessionStore } from "../config/sessions.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -16,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -114,9 +109,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain("done");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -189,9 +182,7 @@ describe("directive behavior", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const texts = (Array.isArray(res) ? res : [res])
|
||||
.map((entry) => entry?.text)
|
||||
.filter(Boolean);
|
||||
const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean);
|
||||
expect(texts).toContain("done");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -187,10 +187,7 @@ describe("directive parsing", () => {
|
||||
});
|
||||
|
||||
it("preserves newlines when stripping reply tags", () => {
|
||||
const res = extractReplyToTag(
|
||||
"line 1\nline 2 [[reply_to_current]]\n\nline 3",
|
||||
"msg-2",
|
||||
);
|
||||
const res = extractReplyToTag("line 1\nline 2 [[reply_to_current]]\n\nline 3", "msg-2");
|
||||
expect(res.replyToId).toBe("msg-2");
|
||||
expect(res.cleaned).toBe("line 1\nline 2\n\nline 3");
|
||||
});
|
||||
|
||||
@@ -26,8 +26,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
@@ -10,8 +10,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -34,8 +33,7 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
},
|
||||
{
|
||||
env: {
|
||||
CLAWDBOT_BUNDLED_SKILLS_DIR: (home) =>
|
||||
path.join(home, "bundled-skills"),
|
||||
CLAWDBOT_BUNDLED_SKILLS_DIR: (home) => path.join(home, "bundled-skills"),
|
||||
},
|
||||
prefix: "clawdbot-media-note-",
|
||||
},
|
||||
|
||||
@@ -14,8 +14,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -105,11 +104,7 @@ describe("queue followups", () => {
|
||||
await Promise.resolve();
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
prompts.some((p) =>
|
||||
p.includes("[Queued messages while agent was busy]"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(prompts.some((p) => p.includes("[Queued messages while agent was busy]"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,23 +127,11 @@ describe("queue followups", () => {
|
||||
drop: "summarize",
|
||||
});
|
||||
|
||||
await getReplyFromConfig(
|
||||
{ Body: "one", From: "+1002", To: "+2000" },
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
await getReplyFromConfig(
|
||||
{ Body: "two", From: "+1002", To: "+2000" },
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
await getReplyFromConfig({ Body: "one", From: "+1002", To: "+2000" }, {}, cfg);
|
||||
await getReplyFromConfig({ Body: "two", From: "+1002", To: "+2000" }, {}, cfg);
|
||||
|
||||
vi.mocked(isEmbeddedPiRunActive).mockReturnValue(false);
|
||||
await getReplyFromConfig(
|
||||
{ Body: "three", From: "+1002", To: "+2000" },
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
await getReplyFromConfig({ Body: "three", From: "+1002", To: "+2000" }, {}, cfg);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await Promise.resolve();
|
||||
|
||||
@@ -9,8 +9,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -228,11 +227,8 @@ describe("RawBody directive parsing", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).toContain(
|
||||
"[Chat messages since your last reply - for context]",
|
||||
);
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).toContain("[Chat messages since your last reply - for context]");
|
||||
expect(prompt).toContain("Peter: hello");
|
||||
expect(prompt).toContain("status please");
|
||||
expect(prompt).not.toContain("/think:high");
|
||||
|
||||
@@ -7,8 +7,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -48,10 +47,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -126,8 +122,7 @@ describe("group intro prompts", () => {
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const extraSystemPrompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
|
||||
?.extraSystemPrompt ?? "";
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
|
||||
expect(extraSystemPrompt).toBe(
|
||||
`You are replying inside the Discord group "Release Squad". Group members: Alice, Bob. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
|
||||
);
|
||||
@@ -158,8 +153,7 @@ describe("group intro prompts", () => {
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const extraSystemPrompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
|
||||
?.extraSystemPrompt ?? "";
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
|
||||
expect(extraSystemPrompt).toBe(
|
||||
`You are replying inside the WhatsApp group "Ops". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant). ${groupParticipationNote} Address the specific sender noted in the message context.`,
|
||||
);
|
||||
@@ -190,8 +184,7 @@ describe("group intro prompts", () => {
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const extraSystemPrompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
|
||||
?.extraSystemPrompt ?? "";
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? "";
|
||||
expect(extraSystemPrompt).toBe(
|
||||
`You are replying inside the Telegram group "Dev Chat". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
|
||||
);
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -163,9 +159,7 @@ describe("trigger handling", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const extra =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.extraSystemPrompt ??
|
||||
"";
|
||||
const extra = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.extraSystemPrompt ?? "";
|
||||
expect(extra).toContain("Test Group");
|
||||
expect(extra).toContain("Activation: always-on");
|
||||
});
|
||||
@@ -207,8 +201,7 @@ describe("trigger handling", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("hello");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).toContain("A new session was started via /new or /reset");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -135,10 +131,7 @@ describe("trigger handling", () => {
|
||||
expect(text).toContain("Elevated mode enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ elevatedLevel?: string }
|
||||
>;
|
||||
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
|
||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
||||
});
|
||||
});
|
||||
@@ -180,10 +173,7 @@ describe("trigger handling", () => {
|
||||
expect(text).toContain("tools.elevated.enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ elevatedLevel?: string }
|
||||
>;
|
||||
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
|
||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { loadSessionStore } from "../config/sessions.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
@@ -146,9 +142,7 @@ describe("trigger handling", () => {
|
||||
expect(text).toContain("Elevated mode disabled.");
|
||||
|
||||
const store = loadSessionStore(cfg.session.store);
|
||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe(
|
||||
"off",
|
||||
);
|
||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("off");
|
||||
});
|
||||
});
|
||||
it("allows elevated directive in groups when mentioned", async () => {
|
||||
@@ -191,13 +185,8 @@ describe("trigger handling", () => {
|
||||
expect(text).toContain("Elevated mode enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ elevatedLevel?: string }
|
||||
>;
|
||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe(
|
||||
"on",
|
||||
);
|
||||
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
|
||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on");
|
||||
});
|
||||
});
|
||||
it("allows elevated directive in direct chats without mentions", async () => {
|
||||
@@ -237,10 +226,7 @@ describe("trigger handling", () => {
|
||||
expect(text).toContain("Elevated mode enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ elevatedLevel?: string }
|
||||
>;
|
||||
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
|
||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -199,8 +195,7 @@ describe("trigger handling", () => {
|
||||
expect(String(blockReplies[0]?.text ?? "")).toContain("Model:");
|
||||
expect(replies.length).toBe(1);
|
||||
expect(replies[0]?.text).toBe("agent says hi");
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).not.toContain("/status");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -48,10 +47,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -124,8 +120,7 @@ describe("trigger handling", () => {
|
||||
expect(blockReplies.length).toBe(1);
|
||||
expect(blockReplies[0]?.text).toContain("Slash commands");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).not.toContain("/commands");
|
||||
expect(text).toBe("ok");
|
||||
});
|
||||
@@ -158,8 +153,7 @@ describe("trigger handling", () => {
|
||||
expect(blockReplies.length).toBe(1);
|
||||
expect(blockReplies[0]?.text).toContain("Identity");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).not.toContain("/whoami");
|
||||
expect(text).toBe("ok");
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -173,10 +169,7 @@ describe("trigger handling", () => {
|
||||
expect(text).toContain("Elevated mode enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ elevatedLevel?: string }
|
||||
>;
|
||||
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
|
||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
||||
});
|
||||
});
|
||||
@@ -215,9 +208,7 @@ describe("trigger handling", () => {
|
||||
});
|
||||
it("returns a context overflow fallback when the embedded agent throws", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(
|
||||
new Error("Context window exceeded"),
|
||||
);
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("Context window exceeded"));
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||
|
||||
@@ -101,9 +97,7 @@ afterEach(() => {
|
||||
describe("trigger handling", () => {
|
||||
it("includes the error cause when the embedded agent throws", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(
|
||||
new Error("sandbox is not defined"),
|
||||
);
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("sandbox is not defined"));
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
@@ -221,12 +215,11 @@ describe("trigger handling", () => {
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Group activation set to always");
|
||||
const store = JSON.parse(
|
||||
await fs.readFile(cfg.session.store, "utf-8"),
|
||||
) as Record<string, { groupActivation?: string }>;
|
||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe(
|
||||
"always",
|
||||
);
|
||||
const store = JSON.parse(await fs.readFile(cfg.session.store, "utf-8")) as Record<
|
||||
string,
|
||||
{ groupActivation?: string }
|
||||
>;
|
||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe("always");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -135,8 +131,7 @@ describe("trigger handling", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
// Not allowlisted: inline /status is treated as plain text and is not stripped.
|
||||
expect(prompt).toContain("/status");
|
||||
});
|
||||
@@ -178,8 +173,7 @@ describe("trigger handling", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).toContain("/help");
|
||||
});
|
||||
});
|
||||
@@ -232,10 +226,7 @@ describe("trigger handling", () => {
|
||||
expect(text).toContain("Send policy set to off");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ sendPolicy?: string }
|
||||
>;
|
||||
const store = JSON.parse(storeRaw) as Record<string, { sendPolicy?: string }>;
|
||||
expect(store[MAIN_SESSION_KEY]?.sendPolicy).toBe("deny");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { resolveSessionKey } from "../config/sessions.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
@@ -193,8 +189,7 @@ describe("trigger handling", () => {
|
||||
// stripped from the prompt; the remaining text continues through the agent.
|
||||
expect(blockReplies.length).toBe(1);
|
||||
expect(String(blockReplies[0]?.text ?? "").length).toBeGreaterThan(0);
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).not.toContain("/status");
|
||||
});
|
||||
});
|
||||
@@ -225,8 +220,7 @@ describe("trigger handling", () => {
|
||||
expect(blockReplies.length).toBe(1);
|
||||
expect(blockReplies[0]?.text).toContain("Help");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).not.toContain("/help");
|
||||
expect(text).toBe("ok");
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -102,10 +101,7 @@ afterEach(() => {
|
||||
describe("trigger handling", () => {
|
||||
it("runs /compact as a gated command", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const storePath = join(
|
||||
tmpdir(),
|
||||
`clawdbot-session-test-${Date.now()}.json`,
|
||||
);
|
||||
const storePath = join(tmpdir(), `clawdbot-session-test-${Date.now()}.json`);
|
||||
vi.mocked(compactEmbeddedPiSession).mockResolvedValue({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
@@ -182,8 +178,7 @@ describe("trigger handling", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).toContain("Give me the status");
|
||||
expect(prompt).not.toContain("/thinking high");
|
||||
expect(prompt).not.toContain("/think high");
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -135,8 +131,7 @@ describe("trigger handling", () => {
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("hello");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
expect(prompt).toContain("A new session was started via /new or /reset");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -182,10 +178,7 @@ describe("trigger handling", () => {
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(
|
||||
text?.startsWith("⚙️ Restarting") ||
|
||||
text?.startsWith("⚠️ Restart failed"),
|
||||
).toBe(true);
|
||||
expect(text?.startsWith("⚙️ Restarting") || text?.startsWith("⚠️ Restart failed")).toBe(true);
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,8 +9,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -50,10 +49,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { loadSessionStore } from "../config/sessions.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
@@ -119,12 +115,8 @@ describe("trigger handling", () => {
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
const normalized = normalizeTestText(text ?? "");
|
||||
expect(normalized).toContain(
|
||||
"Pick: /model <#> or /model <provider/model>",
|
||||
);
|
||||
expect(normalized).toContain(
|
||||
"1) claude-opus-4-5 — anthropic, openrouter",
|
||||
);
|
||||
expect(normalized).toContain("Pick: /model <#> or /model <provider/model>");
|
||||
expect(normalized).toContain("1) claude-opus-4-5 — anthropic, openrouter");
|
||||
expect(normalized).toContain("3) gpt-5.2 — openai, openai-codex");
|
||||
expect(normalized).toContain("More: /model status");
|
||||
expect(normalized).not.toContain("reasoning");
|
||||
@@ -202,9 +194,7 @@ describe("trigger handling", () => {
|
||||
|
||||
const store = loadSessionStore(cfg.session.store);
|
||||
expect(store[sessionKey]?.providerOverride).toBe("openrouter");
|
||||
expect(store[sessionKey]?.modelOverride).toBe(
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
expect(store[sessionKey]?.modelOverride).toBe("anthropic/claude-opus-4-5");
|
||||
});
|
||||
});
|
||||
it("selects a model by index via /model <#>", async () => {
|
||||
@@ -227,9 +217,7 @@ describe("trigger handling", () => {
|
||||
);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(normalizeTestText(text ?? "")).toContain(
|
||||
"Model set to openai/gpt-5.2",
|
||||
);
|
||||
expect(normalizeTestText(text ?? "")).toContain("Model set to openai/gpt-5.2");
|
||||
|
||||
const store = loadSessionStore(cfg.session.store);
|
||||
expect(store[sessionKey]?.providerOverride).toBe("openai");
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -50,15 +49,9 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js";
|
||||
import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
resolveSessionKey,
|
||||
} from "../config/sessions.js";
|
||||
import { resolveAgentIdFromSessionKey, resolveSessionKey } from "../config/sessions.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
const _MAIN_SESSION_KEY = "agent:main:main";
|
||||
@@ -104,90 +97,80 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("trigger handling", () => {
|
||||
it(
|
||||
"stages inbound media into the sandbox workspace",
|
||||
{ timeout: 15_000 },
|
||||
async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const inboundDir = join(home, ".clawdbot", "media", "inbound");
|
||||
await fs.mkdir(inboundDir, { recursive: true });
|
||||
const mediaPath = join(inboundDir, "photo.jpg");
|
||||
await fs.writeFile(mediaPath, "test");
|
||||
it("stages inbound media into the sandbox workspace", { timeout: 15_000 }, async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const inboundDir = join(home, ".clawdbot", "media", "inbound");
|
||||
await fs.mkdir(inboundDir, { recursive: true });
|
||||
const mediaPath = join(inboundDir, "photo.jpg");
|
||||
await fs.writeFile(mediaPath, "test");
|
||||
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||
},
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
sandbox: {
|
||||
mode: "non-main" as const,
|
||||
workspaceRoot: join(home, "sandboxes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: join(home, "sessions.json"),
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
Body: "hi",
|
||||
From: "group:whatsapp:demo",
|
||||
To: "+2000",
|
||||
ChatType: "group" as const,
|
||||
Provider: "whatsapp" as const,
|
||||
MediaPath: mediaPath,
|
||||
MediaType: "image/jpeg",
|
||||
MediaUrl: mediaPath,
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(ctx, {}, cfg);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
|
||||
const prompt =
|
||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const stagedPath = `media/inbound/${basename(mediaPath)}`;
|
||||
expect(prompt).toContain(stagedPath);
|
||||
expect(prompt).not.toContain(mediaPath);
|
||||
|
||||
const sessionKey = resolveSessionKey(
|
||||
cfg.session?.scope ?? "per-sender",
|
||||
ctx,
|
||||
cfg.session?.mainKey,
|
||||
);
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const sandbox = await ensureSandboxWorkspaceForSession({
|
||||
config: cfg,
|
||||
sessionKey,
|
||||
workspaceDir: resolveAgentWorkspaceDir(cfg, agentId),
|
||||
});
|
||||
expect(sandbox).not.toBeNull();
|
||||
if (!sandbox) {
|
||||
throw new Error("Expected sandbox to be set");
|
||||
}
|
||||
const stagedFullPath = join(
|
||||
sandbox.workspaceDir,
|
||||
"media",
|
||||
"inbound",
|
||||
basename(mediaPath),
|
||||
);
|
||||
await expect(fs.stat(stagedFullPath)).resolves.toBeTruthy();
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
sandbox: {
|
||||
mode: "non-main" as const,
|
||||
workspaceRoot: join(home, "sandboxes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: join(home, "sessions.json"),
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
Body: "hi",
|
||||
From: "group:whatsapp:demo",
|
||||
To: "+2000",
|
||||
ChatType: "group" as const,
|
||||
Provider: "whatsapp" as const,
|
||||
MediaPath: mediaPath,
|
||||
MediaType: "image/jpeg",
|
||||
MediaUrl: mediaPath,
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(ctx, {}, cfg);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("ok");
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
|
||||
const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||
const stagedPath = `media/inbound/${basename(mediaPath)}`;
|
||||
expect(prompt).toContain(stagedPath);
|
||||
expect(prompt).not.toContain(mediaPath);
|
||||
|
||||
const sessionKey = resolveSessionKey(
|
||||
cfg.session?.scope ?? "per-sender",
|
||||
ctx,
|
||||
cfg.session?.mainKey,
|
||||
);
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const sandbox = await ensureSandboxWorkspaceForSession({
|
||||
config: cfg,
|
||||
sessionKey,
|
||||
workspaceDir: resolveAgentWorkspaceDir(cfg, agentId),
|
||||
});
|
||||
expect(sandbox).not.toBeNull();
|
||||
if (!sandbox) {
|
||||
throw new Error("Expected sandbox to be set");
|
||||
}
|
||||
const stagedFullPath = join(sandbox.workspaceDir, "media", "inbound", basename(mediaPath));
|
||||
await expect(fs.stat(stagedFullPath)).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
||||
compactEmbeddedPiSession: vi.fn(),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) =>
|
||||
`session:${key.trim() || "main"}`,
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
@@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => modelCatalogMocks);
|
||||
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { loadSessionStore } from "../config/sessions.js";
|
||||
import { getReplyFromConfig } from "./reply.js";
|
||||
|
||||
@@ -137,9 +133,7 @@ describe("trigger handling", () => {
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("⚙️ Agent was aborted.");
|
||||
expect(vi.mocked(abortEmbeddedPiRun)).toHaveBeenCalledWith(
|
||||
targetSessionId,
|
||||
);
|
||||
expect(vi.mocked(abortEmbeddedPiRun)).toHaveBeenCalledWith(targetSessionId);
|
||||
const store = loadSessionStore(cfg.session.store);
|
||||
expect(store[targetSessionKey]?.abortedLastRun).toBe(true);
|
||||
});
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
} from "../../config/sessions.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import {
|
||||
normalizeCommandBody,
|
||||
shouldHandleTextCommands,
|
||||
} from "../commands-registry.js";
|
||||
import { normalizeCommandBody, shouldHandleTextCommands } from "../commands-registry.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
|
||||
@@ -82,9 +79,7 @@ export async function tryFastAbortFromMessage(params: {
|
||||
config: cfg,
|
||||
});
|
||||
// Use RawBody/CommandBody for abort detection (clean message without structural context).
|
||||
const raw = stripStructuralPrefixes(
|
||||
ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "",
|
||||
);
|
||||
const raw = stripStructuralPrefixes(ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "");
|
||||
const isGroup = ctx.ChatType?.trim().toLowerCase() === "group";
|
||||
const stripped = isGroup ? stripMentions(raw, ctx, cfg, agentId) : raw;
|
||||
const normalized = normalizeCommandBody(stripped);
|
||||
|
||||
@@ -17,27 +17,18 @@ import {
|
||||
saveSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import {
|
||||
emitAgentEvent,
|
||||
registerAgentRunContext,
|
||||
} from "../../infra/agent-events.js";
|
||||
import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { VerboseLevel } from "../thinking.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import {
|
||||
buildThreadingToolContext,
|
||||
resolveEnforceFinalTag,
|
||||
} from "./agent-runner-utils.js";
|
||||
import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js";
|
||||
import type { BlockReplyPipeline } from "./block-reply-pipeline.js";
|
||||
import type { FollowupRun } from "./queue.js";
|
||||
import { parseReplyDirectives } from "./reply-directives.js";
|
||||
import {
|
||||
applyReplyTagsToPayload,
|
||||
isRenderablePayload,
|
||||
} from "./reply-payloads.js";
|
||||
import { applyReplyTagsToPayload, isRenderablePayload } from "./reply-payloads.js";
|
||||
import type { TypingSignaler } from "./typing-mode.js";
|
||||
|
||||
export type AgentRunLoopResult =
|
||||
@@ -94,12 +85,9 @@ export async function runAgentTurnWithFallback(params: {
|
||||
while (true) {
|
||||
try {
|
||||
const allowPartialStream = !(
|
||||
params.followupRun.run.reasoningLevel === "stream" &&
|
||||
params.opts?.onReasoningStream
|
||||
params.followupRun.run.reasoningLevel === "stream" && params.opts?.onReasoningStream
|
||||
);
|
||||
const normalizeStreamingText = (
|
||||
payload: ReplyPayload,
|
||||
): { text?: string; skip: boolean } => {
|
||||
const normalizeStreamingText = (payload: ReplyPayload): { text?: string; skip: boolean } => {
|
||||
if (!allowPartialStream) return { skip: true };
|
||||
let text = payload.text;
|
||||
if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) {
|
||||
@@ -120,9 +108,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
}
|
||||
return { text, skip: false };
|
||||
};
|
||||
const handlePartialForTyping = async (
|
||||
payload: ReplyPayload,
|
||||
): Promise<string | undefined> => {
|
||||
const handlePartialForTyping = async (payload: ReplyPayload): Promise<string | undefined> => {
|
||||
const { text, skip } = normalizeStreamingText(payload);
|
||||
if (skip || !text) return undefined;
|
||||
await params.typingSignals.signalTextDelta(text);
|
||||
@@ -149,10 +135,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
startedAt,
|
||||
},
|
||||
});
|
||||
const cliSessionId = getCliSessionId(
|
||||
params.getActiveSessionEntry(),
|
||||
provider,
|
||||
);
|
||||
const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
|
||||
return runCliAgent({
|
||||
sessionId: params.followupRun.run.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -198,8 +181,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
return runEmbeddedPiAgent({
|
||||
sessionId: params.followupRun.run.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageProvider:
|
||||
params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
agentAccountId: params.sessionCtx.AccountId,
|
||||
// Provider threading context for tool auto-injection
|
||||
...buildThreadingToolContext({
|
||||
@@ -215,10 +197,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
prompt: params.commandBody,
|
||||
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
||||
ownerNumbers: params.followupRun.run.ownerNumbers,
|
||||
enforceFinalTag: resolveEnforceFinalTag(
|
||||
params.followupRun.run,
|
||||
provider,
|
||||
),
|
||||
enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
|
||||
provider,
|
||||
model,
|
||||
authProfileId: params.followupRun.run.authProfileId,
|
||||
@@ -233,11 +212,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
onPartialReply: allowPartialStream
|
||||
? async (payload) => {
|
||||
const textForTyping = await handlePartialForTyping(payload);
|
||||
if (
|
||||
!params.opts?.onPartialReply ||
|
||||
textForTyping === undefined
|
||||
)
|
||||
return;
|
||||
if (!params.opts?.onPartialReply || textForTyping === undefined) return;
|
||||
await params.opts.onPartialReply({
|
||||
text: textForTyping,
|
||||
mediaUrls: payload.mediaUrls,
|
||||
@@ -248,8 +223,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
await params.typingSignals.signalMessageStart();
|
||||
},
|
||||
onReasoningStream:
|
||||
params.typingSignals.shouldStartOnReasoning ||
|
||||
params.opts?.onReasoningStream
|
||||
params.typingSignals.shouldStartOnReasoning || params.opts?.onReasoningStream
|
||||
? async (payload) => {
|
||||
await params.typingSignals.signalReasoningDelta();
|
||||
await params.opts?.onReasoningStream?.({
|
||||
@@ -261,16 +235,14 @@ export async function runAgentTurnWithFallback(params: {
|
||||
onAgentEvent: (evt) => {
|
||||
// Trigger typing when tools start executing
|
||||
if (evt.stream === "tool") {
|
||||
const phase =
|
||||
typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
if (phase === "start" || phase === "update") {
|
||||
void params.typingSignals.signalToolStart();
|
||||
}
|
||||
}
|
||||
// Track auto-compaction completion
|
||||
if (evt.stream === "compaction") {
|
||||
const phase =
|
||||
typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const willRetry = Boolean(evt.data.willRetry);
|
||||
if (phase === "end" && !willRetry) {
|
||||
autoCompactionCompleted = true;
|
||||
@@ -281,8 +253,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
params.blockStreamingEnabled && params.opts?.onBlockReply
|
||||
? async (payload) => {
|
||||
const { text, skip } = normalizeStreamingText(payload);
|
||||
const hasPayloadMedia =
|
||||
(payload.mediaUrls?.length ?? 0) > 0;
|
||||
const hasPayloadMedia = (payload.mediaUrls?.length ?? 0) > 0;
|
||||
if (skip && !hasPayloadMedia) return;
|
||||
const taggedPayload = applyReplyTagsToPayload(
|
||||
{
|
||||
@@ -293,22 +264,14 @@ export async function runAgentTurnWithFallback(params: {
|
||||
params.sessionCtx.MessageSid,
|
||||
);
|
||||
// Let through payloads with audioAsVoice flag even if empty (need to track it)
|
||||
if (
|
||||
!isRenderablePayload(taggedPayload) &&
|
||||
!payload.audioAsVoice
|
||||
)
|
||||
return;
|
||||
const parsed = parseReplyDirectives(
|
||||
taggedPayload.text ?? "",
|
||||
{
|
||||
currentMessageId: params.sessionCtx.MessageSid,
|
||||
silentToken: SILENT_REPLY_TOKEN,
|
||||
},
|
||||
);
|
||||
if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice) return;
|
||||
const parsed = parseReplyDirectives(taggedPayload.text ?? "", {
|
||||
currentMessageId: params.sessionCtx.MessageSid,
|
||||
silentToken: SILENT_REPLY_TOKEN,
|
||||
});
|
||||
const cleaned = parsed.text || undefined;
|
||||
const hasRenderableMedia =
|
||||
Boolean(taggedPayload.mediaUrl) ||
|
||||
(taggedPayload.mediaUrls?.length ?? 0) > 0;
|
||||
Boolean(taggedPayload.mediaUrl) || (taggedPayload.mediaUrls?.length ?? 0) > 0;
|
||||
// Skip empty payloads unless they have audioAsVoice flag (need to track it)
|
||||
if (
|
||||
!cleaned &&
|
||||
@@ -322,21 +285,16 @@ export async function runAgentTurnWithFallback(params: {
|
||||
const blockPayload: ReplyPayload = params.applyReplyToMode({
|
||||
...taggedPayload,
|
||||
text: cleaned,
|
||||
audioAsVoice: Boolean(
|
||||
parsed.audioAsVoice || payload.audioAsVoice,
|
||||
),
|
||||
audioAsVoice: Boolean(parsed.audioAsVoice || payload.audioAsVoice),
|
||||
replyToId: taggedPayload.replyToId ?? parsed.replyToId,
|
||||
replyToTag: taggedPayload.replyToTag || parsed.replyToTag,
|
||||
replyToCurrent:
|
||||
taggedPayload.replyToCurrent || parsed.replyToCurrent,
|
||||
replyToCurrent: taggedPayload.replyToCurrent || parsed.replyToCurrent,
|
||||
});
|
||||
|
||||
void params.typingSignals
|
||||
.signalTextDelta(cleaned ?? taggedPayload.text)
|
||||
.catch((err) => {
|
||||
logVerbose(
|
||||
`block reply typing signal failed: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`block reply typing signal failed: ${String(err)}`);
|
||||
});
|
||||
|
||||
params.blockReplyPipeline?.enqueue(blockPayload);
|
||||
@@ -399,8 +357,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
isContextOverflowError(message) ||
|
||||
/context.*overflow|too large|context window/i.test(message);
|
||||
const isCompactionFailure = isCompactionFailureError(message);
|
||||
const isSessionCorruption =
|
||||
/function call turn comes immediately after/i.test(message);
|
||||
const isSessionCorruption = /function call turn comes immediately after/i.test(message);
|
||||
|
||||
if (
|
||||
isCompactionFailure &&
|
||||
@@ -426,8 +383,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
try {
|
||||
// Delete transcript file if it exists
|
||||
if (corruptedSessionId) {
|
||||
const transcriptPath =
|
||||
resolveSessionTranscriptPath(corruptedSessionId);
|
||||
const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId);
|
||||
try {
|
||||
fs.unlinkSync(transcriptPath);
|
||||
} catch {
|
||||
|
||||
@@ -9,9 +9,7 @@ const hasAudioMedia = (urls?: string[]): boolean =>
|
||||
Boolean(urls?.some((url) => isAudioFileName(url)));
|
||||
|
||||
export const isAudioPayload = (payload: ReplyPayload): boolean =>
|
||||
hasAudioMedia(
|
||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined),
|
||||
);
|
||||
hasAudioMedia(payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined));
|
||||
|
||||
export const createShouldEmitToolResult = (params: {
|
||||
sessionKey?: string;
|
||||
|
||||
@@ -3,10 +3,7 @@ import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js"
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||
import {
|
||||
resolveSandboxConfigForAgent,
|
||||
resolveSandboxRuntimeStatus,
|
||||
} from "../../agents/sandbox.js";
|
||||
import { resolveSandboxConfigForAgent, resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
@@ -18,10 +15,7 @@ import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { VerboseLevel } from "../thinking.js";
|
||||
import type { GetReplyOptions } from "../types.js";
|
||||
import {
|
||||
buildThreadingToolContext,
|
||||
resolveEnforceFinalTag,
|
||||
} from "./agent-runner-utils.js";
|
||||
import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js";
|
||||
import {
|
||||
resolveMemoryFlushContextWindowTokens,
|
||||
resolveMemoryFlushSettings,
|
||||
@@ -54,10 +48,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
if (!runtime.sandboxed) return true;
|
||||
const sandboxCfg = resolveSandboxConfigForAgent(
|
||||
params.cfg,
|
||||
runtime.agentId,
|
||||
);
|
||||
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, runtime.agentId);
|
||||
return sandboxCfg.workspaceAccess === "rw";
|
||||
})();
|
||||
|
||||
@@ -69,9 +60,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
shouldRunMemoryFlush({
|
||||
entry:
|
||||
params.sessionEntry ??
|
||||
(params.sessionKey
|
||||
? params.sessionStore?.[params.sessionKey]
|
||||
: undefined),
|
||||
(params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined),
|
||||
contextWindowTokens: resolveMemoryFlushContextWindowTokens({
|
||||
modelId: params.followupRun.run.model ?? params.defaultModel,
|
||||
agentCfgContextTokens: params.agentCfgContextTokens,
|
||||
@@ -111,8 +100,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
runEmbeddedPiAgent({
|
||||
sessionId: params.followupRun.run.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageProvider:
|
||||
params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||
agentAccountId: params.sessionCtx.AccountId,
|
||||
// Provider threading context for tool auto-injection
|
||||
...buildThreadingToolContext({
|
||||
@@ -128,10 +116,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
prompt: memoryFlushSettings.prompt,
|
||||
extraSystemPrompt: flushSystemPrompt,
|
||||
ownerNumbers: params.followupRun.run.ownerNumbers,
|
||||
enforceFinalTag: resolveEnforceFinalTag(
|
||||
params.followupRun.run,
|
||||
provider,
|
||||
),
|
||||
enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
|
||||
provider,
|
||||
model,
|
||||
authProfileId: params.followupRun.run.authProfileId,
|
||||
@@ -143,8 +128,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
runId: flushRunId,
|
||||
onAgentEvent: (evt) => {
|
||||
if (evt.stream === "compaction") {
|
||||
const phase =
|
||||
typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const willRetry = Boolean(evt.data.willRetry);
|
||||
if (phase === "end" && !willRetry) {
|
||||
memoryCompactionCompleted = true;
|
||||
@@ -155,9 +139,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
});
|
||||
let memoryFlushCompactionCount =
|
||||
activeSessionEntry?.compactionCount ??
|
||||
(params.sessionKey
|
||||
? activeSessionStore?.[params.sessionKey]?.compactionCount
|
||||
: 0) ??
|
||||
(params.sessionKey ? activeSessionStore?.[params.sessionKey]?.compactionCount : 0) ??
|
||||
0;
|
||||
if (memoryCompactionCompleted) {
|
||||
const nextCount = await incrementCompactionCount({
|
||||
|
||||
@@ -4,10 +4,7 @@ import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import type { OriginatingChannelType } from "../templating.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import {
|
||||
formatBunFetchSocketError,
|
||||
isBunFetchSocketError,
|
||||
} from "./agent-runner-utils.js";
|
||||
import { formatBunFetchSocketError, isBunFetchSocketError } from "./agent-runner-utils.js";
|
||||
import type { BlockReplyPipeline } from "./block-reply-pipeline.js";
|
||||
import { parseReplyDirectives } from "./reply-directives.js";
|
||||
import {
|
||||
@@ -52,8 +49,7 @@ export function buildReplyPayloads(params: {
|
||||
didLogHeartbeatStrip = true;
|
||||
logVerbose("Stripped stray HEARTBEAT_OK token from reply");
|
||||
}
|
||||
const hasMedia =
|
||||
Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
if (stripped.shouldSkip && !hasMedia) return [];
|
||||
return [{ ...payload, text: stripped.text }];
|
||||
});
|
||||
@@ -105,9 +101,7 @@ export function buildReplyPayloads(params: {
|
||||
const filteredPayloads = shouldDropFinalPayloads
|
||||
? []
|
||||
: params.blockStreamingEnabled
|
||||
? dedupedPayloads.filter(
|
||||
(payload) => !params.blockReplyPipeline?.hasSentPayload(payload),
|
||||
)
|
||||
? dedupedPayloads.filter((payload) => !params.blockReplyPipeline?.hasSentPayload(payload))
|
||||
: dedupedPayloads;
|
||||
const replyPayloads = suppressMessagingToolReplies ? [] : filteredPayloads;
|
||||
|
||||
|
||||
@@ -4,11 +4,7 @@ import type { ChannelThreadingToolContext } from "../../channels/plugins/types.j
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
||||
import {
|
||||
estimateUsageCost,
|
||||
formatTokenCount,
|
||||
formatUsd,
|
||||
} from "../../utils/usage-format.js";
|
||||
import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import type { FollowupRun } from "./queue.js";
|
||||
@@ -73,8 +69,7 @@ export const formatResponseUsageLine = (params: {
|
||||
const output = usage.output;
|
||||
if (typeof input !== "number" && typeof output !== "number") return null;
|
||||
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
||||
const outputLabel =
|
||||
typeof output === "number" ? formatTokenCount(output) : "?";
|
||||
const outputLabel = typeof output === "number" ? formatTokenCount(output) : "?";
|
||||
const cost =
|
||||
params.showCost && typeof input === "number" && typeof output === "number"
|
||||
? estimateUsageCost({
|
||||
@@ -92,10 +87,7 @@ export const formatResponseUsageLine = (params: {
|
||||
return `Usage: ${inputLabel} in / ${outputLabel} out${suffix}`;
|
||||
};
|
||||
|
||||
export const appendUsageLine = (
|
||||
payloads: ReplyPayload[],
|
||||
line: string,
|
||||
): ReplyPayload[] => {
|
||||
export const appendUsageLine = (payloads: ReplyPayload[], line: string): ReplyPayload[] => {
|
||||
let index = -1;
|
||||
for (let i = payloads.length - 1; i >= 0; i -= 1) {
|
||||
if (payloads[i]?.text) {
|
||||
@@ -116,7 +108,5 @@ export const appendUsageLine = (
|
||||
return updated;
|
||||
};
|
||||
|
||||
export const resolveEnforceFinalTag = (
|
||||
run: FollowupRun["run"],
|
||||
provider: string,
|
||||
) => Boolean(run.enforceFinalTag || isReasoningTagProvider(provider));
|
||||
export const resolveEnforceFinalTag = (run: FollowupRun["run"], provider: string) =>
|
||||
Boolean(run.enforceFinalTag || isReasoningTagProvider(provider));
|
||||
|
||||
@@ -28,8 +28,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -43,9 +42,7 @@ describe("runReplyAgent block streaming", () => {
|
||||
it("coalesces duplicate text_end block replies", async () => {
|
||||
const onBlockReply = vi.fn();
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params) => {
|
||||
const block = params.onBlockReply as
|
||||
| ((payload: { text?: string }) => void)
|
||||
| undefined;
|
||||
const block = params.onBlockReply as ((payload: { text?: string }) => void) | undefined;
|
||||
block?.({ text: "Hello" });
|
||||
block?.({ text: "Hello" });
|
||||
return {
|
||||
|
||||
@@ -34,8 +34,7 @@ vi.mock("../../agents/cli-runner.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
|
||||
@@ -34,8 +34,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -124,9 +123,7 @@ function createMinimalRun(params?: {
|
||||
describe("runReplyAgent typing (heartbeat)", () => {
|
||||
it("resets corrupted Gemini sessions and deletes transcripts", async () => {
|
||||
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const stateDir = await fs.mkdtemp(
|
||||
path.join(tmpdir(), "clawdbot-session-reset-"),
|
||||
);
|
||||
const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-reset-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
||||
try {
|
||||
const sessionId = "session-corrupt";
|
||||
@@ -173,9 +170,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
});
|
||||
it("keeps sessions intact on other errors", async () => {
|
||||
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const stateDir = await fs.mkdtemp(
|
||||
path.join(tmpdir(), "clawdbot-session-noreset-"),
|
||||
);
|
||||
const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-noreset-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
||||
try {
|
||||
const sessionId = "session-ok";
|
||||
|
||||
@@ -33,8 +33,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -123,9 +122,7 @@ function createMinimalRun(params?: {
|
||||
describe("runReplyAgent typing (heartbeat)", () => {
|
||||
it("retries after compaction failure by resetting the session", async () => {
|
||||
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const stateDir = await fs.mkdtemp(
|
||||
path.join(tmpdir(), "clawdbot-session-compaction-reset-"),
|
||||
);
|
||||
const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-compaction-reset-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
||||
try {
|
||||
const sessionId = "session";
|
||||
@@ -173,9 +170,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
});
|
||||
it("retries after context overflow payload by resetting the session", async () => {
|
||||
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const stateDir = await fs.mkdtemp(
|
||||
path.join(tmpdir(), "clawdbot-session-overflow-reset-"),
|
||||
);
|
||||
const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-overflow-reset-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
||||
try {
|
||||
const sessionId = "session";
|
||||
@@ -188,9 +183,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
|
||||
runEmbeddedPiAgentMock
|
||||
.mockImplementationOnce(async () => ({
|
||||
payloads: [
|
||||
{ text: "Context overflow: prompt too large", isError: true },
|
||||
],
|
||||
payloads: [{ text: "Context overflow: prompt too large", isError: true }],
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
error: {
|
||||
|
||||
@@ -33,8 +33,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -123,12 +122,10 @@ function createMinimalRun(params?: {
|
||||
describe("runReplyAgent typing (heartbeat)", () => {
|
||||
it("signals typing on block replies", async () => {
|
||||
const onBlockReply = vi.fn();
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onBlockReply?.({ text: "chunk", mediaUrls: [] });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onBlockReply?.({ text: "chunk", mediaUrls: [] });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
typingMode: "message",
|
||||
@@ -148,12 +145,10 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
});
|
||||
it("signals typing on tool results", async () => {
|
||||
const onToolResult = vi.fn();
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onToolResult?.({ text: "tooling", mediaUrls: [] });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onToolResult?.({ text: "tooling", mediaUrls: [] });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
typingMode: "message",
|
||||
@@ -169,12 +164,10 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
});
|
||||
it("skips typing for silent tool results", async () => {
|
||||
const onToolResult = vi.fn();
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onToolResult?.({ text: "NO_REPLY", mediaUrls: [] });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onToolResult?.({ text: "NO_REPLY", mediaUrls: [] });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
typingMode: "message",
|
||||
@@ -195,10 +188,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: {
|
||||
onAgentEvent?: (evt: {
|
||||
stream: string;
|
||||
data: Record<string, unknown>;
|
||||
}) => void;
|
||||
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
|
||||
}) => {
|
||||
params.onAgentEvent?.({
|
||||
stream: "compaction",
|
||||
|
||||
@@ -30,8 +30,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -120,12 +119,10 @@ function createMinimalRun(params?: {
|
||||
describe("runReplyAgent typing (heartbeat)", () => {
|
||||
it("signals typing for normal runs", async () => {
|
||||
const onPartialReply = vi.fn();
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "hi" });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "hi" });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
opts: { isHeartbeat: false, onPartialReply },
|
||||
@@ -137,12 +134,10 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
expect(typing.startTypingLoop).toHaveBeenCalled();
|
||||
});
|
||||
it("signals typing even without consumer partial handler", async () => {
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "hi" });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "hi" });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
typingMode: "message",
|
||||
@@ -154,12 +149,10 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
});
|
||||
it("never signals typing for heartbeat runs", async () => {
|
||||
const onPartialReply = vi.fn();
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "hi" });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "hi" });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
opts: { isHeartbeat: true, onPartialReply },
|
||||
@@ -172,12 +165,10 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
});
|
||||
it("suppresses partial streaming for NO_REPLY", async () => {
|
||||
const onPartialReply = vi.fn();
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "NO_REPLY" });
|
||||
return { payloads: [{ text: "NO_REPLY" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onPartialReply?.({ text: "NO_REPLY" });
|
||||
return { payloads: [{ text: "NO_REPLY" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
opts: { isHeartbeat: false, onPartialReply },
|
||||
@@ -189,12 +180,10 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
||||
});
|
||||
it("starts typing on assistant message start in message mode", async () => {
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onAssistantMessageStart?.();
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => {
|
||||
await params.onAssistantMessageStart?.();
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
typingMode: "message",
|
||||
@@ -208,9 +197,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: {
|
||||
onPartialReply?: (payload: { text?: string }) => Promise<void> | void;
|
||||
onReasoningStream?: (payload: {
|
||||
text?: string;
|
||||
}) => Promise<void> | void;
|
||||
onReasoningStream?: (payload: { text?: string }) => Promise<void> | void;
|
||||
}) => {
|
||||
await params.onReasoningStream?.({ text: "Reasoning:\n_step_" });
|
||||
await params.onPartialReply?.({ text: "hi" });
|
||||
@@ -228,9 +215,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
});
|
||||
it("suppresses typing in never mode", async () => {
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: {
|
||||
onPartialReply?: (payload: { text?: string }) => void;
|
||||
}) => {
|
||||
async (params: { onPartialReply?: (payload: { text?: string }) => void }) => {
|
||||
params.onPartialReply?.({ text: "hi" });
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
|
||||
@@ -34,8 +34,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -124,13 +123,9 @@ function createMinimalRun(params?: {
|
||||
describe("runReplyAgent typing (heartbeat)", () => {
|
||||
it("still replies even if session reset fails to persist", async () => {
|
||||
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const stateDir = await fs.mkdtemp(
|
||||
path.join(tmpdir(), "clawdbot-session-reset-fail-"),
|
||||
);
|
||||
const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-reset-fail-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
||||
const saveSpy = vi
|
||||
.spyOn(sessions, "saveSessionStore")
|
||||
.mockRejectedValueOnce(new Error("boom"));
|
||||
const saveSpy = vi.spyOn(sessions, "saveSessionStore").mockRejectedValueOnce(new Error("boom"));
|
||||
try {
|
||||
const sessionId = "session-corrupt";
|
||||
const storePath = path.join(stateDir, "sessions", "sessions.json");
|
||||
@@ -185,9 +180,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
const payloads = Array.isArray(res) ? res : res ? [res] : [];
|
||||
expect(payloads.length).toBe(1);
|
||||
expect(payloads[0]?.text).toContain("LLM connection failed");
|
||||
expect(payloads[0]?.text).toContain(
|
||||
"socket connection was closed unexpectedly",
|
||||
);
|
||||
expect(payloads[0]?.text).toContain("socket connection was closed unexpectedly");
|
||||
expect(payloads[0]?.text).toContain("```");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,10 +13,7 @@ const runCliAgentMock = vi.fn();
|
||||
type EmbeddedRunParams = {
|
||||
prompt?: string;
|
||||
extraSystemPrompt?: string;
|
||||
onAgentEvent?: (evt: {
|
||||
stream?: string;
|
||||
data?: { phase?: string; willRetry?: boolean };
|
||||
}) => void;
|
||||
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
||||
};
|
||||
|
||||
vi.mock("../../agents/model-fallback.js", () => ({
|
||||
@@ -45,8 +42,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -140,21 +136,19 @@ describe("runReplyAgent memory flush", () => {
|
||||
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedRunParams) => {
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
params.onAgentEvent?.({
|
||||
stream: "compaction",
|
||||
data: { phase: "end", willRetry: false },
|
||||
});
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
params.onAgentEvent?.({
|
||||
stream: "compaction",
|
||||
data: { phase: "end", willRetry: false },
|
||||
});
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
|
||||
storePath,
|
||||
|
||||
@@ -13,10 +13,7 @@ const runCliAgentMock = vi.fn();
|
||||
type EmbeddedRunParams = {
|
||||
prompt?: string;
|
||||
extraSystemPrompt?: string;
|
||||
onAgentEvent?: (evt: {
|
||||
stream?: string;
|
||||
data?: { phase?: string; willRetry?: boolean };
|
||||
}) => void;
|
||||
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
||||
};
|
||||
|
||||
vi.mock("../../agents/model-fallback.js", () => ({
|
||||
@@ -45,8 +42,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -141,18 +137,16 @@ describe("runReplyAgent memory flush", () => {
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
const calls: Array<{ prompt?: string }> = [];
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
|
||||
storePath,
|
||||
@@ -184,10 +178,7 @@ describe("runReplyAgent memory flush", () => {
|
||||
typingMode: "instant",
|
||||
});
|
||||
|
||||
expect(calls.map((call) => call.prompt)).toEqual([
|
||||
DEFAULT_MEMORY_FLUSH_PROMPT,
|
||||
"hello",
|
||||
]);
|
||||
expect(calls.map((call) => call.prompt)).toEqual([DEFAULT_MEMORY_FLUSH_PROMPT, "hello"]);
|
||||
|
||||
const stored = JSON.parse(await fs.readFile(storePath, "utf-8"));
|
||||
expect(stored[sessionKey].memoryFlushAt).toBeTypeOf("number");
|
||||
@@ -207,12 +198,10 @@ describe("runReplyAgent memory flush", () => {
|
||||
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (_params: EmbeddedRunParams) => ({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
}),
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (_params: EmbeddedRunParams) => ({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
}));
|
||||
|
||||
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
|
||||
storePath,
|
||||
@@ -250,9 +239,7 @@ describe("runReplyAgent memory flush", () => {
|
||||
});
|
||||
|
||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1);
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as
|
||||
| { prompt?: string }
|
||||
| undefined;
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as { prompt?: string } | undefined;
|
||||
expect(call?.prompt).toBe("hello");
|
||||
|
||||
const stored = JSON.parse(await fs.readFile(storePath, "utf-8"));
|
||||
|
||||
@@ -12,10 +12,7 @@ const runCliAgentMock = vi.fn();
|
||||
type EmbeddedRunParams = {
|
||||
prompt?: string;
|
||||
extraSystemPrompt?: string;
|
||||
onAgentEvent?: (evt: {
|
||||
stream?: string;
|
||||
data?: { phase?: string; willRetry?: boolean };
|
||||
}) => void;
|
||||
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
||||
};
|
||||
|
||||
vi.mock("../../agents/model-fallback.js", () => ({
|
||||
@@ -44,8 +41,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -141,15 +137,13 @@ describe("runReplyAgent memory flush", () => {
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
const calls: Array<{ prompt?: string }> = [];
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
runCliAgentMock.mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
@@ -187,9 +181,7 @@ describe("runReplyAgent memory flush", () => {
|
||||
});
|
||||
|
||||
expect(runCliAgentMock).toHaveBeenCalledTimes(1);
|
||||
const call = runCliAgentMock.mock.calls[0]?.[0] as
|
||||
| { prompt?: string }
|
||||
| undefined;
|
||||
const call = runCliAgentMock.mock.calls[0]?.[0] as { prompt?: string } | undefined;
|
||||
expect(call?.prompt).toBe("hello");
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -12,10 +12,7 @@ const runCliAgentMock = vi.fn();
|
||||
type EmbeddedRunParams = {
|
||||
prompt?: string;
|
||||
extraSystemPrompt?: string;
|
||||
onAgentEvent?: (evt: {
|
||||
stream?: string;
|
||||
data?: { phase?: string; willRetry?: boolean };
|
||||
}) => void;
|
||||
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
||||
};
|
||||
|
||||
vi.mock("../../agents/model-fallback.js", () => ({
|
||||
@@ -44,8 +41,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -140,15 +136,13 @@ describe("runReplyAgent memory flush", () => {
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
const calls: Array<{ prompt?: string }> = [];
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
|
||||
storePath,
|
||||
@@ -207,15 +201,13 @@ describe("runReplyAgent memory flush", () => {
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
const calls: Array<{ prompt?: string }> = [];
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
|
||||
storePath,
|
||||
|
||||
@@ -13,10 +13,7 @@ const runCliAgentMock = vi.fn();
|
||||
type EmbeddedRunParams = {
|
||||
prompt?: string;
|
||||
extraSystemPrompt?: string;
|
||||
onAgentEvent?: (evt: {
|
||||
stream?: string;
|
||||
data?: { phase?: string; willRetry?: boolean };
|
||||
}) => void;
|
||||
onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void;
|
||||
};
|
||||
|
||||
vi.mock("../../agents/model-fallback.js", () => ({
|
||||
@@ -45,8 +42,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -141,18 +137,16 @@ describe("runReplyAgent memory flush", () => {
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
const calls: Array<EmbeddedRunParams> = [];
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedRunParams) => {
|
||||
calls.push(params);
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
calls.push(params);
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
|
||||
storePath,
|
||||
@@ -221,15 +215,13 @@ describe("runReplyAgent memory flush", () => {
|
||||
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
|
||||
|
||||
const calls: Array<{ prompt?: string }> = [];
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
|
||||
calls.push({ prompt: params.prompt });
|
||||
return {
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { agentMeta: { usage: { input: 1, output: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
|
||||
storePath,
|
||||
|
||||
@@ -28,8 +28,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -101,9 +100,7 @@ describe("runReplyAgent messaging tool suppression", () => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||
payloads: [{ text: "hello world!" }],
|
||||
messagingToolSentTexts: ["different message"],
|
||||
messagingToolSentTargets: [
|
||||
{ tool: "slack", provider: "slack", to: "channel:C1" },
|
||||
],
|
||||
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
|
||||
meta: {},
|
||||
});
|
||||
|
||||
@@ -116,9 +113,7 @@ describe("runReplyAgent messaging tool suppression", () => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||
payloads: [{ text: "hello world!" }],
|
||||
messagingToolSentTexts: ["different message"],
|
||||
messagingToolSentTargets: [
|
||||
{ tool: "discord", provider: "discord", to: "channel:C1" },
|
||||
],
|
||||
messagingToolSentTargets: [{ tool: "discord", provider: "discord", to: "channel:C1" }],
|
||||
meta: {},
|
||||
});
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./queue.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
|
||||
return {
|
||||
...actual,
|
||||
enqueueFollowupRun: vi.fn(),
|
||||
@@ -118,11 +117,7 @@ describe("runReplyAgent fallback reasoning tags", () => {
|
||||
meta: {},
|
||||
});
|
||||
runWithModelFallbackMock.mockImplementationOnce(
|
||||
async ({
|
||||
run,
|
||||
}: {
|
||||
run: (provider: string, model: string) => Promise<unknown>;
|
||||
}) => ({
|
||||
async ({ run }: { run: (provider: string, model: string) => Promise<unknown> }) => ({
|
||||
result: await run("google-antigravity", "gemini-3"),
|
||||
provider: "google-antigravity",
|
||||
model: "gemini-3",
|
||||
@@ -131,27 +126,19 @@ describe("runReplyAgent fallback reasoning tags", () => {
|
||||
|
||||
await createRun();
|
||||
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as
|
||||
| EmbeddedPiAgentParams
|
||||
| undefined;
|
||||
const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as EmbeddedPiAgentParams | undefined;
|
||||
expect(call?.enforceFinalTag).toBe(true);
|
||||
});
|
||||
|
||||
it("enforces <final> during memory flush on fallback providers", async () => {
|
||||
runEmbeddedPiAgentMock.mockImplementation(
|
||||
async (params: EmbeddedPiAgentParams) => {
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return { payloads: [{ text: "ok" }], meta: {} };
|
||||
},
|
||||
);
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedPiAgentParams) => {
|
||||
if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) {
|
||||
return { payloads: [], meta: {} };
|
||||
}
|
||||
return { payloads: [{ text: "ok" }], meta: {} };
|
||||
});
|
||||
runWithModelFallbackMock.mockImplementation(
|
||||
async ({
|
||||
run,
|
||||
}: {
|
||||
run: (provider: string, model: string) => Promise<unknown>;
|
||||
}) => ({
|
||||
async ({ run }: { run: (provider: string, model: string) => Promise<unknown> }) => ({
|
||||
result: await run("google-antigravity", "gemini-3"),
|
||||
provider: "google-antigravity",
|
||||
model: "gemini-3",
|
||||
@@ -169,8 +156,7 @@ describe("runReplyAgent fallback reasoning tags", () => {
|
||||
|
||||
const flushCall = runEmbeddedPiAgentMock.mock.calls.find(
|
||||
([params]) =>
|
||||
(params as EmbeddedPiAgentParams | undefined)?.prompt ===
|
||||
DEFAULT_MEMORY_FLUSH_PROMPT,
|
||||
(params as EmbeddedPiAgentParams | undefined)?.prompt === DEFAULT_MEMORY_FLUSH_PROMPT,
|
||||
)?.[0] as EmbeddedPiAgentParams | undefined;
|
||||
|
||||
expect(flushCall?.enforceFinalTag).toBe(true);
|
||||
|
||||
@@ -29,25 +29,12 @@ import {
|
||||
} from "./agent-runner-helpers.js";
|
||||
import { runMemoryFlushIfNeeded } from "./agent-runner-memory.js";
|
||||
import { buildReplyPayloads } from "./agent-runner-payloads.js";
|
||||
import {
|
||||
appendUsageLine,
|
||||
formatResponseUsageLine,
|
||||
} from "./agent-runner-utils.js";
|
||||
import {
|
||||
createAudioAsVoiceBuffer,
|
||||
createBlockReplyPipeline,
|
||||
} from "./block-reply-pipeline.js";
|
||||
import { appendUsageLine, formatResponseUsageLine } from "./agent-runner-utils.js";
|
||||
import { createAudioAsVoiceBuffer, createBlockReplyPipeline } from "./block-reply-pipeline.js";
|
||||
import { resolveBlockStreamingCoalescing } from "./block-streaming.js";
|
||||
import { createFollowupRunner } from "./followup-runner.js";
|
||||
import {
|
||||
enqueueFollowupRun,
|
||||
type FollowupRun,
|
||||
type QueueSettings,
|
||||
} from "./queue.js";
|
||||
import {
|
||||
createReplyToModeFilterForChannel,
|
||||
resolveReplyToMode,
|
||||
} from "./reply-threading.js";
|
||||
import { enqueueFollowupRun, type FollowupRun, type QueueSettings } from "./queue.js";
|
||||
import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js";
|
||||
import { incrementCompactionCount } from "./session-updates.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
import { createTypingSignaler } from "./typing-mode.js";
|
||||
@@ -129,8 +116,7 @@ export async function runReplyAgent(params: {
|
||||
});
|
||||
|
||||
const pendingToolTasks = new Set<Promise<void>>();
|
||||
const blockReplyTimeoutMs =
|
||||
opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS;
|
||||
const blockReplyTimeoutMs = opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS;
|
||||
|
||||
const replyToChannel =
|
||||
sessionCtx.OriginatingChannel ??
|
||||
@@ -142,10 +128,7 @@ export async function runReplyAgent(params: {
|
||||
replyToChannel,
|
||||
sessionCtx.AccountId,
|
||||
);
|
||||
const applyReplyToMode = createReplyToModeFilterForChannel(
|
||||
replyToMode,
|
||||
replyToChannel,
|
||||
);
|
||||
const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel);
|
||||
const cfg = followupRun.run.config;
|
||||
const blockReplyCoalescing =
|
||||
blockStreamingEnabled && opts?.onBlockReply
|
||||
@@ -167,10 +150,7 @@ export async function runReplyAgent(params: {
|
||||
: null;
|
||||
|
||||
if (shouldSteer && isStreaming) {
|
||||
const steered = queueEmbeddedPiMessage(
|
||||
followupRun.run.sessionId,
|
||||
followupRun.prompt,
|
||||
);
|
||||
const steered = queueEmbeddedPiMessage(followupRun.run.sessionId, followupRun.prompt);
|
||||
if (steered && !shouldFollowup) {
|
||||
if (activeSessionEntry && activeSessionStore && sessionKey) {
|
||||
activeSessionEntry.updatedAt = Date.now();
|
||||
@@ -225,9 +205,7 @@ export async function runReplyAgent(params: {
|
||||
});
|
||||
|
||||
let responseUsageLine: string | undefined;
|
||||
const resetSessionAfterCompactionFailure = async (
|
||||
reason: string,
|
||||
): Promise<boolean> => {
|
||||
const resetSessionAfterCompactionFailure = async (reason: string): Promise<boolean> => {
|
||||
if (!sessionKey || !activeSessionStore || !storePath) return false;
|
||||
const nextSessionId = crypto.randomUUID();
|
||||
const nextEntry: SessionEntry = {
|
||||
@@ -239,14 +217,8 @@ export async function runReplyAgent(params: {
|
||||
};
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const topicId =
|
||||
typeof sessionCtx.MessageThreadId === "number"
|
||||
? sessionCtx.MessageThreadId
|
||||
: undefined;
|
||||
const nextSessionFile = resolveSessionTranscriptPath(
|
||||
nextSessionId,
|
||||
agentId,
|
||||
topicId,
|
||||
);
|
||||
typeof sessionCtx.MessageThreadId === "number" ? sessionCtx.MessageThreadId : undefined;
|
||||
const nextSessionFile = resolveSessionTranscriptPath(nextSessionId, agentId, topicId);
|
||||
nextEntry.sessionFile = nextSessionFile;
|
||||
activeSessionStore[sessionKey] = nextEntry;
|
||||
try {
|
||||
@@ -289,11 +261,7 @@ export async function runReplyAgent(params: {
|
||||
});
|
||||
|
||||
if (runOutcome.kind === "final") {
|
||||
return finalizeWithFollowup(
|
||||
runOutcome.payload,
|
||||
queueKey,
|
||||
runFollowupTurn,
|
||||
);
|
||||
return finalizeWithFollowup(runOutcome.payload, queueKey, runFollowupTurn);
|
||||
}
|
||||
|
||||
const { runResult, fallbackProvider, fallbackModel } = runOutcome;
|
||||
@@ -354,12 +322,9 @@ export async function runReplyAgent(params: {
|
||||
await signalTypingIfNeeded(replyPayloads, typingSignals);
|
||||
|
||||
const usage = runResult.meta.agentMeta?.usage;
|
||||
const modelUsed =
|
||||
runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||
const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||
const providerUsed =
|
||||
runResult.meta.agentMeta?.provider ??
|
||||
fallbackProvider ??
|
||||
followupRun.run.provider;
|
||||
runResult.meta.agentMeta?.provider ?? fallbackProvider ?? followupRun.run.provider;
|
||||
const cliSessionId = isCliProvider(providerUsed, cfg)
|
||||
? runResult.meta.agentMeta?.sessionId?.trim()
|
||||
: undefined;
|
||||
@@ -378,13 +343,11 @@ export async function runReplyAgent(params: {
|
||||
update: async (entry) => {
|
||||
const input = usage.input ?? 0;
|
||||
const output = usage.output ?? 0;
|
||||
const promptTokens =
|
||||
input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
const promptTokens = input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
const patch: Partial<SessionEntry> = {
|
||||
inputTokens: input,
|
||||
outputTokens: output,
|
||||
totalTokens:
|
||||
promptTokens > 0 ? promptTokens : (usage.total ?? input),
|
||||
totalTokens: promptTokens > 0 ? promptTokens : (usage.total ?? input),
|
||||
modelProvider: providerUsed,
|
||||
model: modelUsed,
|
||||
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
||||
@@ -437,9 +400,7 @@ export async function runReplyAgent(params: {
|
||||
|
||||
const responseUsageEnabled =
|
||||
(activeSessionEntry?.responseUsage ??
|
||||
(sessionKey
|
||||
? activeSessionStore?.[sessionKey]?.responseUsage
|
||||
: undefined)) === "on";
|
||||
(sessionKey ? activeSessionStore?.[sessionKey]?.responseUsage : undefined)) === "on";
|
||||
if (responseUsageEnabled && hasNonzeroUsage(usage)) {
|
||||
const authMode = resolveModelAuthMode(providerUsed, cfg);
|
||||
const showCost = authMode === "api-key";
|
||||
@@ -469,17 +430,11 @@ export async function runReplyAgent(params: {
|
||||
});
|
||||
if (resolvedVerboseLevel === "on") {
|
||||
const suffix = typeof count === "number" ? ` (count ${count})` : "";
|
||||
finalPayloads = [
|
||||
{ text: `🧹 Auto-compaction complete${suffix}.` },
|
||||
...finalPayloads,
|
||||
];
|
||||
finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads];
|
||||
}
|
||||
}
|
||||
if (resolvedVerboseLevel === "on" && activeIsNewSession) {
|
||||
finalPayloads = [
|
||||
{ text: `🧭 New session: ${followupRun.run.sessionId}` },
|
||||
...finalPayloads,
|
||||
];
|
||||
finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads];
|
||||
}
|
||||
if (responseUsageLine) {
|
||||
finalPayloads = appendUsageLine(finalPayloads, responseUsageLine);
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import {
|
||||
getFinishedSession,
|
||||
getSession,
|
||||
markExited,
|
||||
} from "../../agents/bash-process-registry.js";
|
||||
import { getFinishedSession, getSession, markExited } from "../../agents/bash-process-registry.js";
|
||||
import { createExecTool } from "../../agents/bash-tools.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import { killProcessTree } from "../../agents/shell-utils.js";
|
||||
@@ -41,8 +37,7 @@ function clampNumber(value: number, min: number, max: number) {
|
||||
|
||||
function resolveForegroundMs(cfg: ClawdbotConfig): number {
|
||||
const raw = cfg.commands?.bashForegroundMs;
|
||||
if (typeof raw !== "number" || Number.isNaN(raw))
|
||||
return DEFAULT_FOREGROUND_MS;
|
||||
if (typeof raw !== "number" || Number.isNaN(raw)) return DEFAULT_FOREGROUND_MS;
|
||||
return clampNumber(Math.floor(raw), 0, MAX_FOREGROUND_MS);
|
||||
}
|
||||
|
||||
@@ -98,8 +93,7 @@ function resolveRawCommandBody(params: {
|
||||
agentId?: string;
|
||||
isGroup: boolean;
|
||||
}) {
|
||||
const source =
|
||||
params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body ?? "";
|
||||
const source = params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body ?? "";
|
||||
const stripped = stripStructuralPrefixes(source);
|
||||
return params.isGroup
|
||||
? stripMentions(stripped, params.ctx, params.cfg, params.agentId)
|
||||
@@ -110,8 +104,7 @@ function getScopedSession(sessionId: string) {
|
||||
const running = getSession(sessionId);
|
||||
if (running && running.scopeKey === CHAT_BASH_SCOPE_KEY) return { running };
|
||||
const finished = getFinishedSession(sessionId);
|
||||
if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY)
|
||||
return { finished };
|
||||
if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY) return { finished };
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -165,11 +158,7 @@ function formatElevatedUnavailableMessage(params: {
|
||||
`elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`,
|
||||
);
|
||||
if (params.failures.length > 0) {
|
||||
lines.push(
|
||||
`Failing gates: ${params.failures
|
||||
.map((f) => `${f.gate} (${f.key})`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
lines.push(`Failing gates: ${params.failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`);
|
||||
} else {
|
||||
lines.push(
|
||||
"Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>).",
|
||||
@@ -244,18 +233,14 @@ export async function handleBashChatCommand(params: {
|
||||
|
||||
if (request.action === "poll") {
|
||||
const sessionId =
|
||||
request.sessionId?.trim() ||
|
||||
(liveJob?.state === "running" ? liveJob.sessionId : "");
|
||||
request.sessionId?.trim() || (liveJob?.state === "running" ? liveJob.sessionId : "");
|
||||
if (!sessionId) {
|
||||
return { text: "⚙️ No active bash job." };
|
||||
}
|
||||
const { running, finished } = getScopedSession(sessionId);
|
||||
if (running) {
|
||||
attachActiveWatcher(sessionId);
|
||||
const runtimeSec = Math.max(
|
||||
0,
|
||||
Math.floor((Date.now() - running.startedAt) / 1000),
|
||||
);
|
||||
const runtimeSec = Math.max(0, Math.floor((Date.now() - running.startedAt) / 1000));
|
||||
const tail = running.tail || "(no output yet)";
|
||||
return {
|
||||
text: [
|
||||
@@ -291,8 +276,7 @@ export async function handleBashChatCommand(params: {
|
||||
|
||||
if (request.action === "stop") {
|
||||
const sessionId =
|
||||
request.sessionId?.trim() ||
|
||||
(liveJob?.state === "running" ? liveJob.sessionId : "");
|
||||
request.sessionId?.trim() || (liveJob?.state === "running" ? liveJob.sessionId : "");
|
||||
if (!sessionId) {
|
||||
return { text: "⚙️ No active bash job." };
|
||||
}
|
||||
@@ -326,9 +310,7 @@ export async function handleBashChatCommand(params: {
|
||||
// request.action === "run"
|
||||
if (liveJob) {
|
||||
const label =
|
||||
liveJob.state === "running"
|
||||
? formatSessionSnippet(liveJob.sessionId)
|
||||
: "starting";
|
||||
liveJob.state === "running" ? formatSessionSnippet(liveJob.sessionId) : "starting";
|
||||
return {
|
||||
text: `⚠️ A bash job is already running (${label}). Use !poll / !stop (or /bash poll / /bash stop).`,
|
||||
};
|
||||
@@ -346,8 +328,7 @@ export async function handleBashChatCommand(params: {
|
||||
try {
|
||||
const foregroundMs = resolveForegroundMs(params.cfg);
|
||||
const shouldBackgroundImmediately = foregroundMs <= 0;
|
||||
const timeoutSec =
|
||||
params.cfg.tools?.exec?.timeoutSec ?? params.cfg.tools?.bash?.timeoutSec;
|
||||
const timeoutSec = params.cfg.tools?.exec?.timeoutSec ?? params.cfg.tools?.bash?.timeoutSec;
|
||||
const execTool = createExecTool({
|
||||
scopeKey: CHAT_BASH_SCOPE_KEY,
|
||||
allowBackground: true,
|
||||
@@ -385,14 +366,11 @@ export async function handleBashChatCommand(params: {
|
||||
|
||||
// Completed in foreground.
|
||||
activeJob = null;
|
||||
const exitCode =
|
||||
result.details?.status === "completed" ? result.details.exitCode : 0;
|
||||
const exitCode = result.details?.status === "completed" ? result.details.exitCode : 0;
|
||||
const output =
|
||||
result.details?.status === "completed"
|
||||
? result.details.aggregated
|
||||
: result.content
|
||||
.map((chunk) => (chunk.type === "text" ? chunk.text : ""))
|
||||
.join("\n");
|
||||
: result.content.map((chunk) => (chunk.type === "text" ? chunk.text : "")).join("\n");
|
||||
return {
|
||||
text: [
|
||||
`⚙️ bash: ${commandText}`,
|
||||
@@ -404,9 +382,7 @@ export async function handleBashChatCommand(params: {
|
||||
activeJob = null;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
text: [`⚠️ bash failed: ${commandText}`, formatOutputBlock(message)].join(
|
||||
"\n",
|
||||
),
|
||||
text: [`⚠️ bash failed: ${commandText}`, formatOutputBlock(message)].join("\n"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,7 @@ export function createBlockReplyCoalescer(params: {
|
||||
|
||||
const enqueue = (payload: ReplyPayload) => {
|
||||
if (shouldAbort()) return;
|
||||
const hasMedia =
|
||||
Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
const text = payload.text ?? "";
|
||||
const hasText = text.trim().length > 0;
|
||||
if (hasMedia) {
|
||||
@@ -79,8 +78,7 @@ export function createBlockReplyCoalescer(params: {
|
||||
|
||||
if (
|
||||
bufferText &&
|
||||
(bufferReplyToId !== payload.replyToId ||
|
||||
bufferAudioAsVoice !== payload.audioAsVoice)
|
||||
(bufferReplyToId !== payload.replyToId || bufferAudioAsVoice !== payload.audioAsVoice)
|
||||
) {
|
||||
void flush({ force: true });
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ export function createAudioAsVoiceBuffer(params: {
|
||||
}
|
||||
},
|
||||
shouldBuffer: (payload) => params.isAudioPayload(payload),
|
||||
finalize: (payload) =>
|
||||
seenAudioAsVoice ? { ...payload, audioAsVoice: true } : payload,
|
||||
finalize: (payload) => (seenAudioAsVoice ? { ...payload, audioAsVoice: true } : payload),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,9 +96,7 @@ export function createBlockReplyPipeline(params: {
|
||||
if (sentKeys.has(payloadKey) || pendingKeys.has(payloadKey)) return;
|
||||
pendingKeys.add(payloadKey);
|
||||
|
||||
const timeoutError = new Error(
|
||||
`block reply delivery timed out after ${timeoutMs}ms`,
|
||||
);
|
||||
const timeoutError = new Error(`block reply delivery timed out after ${timeoutMs}ms`);
|
||||
const abortController = new AbortController();
|
||||
sendChain = sendChain
|
||||
.then(async () => {
|
||||
@@ -180,8 +177,7 @@ export function createBlockReplyPipeline(params: {
|
||||
const enqueue = (payload: ReplyPayload) => {
|
||||
if (aborted) return;
|
||||
if (bufferPayload(payload)) return;
|
||||
const hasMedia =
|
||||
Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
if (hasMedia) {
|
||||
void coalescer?.flush({ force: true });
|
||||
sendPayload(payload);
|
||||
@@ -189,11 +185,7 @@ export function createBlockReplyPipeline(params: {
|
||||
}
|
||||
if (coalescer) {
|
||||
const payloadKey = createBlockReplyPayloadKey(payload);
|
||||
if (
|
||||
seenKeys.has(payloadKey) ||
|
||||
pendingKeys.has(payloadKey) ||
|
||||
bufferedKeys.has(payloadKey)
|
||||
) {
|
||||
if (seenKeys.has(payloadKey) || pendingKeys.has(payloadKey) || bufferedKeys.has(payloadKey)) {
|
||||
return;
|
||||
}
|
||||
bufferedKeys.add(payloadKey);
|
||||
@@ -217,8 +209,7 @@ export function createBlockReplyPipeline(params: {
|
||||
enqueue,
|
||||
flush,
|
||||
stop,
|
||||
hasBuffered: () =>
|
||||
Boolean(coalescer?.hasBuffered() || bufferedPayloads.length > 0),
|
||||
hasBuffered: () => Boolean(coalescer?.hasBuffered() || bufferedPayloads.length > 0),
|
||||
didStream: () => didStream,
|
||||
isAborted: () => aborted,
|
||||
hasSentPayload: (payload) => {
|
||||
|
||||
@@ -14,9 +14,7 @@ const BLOCK_CHUNK_PROVIDERS = new Set<TextChunkProvider>([
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
]);
|
||||
|
||||
function normalizeChunkProvider(
|
||||
provider?: string,
|
||||
): TextChunkProvider | undefined {
|
||||
function normalizeChunkProvider(provider?: string): TextChunkProvider | undefined {
|
||||
if (!provider) return undefined;
|
||||
const cleaned = provider.trim().toLowerCase();
|
||||
return BLOCK_CHUNK_PROVIDERS.has(cleaned as TextChunkProvider)
|
||||
@@ -26,10 +24,7 @@ function normalizeChunkProvider(
|
||||
|
||||
type ProviderBlockStreamingConfig = {
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
accounts?: Record<
|
||||
string,
|
||||
{ blockStreamingCoalesce?: BlockStreamingCoalesceConfig }
|
||||
>;
|
||||
accounts?: Record<string, { blockStreamingCoalesce?: BlockStreamingCoalesceConfig }>;
|
||||
};
|
||||
|
||||
function resolveProviderBlockStreamingCoalesce(params: {
|
||||
@@ -72,20 +67,13 @@ export function resolveBlockStreamingChunking(
|
||||
fallbackLimit: providerChunkLimit,
|
||||
});
|
||||
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
|
||||
const maxRequested = Math.max(
|
||||
1,
|
||||
Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX),
|
||||
);
|
||||
const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX));
|
||||
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
||||
const minFallback = DEFAULT_BLOCK_STREAM_MIN;
|
||||
const minRequested = Math.max(
|
||||
1,
|
||||
Math.floor(chunkCfg?.minChars ?? minFallback),
|
||||
);
|
||||
const minRequested = Math.max(1, Math.floor(chunkCfg?.minChars ?? minFallback));
|
||||
const minChars = Math.min(minRequested, maxChars);
|
||||
const breakPreference =
|
||||
chunkCfg?.breakPreference === "newline" ||
|
||||
chunkCfg?.breakPreference === "sentence"
|
||||
chunkCfg?.breakPreference === "newline" || chunkCfg?.breakPreference === "sentence"
|
||||
? chunkCfg.breakPreference
|
||||
: "paragraph";
|
||||
return { minChars, maxChars, breakPreference };
|
||||
@@ -117,8 +105,7 @@ export function resolveBlockStreamingCoalescing(
|
||||
providerKey,
|
||||
accountId,
|
||||
});
|
||||
const coalesceCfg =
|
||||
providerCfg ?? cfg?.agents?.defaults?.blockStreamingCoalesce;
|
||||
const coalesceCfg = providerCfg ?? cfg?.agents?.defaults?.blockStreamingCoalesce;
|
||||
const minRequested = Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
@@ -128,23 +115,17 @@ export function resolveBlockStreamingCoalescing(
|
||||
DEFAULT_BLOCK_STREAM_MIN,
|
||||
),
|
||||
);
|
||||
const maxRequested = Math.max(
|
||||
1,
|
||||
Math.floor(coalesceCfg?.maxChars ?? textLimit),
|
||||
);
|
||||
const maxRequested = Math.max(1, Math.floor(coalesceCfg?.maxChars ?? textLimit));
|
||||
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
||||
const minChars = Math.min(minRequested, maxChars);
|
||||
const idleMs = Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
coalesceCfg?.idleMs ??
|
||||
providerDefaults?.idleMs ??
|
||||
DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS,
|
||||
coalesceCfg?.idleMs ?? providerDefaults?.idleMs ?? DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS,
|
||||
),
|
||||
);
|
||||
const preference = chunking?.breakPreference ?? "paragraph";
|
||||
const joiner =
|
||||
preference === "sentence" ? " " : preference === "newline" ? "\n" : "\n\n";
|
||||
const joiner = preference === "sentence" ? " " : preference === "newline" ? "\n" : "\n\n";
|
||||
return {
|
||||
minChars,
|
||||
maxChars,
|
||||
|
||||
@@ -30,9 +30,7 @@ export async function applySessionHints(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const messageIdHint = params.messageId?.trim()
|
||||
? `[message_id: ${params.messageId.trim()}]`
|
||||
: "";
|
||||
const messageIdHint = params.messageId?.trim() ? `[message_id: ${params.messageId.trim()}]` : "";
|
||||
if (messageIdHint) {
|
||||
prefixedBodyBase = `${prefixedBodyBase}\n${messageIdHint}`;
|
||||
}
|
||||
|
||||
@@ -2,26 +2,17 @@ import { logVerbose } from "../../globals.js";
|
||||
import { handleBashChatCommand } from "./bash-command.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
|
||||
export const handleBashCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleBashCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
const { command } = params;
|
||||
const bashSlashRequested =
|
||||
command.commandBodyNormalized === "/bash" ||
|
||||
command.commandBodyNormalized.startsWith("/bash ");
|
||||
command.commandBodyNormalized === "/bash" || command.commandBodyNormalized.startsWith("/bash ");
|
||||
const bashBangRequested = command.commandBodyNormalized.startsWith("!");
|
||||
if (
|
||||
!bashSlashRequested &&
|
||||
!(bashBangRequested && command.isAuthorizedSender)
|
||||
) {
|
||||
if (!bashSlashRequested && !(bashBangRequested && command.isAuthorizedSender)) {
|
||||
return null;
|
||||
}
|
||||
if (!command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
`Ignoring /bash from unauthorized sender: ${command.senderId || "<unknown>"}`,
|
||||
);
|
||||
logVerbose(`Ignoring /bash from unauthorized sender: ${command.senderId || "<unknown>"}`);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
const reply = await handleBashChatCommand({
|
||||
|
||||
@@ -73,24 +73,19 @@ export const handleCompactCommand: CommandHandler = async (params) => {
|
||||
skillsSnapshot: params.sessionEntry.skillsSnapshot,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
thinkLevel:
|
||||
params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
|
||||
thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
|
||||
bashElevated: {
|
||||
enabled: false,
|
||||
allowed: false,
|
||||
defaultLevel: "off",
|
||||
},
|
||||
customInstructions,
|
||||
ownerNumbers:
|
||||
params.command.ownerList.length > 0
|
||||
? params.command.ownerList
|
||||
: undefined,
|
||||
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
|
||||
});
|
||||
|
||||
const totalTokens =
|
||||
params.sessionEntry.totalTokens ??
|
||||
(params.sessionEntry.inputTokens ?? 0) +
|
||||
(params.sessionEntry.outputTokens ?? 0);
|
||||
(params.sessionEntry.inputTokens ?? 0) + (params.sessionEntry.outputTokens ?? 0);
|
||||
const contextSummary = formatContextUsageShort(
|
||||
totalTokens > 0 ? totalTokens : null,
|
||||
params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
|
||||
|
||||
@@ -20,14 +20,9 @@ import type { CommandHandler } from "./commands-types.js";
|
||||
import { parseConfigCommand } from "./config-commands.js";
|
||||
import { parseDebugCommand } from "./debug-commands.js";
|
||||
|
||||
export const handleConfigCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleConfigCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
const configCommand = parseConfigCommand(
|
||||
params.command.commandBodyNormalized,
|
||||
);
|
||||
const configCommand = parseConfigCommand(params.command.commandBodyNormalized);
|
||||
if (!configCommand) return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
@@ -50,11 +45,7 @@ export const handleConfigCommand: CommandHandler = async (
|
||||
};
|
||||
}
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (
|
||||
!snapshot.valid ||
|
||||
!snapshot.parsed ||
|
||||
typeof snapshot.parsed !== "object"
|
||||
) {
|
||||
if (!snapshot.valid || !snapshot.parsed || typeof snapshot.parsed !== "object") {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
@@ -62,9 +53,7 @@ export const handleConfigCommand: CommandHandler = async (
|
||||
},
|
||||
};
|
||||
}
|
||||
const parsedBase = structuredClone(
|
||||
snapshot.parsed as Record<string, unknown>,
|
||||
);
|
||||
const parsedBase = structuredClone(snapshot.parsed as Record<string, unknown>);
|
||||
|
||||
if (configCommand.action === "show") {
|
||||
const pathRaw = configCommand.path?.trim();
|
||||
@@ -159,10 +148,7 @@ export const handleConfigCommand: CommandHandler = async (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handleDebugCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleDebugCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
const debugCommand = parseDebugCommand(params.command.commandBodyNormalized);
|
||||
if (!debugCommand) return null;
|
||||
|
||||
@@ -14,8 +14,7 @@ export function buildCommandContext(params: {
|
||||
triggerBodyNormalized: string;
|
||||
commandAuthorized: boolean;
|
||||
}): CommandContext {
|
||||
const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } =
|
||||
params;
|
||||
const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } = params;
|
||||
const auth = resolveCommandAuthorization({
|
||||
ctx,
|
||||
cfg,
|
||||
@@ -23,13 +22,10 @@ export function buildCommandContext(params: {
|
||||
});
|
||||
const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase();
|
||||
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
|
||||
const abortKey =
|
||||
sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
||||
const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
||||
const rawBodyNormalized = triggerBodyNormalized;
|
||||
const commandBodyNormalized = normalizeCommandBody(
|
||||
isGroup
|
||||
? stripMentions(rawBodyNormalized, ctx, cfg, agentId)
|
||||
: rawBodyNormalized,
|
||||
isGroup ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) : rawBodyNormalized,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -39,9 +39,7 @@ const HANDLERS: CommandHandler[] = [
|
||||
handleAbortTrigger,
|
||||
];
|
||||
|
||||
export async function handleCommands(
|
||||
params: HandleCommandsParams,
|
||||
): Promise<CommandHandlerResult> {
|
||||
export async function handleCommands(params: HandleCommandsParams): Promise<CommandHandlerResult> {
|
||||
const resetRequested =
|
||||
params.command.commandBodyNormalized === "/reset" ||
|
||||
params.command.commandBodyNormalized === "/new";
|
||||
@@ -71,9 +69,7 @@ export async function handleCommands(
|
||||
chatType: params.sessionEntry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
logVerbose(
|
||||
`Send blocked by policy for session ${params.sessionKey ?? "unknown"}`,
|
||||
);
|
||||
logVerbose(`Send blocked by policy for session ${params.sessionKey ?? "unknown"}`);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@ import { buildCommandsMessage, buildHelpMessage } from "../status.js";
|
||||
import { buildStatusReply } from "./commands-status.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
|
||||
export const handleHelpCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleHelpCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
if (params.command.commandBodyNormalized !== "/help") return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
@@ -21,10 +18,7 @@ export const handleHelpCommand: CommandHandler = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const handleCommandsListCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleCommandsListCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
if (params.command.commandBodyNormalized !== "/commands") return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
@@ -39,14 +33,10 @@ export const handleCommandsListCommand: CommandHandler = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const handleStatusCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
const statusRequested =
|
||||
params.directives.hasStatusDirective ||
|
||||
params.command.commandBodyNormalized === "/status";
|
||||
params.directives.hasStatusDirective || params.command.commandBodyNormalized === "/status";
|
||||
if (!statusRequested) return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
@@ -74,10 +64,7 @@ export const handleStatusCommand: CommandHandler = async (
|
||||
return { shouldContinue: false, reply };
|
||||
};
|
||||
|
||||
export const handleWhoamiCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleWhoamiCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
if (params.command.commandBodyNormalized !== "/whoami") return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
@@ -91,9 +78,7 @@ export const handleWhoamiCommand: CommandHandler = async (
|
||||
const lines = ["🧭 Identity", `Channel: ${params.command.channel}`];
|
||||
if (senderId) lines.push(`User id: ${senderId}`);
|
||||
if (senderUsername) {
|
||||
const handle = senderUsername.startsWith("@")
|
||||
? senderUsername
|
||||
: `@${senderUsername}`;
|
||||
const handle = senderUsername.startsWith("@") ? senderUsername : `@${senderUsername}`;
|
||||
lines.push(`Username: ${handle}`);
|
||||
}
|
||||
if (params.ctx.ChatType === "group" && params.ctx.From) {
|
||||
|
||||
@@ -2,10 +2,7 @@ import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { saveSessionStore } from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import {
|
||||
scheduleGatewaySigusr1Restart,
|
||||
triggerClawdbotRestart,
|
||||
} from "../../infra/restart.js";
|
||||
import { scheduleGatewaySigusr1Restart, triggerClawdbotRestart } from "../../infra/restart.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { parseActivationCommand } from "../group-activation.js";
|
||||
import { parseSendPolicyCommand } from "../send-policy.js";
|
||||
@@ -33,12 +30,8 @@ function resolveAbortTarget(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
}) {
|
||||
const targetSessionKey =
|
||||
params.ctx.CommandTargetSessionKey?.trim() || params.sessionKey;
|
||||
const { entry, key } = resolveSessionEntryForKey(
|
||||
params.sessionStore,
|
||||
targetSessionKey,
|
||||
);
|
||||
const targetSessionKey = params.ctx.CommandTargetSessionKey?.trim() || params.sessionKey;
|
||||
const { entry, key } = resolveSessionEntryForKey(params.sessionStore, targetSessionKey);
|
||||
if (entry && key) return { entry, key, sessionId: entry.sessionId };
|
||||
if (params.sessionEntry && params.sessionKey) {
|
||||
return {
|
||||
@@ -50,14 +43,9 @@ function resolveAbortTarget(params: {
|
||||
return { entry: undefined, key: targetSessionKey, sessionId: undefined };
|
||||
}
|
||||
|
||||
export const handleActivationCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleActivationCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
const activationCommand = parseActivationCommand(
|
||||
params.command.commandBodyNormalized,
|
||||
);
|
||||
const activationCommand = parseActivationCommand(params.command.commandBodyNormalized);
|
||||
if (!activationCommand.hasCommand) return null;
|
||||
if (!params.isGroup) {
|
||||
return {
|
||||
@@ -94,14 +82,9 @@ export const handleActivationCommand: CommandHandler = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const handleSendPolicyCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleSendPolicyCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
const sendPolicyCommand = parseSendPolicyCommand(
|
||||
params.command.commandBodyNormalized,
|
||||
);
|
||||
const sendPolicyCommand = parseSendPolicyCommand(params.command.commandBodyNormalized);
|
||||
if (!sendPolicyCommand.hasCommand) return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
@@ -139,10 +122,7 @@ export const handleSendPolicyCommand: CommandHandler = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const handleRestartCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleRestartCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
if (params.command.commandBodyNormalized !== "/restart") return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
@@ -171,9 +151,7 @@ export const handleRestartCommand: CommandHandler = async (
|
||||
}
|
||||
const restartMethod = triggerClawdbotRestart();
|
||||
if (!restartMethod.ok) {
|
||||
const detail = restartMethod.detail
|
||||
? ` Details: ${restartMethod.detail}`
|
||||
: "";
|
||||
const detail = restartMethod.detail ? ` Details: ${restartMethod.detail}` : "";
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
@@ -189,10 +167,7 @@ export const handleRestartCommand: CommandHandler = async (
|
||||
};
|
||||
};
|
||||
|
||||
export const handleStopCommand: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleStopCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
if (params.command.commandBodyNormalized !== "/stop") return null;
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
@@ -223,10 +198,7 @@ export const handleStopCommand: CommandHandler = async (
|
||||
return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } };
|
||||
};
|
||||
|
||||
export const handleAbortTrigger: CommandHandler = async (
|
||||
params,
|
||||
allowTextCommands,
|
||||
) => {
|
||||
export const handleAbortTrigger: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) return null;
|
||||
if (!isAbortTrigger(params.command.rawBodyNormalized)) return null;
|
||||
const abortTarget = resolveAbortTarget({
|
||||
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
resolveAuthProfileDisplayLabel,
|
||||
resolveAuthProfileOrder,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
getCustomProviderApiKey,
|
||||
resolveEnvApiKey,
|
||||
} from "../../agents/model-auth.js";
|
||||
import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js";
|
||||
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { SessionEntry, SessionScope } from "../../config/sessions.js";
|
||||
@@ -23,12 +20,7 @@ import {
|
||||
} from "../../infra/provider-usage.js";
|
||||
import { normalizeGroupActivation } from "../group-activation.js";
|
||||
import { buildStatusMessage } from "../status.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
ThinkLevel,
|
||||
VerboseLevel,
|
||||
} from "../thinking.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import type { CommandContext } from "./commands-types.js";
|
||||
import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
|
||||
@@ -132,9 +124,7 @@ export async function buildStatusReply(params: {
|
||||
defaultGroupActivation,
|
||||
} = params;
|
||||
if (!command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
`Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`,
|
||||
);
|
||||
logVerbose(`Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`);
|
||||
return undefined;
|
||||
}
|
||||
const statusAgentId = sessionKey
|
||||
@@ -151,10 +141,7 @@ export async function buildStatusReply(params: {
|
||||
agentDir: statusAgentDir,
|
||||
});
|
||||
usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
|
||||
if (
|
||||
!usageLine &&
|
||||
(resolvedVerboseLevel === "on" || resolvedElevatedLevel === "on")
|
||||
) {
|
||||
if (!usageLine && (resolvedVerboseLevel === "on" || resolvedElevatedLevel === "on")) {
|
||||
const entry = usageSummary.providers[0];
|
||||
if (entry?.error) {
|
||||
usageLine = `📊 Usage: ${entry.displayName} (${entry.error})`;
|
||||
@@ -172,13 +159,10 @@ export async function buildStatusReply(params: {
|
||||
const queueKey = sessionKey ?? sessionEntry?.sessionId;
|
||||
const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
|
||||
const queueOverrides = Boolean(
|
||||
sessionEntry?.queueDebounceMs ??
|
||||
sessionEntry?.queueCap ??
|
||||
sessionEntry?.queueDrop,
|
||||
sessionEntry?.queueDebounceMs ?? sessionEntry?.queueCap ?? sessionEntry?.queueDrop,
|
||||
);
|
||||
const groupActivation = isGroup
|
||||
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
|
||||
defaultGroupActivation())
|
||||
? (normalizeGroupActivation(sessionEntry?.groupActivation) ?? defaultGroupActivation())
|
||||
: undefined;
|
||||
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||
const statusText = buildStatusMessage({
|
||||
@@ -202,12 +186,7 @@ export async function buildStatusReply(params: {
|
||||
resolvedVerbose: resolvedVerboseLevel,
|
||||
resolvedReasoning: resolvedReasoningLevel,
|
||||
resolvedElevated: resolvedElevatedLevel,
|
||||
modelAuth: resolveModelAuthLabel(
|
||||
provider,
|
||||
cfg,
|
||||
sessionEntry,
|
||||
statusAgentDir,
|
||||
),
|
||||
modelAuth: resolveModelAuthLabel(provider, cfg, sessionEntry, statusAgentDir),
|
||||
usageLine: usageLine ?? undefined,
|
||||
queue: {
|
||||
mode: queueSettings.mode,
|
||||
|
||||
@@ -2,12 +2,7 @@ import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { SessionEntry, SessionScope } from "../../config/sessions.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
ThinkLevel,
|
||||
VerboseLevel,
|
||||
} from "../thinking.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import type { InlineDirectives } from "./directive-handling.js";
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import { resetBashChatCommandForTests } from "./bash-command.js";
|
||||
import { buildCommandContext, handleCommands } from "./commands.js";
|
||||
import { parseInlineDirectives } from "./directive-handling.js";
|
||||
|
||||
function buildParams(
|
||||
commandBody: string,
|
||||
cfg: ClawdbotConfig,
|
||||
ctxOverrides?: Partial<MsgContext>,
|
||||
) {
|
||||
function buildParams(commandBody: string, cfg: ClawdbotConfig, ctxOverrides?: Partial<MsgContext>) {
|
||||
const ctx = {
|
||||
Body: commandBody,
|
||||
CommandBody: commandBody,
|
||||
@@ -71,9 +67,7 @@ describe("handleCommands gating", () => {
|
||||
params.elevated = {
|
||||
enabled: true,
|
||||
allowed: false,
|
||||
failures: [
|
||||
{ gate: "allowFrom", key: "tools.elevated.allowFrom.whatsapp" },
|
||||
],
|
||||
failures: [{ gate: "allowFrom", key: "tools.elevated.allowFrom.whatsapp" }],
|
||||
};
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
|
||||
@@ -23,8 +23,7 @@ export function parseConfigCommand(raw: string): ConfigCommand | null {
|
||||
case "get":
|
||||
return { action: "show", path: args || undefined };
|
||||
case "unset": {
|
||||
if (!args)
|
||||
return { action: "error", message: "Usage: /config unset path" };
|
||||
if (!args) return { action: "error", message: "Usage: /config unset path" };
|
||||
return { action: "unset", path: args };
|
||||
}
|
||||
case "set": {
|
||||
|
||||
@@ -24,8 +24,7 @@ export function parseDebugCommand(raw: string): DebugCommand | null {
|
||||
case "reset":
|
||||
return { action: "reset" };
|
||||
case "unset": {
|
||||
if (!args)
|
||||
return { action: "error", message: "Usage: /debug unset path" };
|
||||
if (!args) return { action: "error", message: "Usage: /debug unset path" };
|
||||
return { action: "unset", path: args };
|
||||
}
|
||||
case "set": {
|
||||
|
||||
@@ -65,8 +65,7 @@ export const resolveAuthLabel = async (
|
||||
const configProfile = cfg.auth?.profiles?.[profileId];
|
||||
const missing =
|
||||
!profile ||
|
||||
(configProfile?.provider &&
|
||||
configProfile.provider !== profile.provider) ||
|
||||
(configProfile?.provider && configProfile.provider !== profile.provider) ||
|
||||
(configProfile?.mode &&
|
||||
configProfile.mode !== profile.type &&
|
||||
!(configProfile.mode === "oauth" && profile.type === "token"));
|
||||
@@ -115,11 +114,7 @@ export const resolveAuthLabel = async (
|
||||
if (lastGood && profileId === lastGood) flags.push("lastGood");
|
||||
if (isProfileInCooldown(store, profileId)) {
|
||||
const until = store.usageStats?.[profileId]?.cooldownUntil;
|
||||
if (
|
||||
typeof until === "number" &&
|
||||
Number.isFinite(until) &&
|
||||
until > now
|
||||
) {
|
||||
if (typeof until === "number" && Number.isFinite(until) && until > now) {
|
||||
flags.push(`cooldown ${formatUntil(until)}`);
|
||||
} else {
|
||||
flags.push("cooldown");
|
||||
@@ -127,8 +122,7 @@ export const resolveAuthLabel = async (
|
||||
}
|
||||
if (
|
||||
!profile ||
|
||||
(configProfile?.provider &&
|
||||
configProfile.provider !== profile.provider) ||
|
||||
(configProfile?.provider && configProfile.provider !== profile.provider) ||
|
||||
(configProfile?.mode &&
|
||||
configProfile.mode !== profile.type &&
|
||||
!(configProfile.mode === "oauth" && profile.type === "token"))
|
||||
@@ -146,11 +140,7 @@ export const resolveAuthLabel = async (
|
||||
Number.isFinite(profile.expires) &&
|
||||
profile.expires > 0
|
||||
) {
|
||||
flags.push(
|
||||
profile.expires <= now
|
||||
? "expired"
|
||||
: `exp ${formatUntil(profile.expires)}`,
|
||||
);
|
||||
flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`);
|
||||
}
|
||||
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
||||
return `${profileId}=token:${maskApiKey(profile.token)}${suffix}`;
|
||||
@@ -171,11 +161,7 @@ export const resolveAuthLabel = async (
|
||||
Number.isFinite(profile.expires) &&
|
||||
profile.expires > 0
|
||||
) {
|
||||
flags.push(
|
||||
profile.expires <= now
|
||||
? "expired"
|
||||
: `exp ${formatUntil(profile.expires)}`,
|
||||
);
|
||||
flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`);
|
||||
}
|
||||
const suffixLabel = suffix ? ` ${suffix}` : "";
|
||||
const suffixFlags = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
||||
@@ -199,8 +185,7 @@ export const resolveAuthLabel = async (
|
||||
if (customKey) {
|
||||
return {
|
||||
label: maskApiKey(customKey),
|
||||
source:
|
||||
mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "",
|
||||
source: mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "",
|
||||
};
|
||||
}
|
||||
return { label: "missing", source: "missing" };
|
||||
|
||||
@@ -6,12 +6,7 @@ import type { ReplyPayload } from "../types.js";
|
||||
import { handleDirectiveOnly } from "./directive-handling.impl.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { isDirectiveOnly } from "./directive-handling.parse.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
ThinkLevel,
|
||||
VerboseLevel,
|
||||
} from "./directives.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js";
|
||||
|
||||
export async function applyInlineDirectivesFastLane(params: {
|
||||
directives: InlineDirectives;
|
||||
@@ -45,9 +40,7 @@ export async function applyInlineDirectivesFastLane(params: {
|
||||
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
|
||||
allowedModelKeys: Set<string>;
|
||||
allowedModelCatalog: Awaited<
|
||||
ReturnType<
|
||||
typeof import("../../agents/model-catalog.js").loadModelCatalog
|
||||
>
|
||||
ReturnType<typeof import("../../agents/model-catalog.js").loadModelCatalog>
|
||||
>;
|
||||
resetModelOverride: boolean;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import {
|
||||
formatThinkingLevels,
|
||||
formatXHighModelHint,
|
||||
supportsXHighThinking,
|
||||
} from "../thinking.js";
|
||||
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import {
|
||||
maybeHandleModelDirectiveInfo,
|
||||
@@ -28,12 +21,7 @@ import {
|
||||
formatReasoningEvent,
|
||||
withOptions,
|
||||
} from "./directive-handling.shared.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
ThinkLevel,
|
||||
VerboseLevel,
|
||||
} from "./directives.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js";
|
||||
|
||||
export async function handleDirectiveOnly(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
@@ -95,8 +83,7 @@ export async function handleDirectiveOnly(params: {
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
}).sandboxed;
|
||||
const shouldHintDirectRuntime =
|
||||
directives.hasElevatedDirective && !runtimeIsSandboxed;
|
||||
const shouldHintDirectRuntime = directives.hasElevatedDirective && !runtimeIsSandboxed;
|
||||
|
||||
const modelInfo = await maybeHandleModelDirectiveInfo({
|
||||
directives,
|
||||
@@ -161,10 +148,7 @@ export async function handleDirectiveOnly(params: {
|
||||
if (!directives.rawReasoningLevel) {
|
||||
const level = currentReasoningLevel ?? "off";
|
||||
return {
|
||||
text: withOptions(
|
||||
`Current reasoning level: ${level}.`,
|
||||
"on, off, stream",
|
||||
),
|
||||
text: withOptions(`Current reasoning level: ${level}.`, "on, off, stream"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -196,10 +180,7 @@ export async function handleDirectiveOnly(params: {
|
||||
text: `Unrecognized elevated level "${directives.rawElevatedLevel}". Valid levels: off, on.`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
directives.hasElevatedDirective &&
|
||||
(!elevatedEnabled || !elevatedAllowed)
|
||||
) {
|
||||
if (directives.hasElevatedDirective && (!elevatedEnabled || !elevatedAllowed)) {
|
||||
return {
|
||||
text: formatElevatedUnavailableText({
|
||||
runtimeSandboxed: runtimeIsSandboxed,
|
||||
@@ -229,8 +210,7 @@ export async function handleDirectiveOnly(params: {
|
||||
|
||||
const nextThinkLevel = directives.hasThinkDirective
|
||||
? directives.thinkLevel
|
||||
: ((sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
|
||||
currentThinkLevel);
|
||||
: ((sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? currentThinkLevel);
|
||||
const shouldDowngradeXHigh =
|
||||
!directives.hasThinkDirective &&
|
||||
nextThinkLevel === "xhigh" &&
|
||||
@@ -242,17 +222,14 @@ export async function handleDirectiveOnly(params: {
|
||||
(sessionEntry.elevatedLevel as ElevatedLevel | undefined) ??
|
||||
(elevatedAllowed ? ("on" as ElevatedLevel) : ("off" as ElevatedLevel));
|
||||
const prevReasoningLevel =
|
||||
currentReasoningLevel ??
|
||||
(sessionEntry.reasoningLevel as ReasoningLevel | undefined) ??
|
||||
"off";
|
||||
currentReasoningLevel ?? (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? "off";
|
||||
let elevatedChanged =
|
||||
directives.hasElevatedDirective &&
|
||||
directives.elevatedLevel !== undefined &&
|
||||
elevatedEnabled &&
|
||||
elevatedAllowed;
|
||||
let reasoningChanged =
|
||||
directives.hasReasoningDirective &&
|
||||
directives.reasoningLevel !== undefined;
|
||||
directives.hasReasoningDirective && directives.reasoningLevel !== undefined;
|
||||
if (directives.hasThinkDirective && directives.thinkLevel) {
|
||||
if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel;
|
||||
else sessionEntry.thinkingLevel = directives.thinkLevel;
|
||||
@@ -264,12 +241,10 @@ export async function handleDirectiveOnly(params: {
|
||||
applyVerboseOverride(sessionEntry, directives.verboseLevel);
|
||||
}
|
||||
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
||||
if (directives.reasoningLevel === "off")
|
||||
delete sessionEntry.reasoningLevel;
|
||||
if (directives.reasoningLevel === "off") delete sessionEntry.reasoningLevel;
|
||||
else sessionEntry.reasoningLevel = directives.reasoningLevel;
|
||||
reasoningChanged =
|
||||
directives.reasoningLevel !== prevReasoningLevel &&
|
||||
directives.reasoningLevel !== undefined;
|
||||
directives.reasoningLevel !== prevReasoningLevel && directives.reasoningLevel !== undefined;
|
||||
}
|
||||
if (directives.hasElevatedDirective && directives.elevatedLevel) {
|
||||
// Unlike other toggles, elevated defaults can be "on".
|
||||
@@ -277,8 +252,7 @@ export async function handleDirectiveOnly(params: {
|
||||
sessionEntry.elevatedLevel = directives.elevatedLevel;
|
||||
elevatedChanged =
|
||||
elevatedChanged ||
|
||||
(directives.elevatedLevel !== prevElevatedLevel &&
|
||||
directives.elevatedLevel !== undefined);
|
||||
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
||||
}
|
||||
if (modelSelection) {
|
||||
if (modelSelection.isDefault) {
|
||||
@@ -319,26 +293,21 @@ export async function handleDirectiveOnly(params: {
|
||||
if (modelSelection) {
|
||||
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
||||
if (nextLabel !== initialModelLabel) {
|
||||
enqueueSystemEvent(
|
||||
formatModelSwitchEvent(nextLabel, modelSelection.alias),
|
||||
{
|
||||
sessionKey,
|
||||
contextKey: `model:${nextLabel}`,
|
||||
},
|
||||
);
|
||||
enqueueSystemEvent(formatModelSwitchEvent(nextLabel, modelSelection.alias), {
|
||||
sessionKey,
|
||||
contextKey: `model:${nextLabel}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (elevatedChanged) {
|
||||
const nextElevated = (sessionEntry.elevatedLevel ??
|
||||
"off") as ElevatedLevel;
|
||||
const nextElevated = (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel;
|
||||
enqueueSystemEvent(formatElevatedEvent(nextElevated), {
|
||||
sessionKey,
|
||||
contextKey: "mode:elevated",
|
||||
});
|
||||
}
|
||||
if (reasoningChanged) {
|
||||
const nextReasoning = (sessionEntry.reasoningLevel ??
|
||||
"off") as ReasoningLevel;
|
||||
const nextReasoning = (sessionEntry.reasoningLevel ?? "off") as ReasoningLevel;
|
||||
enqueueSystemEvent(formatReasoningEvent(nextReasoning), {
|
||||
sessionKey,
|
||||
contextKey: "mode:reasoning",
|
||||
@@ -385,9 +354,7 @@ export async function handleDirectiveOnly(params: {
|
||||
}
|
||||
if (modelSelection) {
|
||||
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
||||
const labelWithAlias = modelSelection.alias
|
||||
? `${modelSelection.alias} (${label})`
|
||||
: label;
|
||||
const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label;
|
||||
parts.push(
|
||||
modelSelection.isDefault
|
||||
? `Model reset to default (${labelWithAlias}).`
|
||||
@@ -398,27 +365,18 @@ export async function handleDirectiveOnly(params: {
|
||||
}
|
||||
}
|
||||
if (directives.hasQueueDirective && directives.queueMode) {
|
||||
parts.push(
|
||||
formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`),
|
||||
);
|
||||
parts.push(formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`));
|
||||
} else if (directives.hasQueueDirective && directives.queueReset) {
|
||||
parts.push(formatDirectiveAck("Queue mode reset to default."));
|
||||
}
|
||||
if (
|
||||
directives.hasQueueDirective &&
|
||||
typeof directives.debounceMs === "number"
|
||||
) {
|
||||
parts.push(
|
||||
formatDirectiveAck(`Queue debounce set to ${directives.debounceMs}ms.`),
|
||||
);
|
||||
if (directives.hasQueueDirective && typeof directives.debounceMs === "number") {
|
||||
parts.push(formatDirectiveAck(`Queue debounce set to ${directives.debounceMs}ms.`));
|
||||
}
|
||||
if (directives.hasQueueDirective && typeof directives.cap === "number") {
|
||||
parts.push(formatDirectiveAck(`Queue cap set to ${directives.cap}.`));
|
||||
}
|
||||
if (directives.hasQueueDirective && directives.dropPolicy) {
|
||||
parts.push(
|
||||
formatDirectiveAck(`Queue drop set to ${directives.dropPolicy}.`),
|
||||
);
|
||||
parts.push(formatDirectiveAck(`Queue drop set to ${directives.dropPolicy}.`));
|
||||
}
|
||||
const ack = parts.join(" ").trim();
|
||||
if (!ack && directives.hasStatusDirective) return undefined;
|
||||
|
||||
@@ -52,9 +52,7 @@ function sortProvidersForPicker(providers: string[]): string[] {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildModelPickerItems(
|
||||
catalog: ModelPickerCatalogEntry[],
|
||||
): ModelPickerItem[] {
|
||||
export function buildModelPickerItems(catalog: ModelPickerCatalogEntry[]): ModelPickerItem[] {
|
||||
const byModel = new Map<string, { providerModels: Record<string, string> }>();
|
||||
for (const entry of catalog) {
|
||||
const provider = normalizeProviderId(entry.provider);
|
||||
@@ -72,9 +70,7 @@ export function buildModelPickerItems(
|
||||
const providers = sortProvidersForPicker(Object.keys(data.providerModels));
|
||||
out.push({ model, providers, providerModels: data.providerModels });
|
||||
}
|
||||
out.sort((a, b) =>
|
||||
a.model.toLowerCase().localeCompare(b.model.toLowerCase()),
|
||||
);
|
||||
out.sort((a, b) => a.model.toLowerCase().localeCompare(b.model.toLowerCase()));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,7 @@ import {
|
||||
resolveProviderEndpointLabel,
|
||||
} from "./directive-handling.model-picker.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import {
|
||||
type ModelDirectiveSelection,
|
||||
resolveModelDirectiveSelection,
|
||||
} from "./model-selection.js";
|
||||
import { type ModelDirectiveSelection, resolveModelDirectiveSelection } from "./model-selection.js";
|
||||
|
||||
function buildModelPickerCatalog(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
@@ -127,10 +124,7 @@ export async function maybeHandleModelDirectiveInfo(params: {
|
||||
const items = buildModelPickerItems(pickerCatalog);
|
||||
if (items.length === 0) return { text: "No models available." };
|
||||
const current = `${params.provider}/${params.model}`;
|
||||
const lines: string[] = [
|
||||
`Current: ${current}`,
|
||||
"Pick: /model <#> or /model <provider/model>",
|
||||
];
|
||||
const lines: string[] = [`Current: ${current}`, "Pick: /model <#> or /model <provider/model>"];
|
||||
for (const [idx, item] of items.entries()) {
|
||||
lines.push(`${idx + 1}) ${item.model} — ${item.providers.join(", ")}`);
|
||||
}
|
||||
@@ -194,8 +188,7 @@ export async function maybeHandleModelDirectiveInfo(params: {
|
||||
for (const entry of models) {
|
||||
const label = `${provider}/${entry.id}`;
|
||||
const aliases = params.aliasIndex.byKey.get(label);
|
||||
const aliasSuffix =
|
||||
aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : "";
|
||||
const aliasSuffix = aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : "";
|
||||
lines.push(` • ${label}${aliasSuffix}`);
|
||||
}
|
||||
}
|
||||
@@ -217,10 +210,7 @@ export function resolveModelSelectionFromDirective(params: {
|
||||
profileOverride?: string;
|
||||
errorText?: string;
|
||||
} {
|
||||
if (
|
||||
!params.directives.hasModelDirective ||
|
||||
!params.directives.rawModelDirective
|
||||
) {
|
||||
if (!params.directives.hasModelDirective || !params.directives.rawModelDirective) {
|
||||
if (params.directives.rawModelProfile) {
|
||||
return { errorText: "Auth profile override requires a model selection." };
|
||||
}
|
||||
@@ -261,9 +251,7 @@ export function resolveModelSelectionFromDirective(params: {
|
||||
modelSelection = {
|
||||
provider: picked.provider,
|
||||
model: picked.model,
|
||||
isDefault:
|
||||
picked.provider === params.defaultProvider &&
|
||||
picked.model === params.defaultModel,
|
||||
isDefault: picked.provider === params.defaultProvider && picked.model === params.defaultModel,
|
||||
...(alias ? { alias } : {}),
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { extractModelDirective } from "../model.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import type {
|
||||
ElevatedLevel,
|
||||
ReasoningLevel,
|
||||
ThinkLevel,
|
||||
VerboseLevel,
|
||||
} from "./directives.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js";
|
||||
import {
|
||||
extractElevatedDirective,
|
||||
extractReasoningDirective,
|
||||
@@ -89,10 +84,9 @@ export function parseInlineDirectives(
|
||||
}
|
||||
: extractElevatedDirective(reasoningCleaned);
|
||||
const allowStatusDirective = options?.allowStatusDirective !== false;
|
||||
const { cleaned: statusCleaned, hasDirective: hasStatusDirective } =
|
||||
allowStatusDirective
|
||||
? extractStatusDirective(elevatedCleaned)
|
||||
: { cleaned: elevatedCleaned, hasDirective: false };
|
||||
const { cleaned: statusCleaned, hasDirective: hasStatusDirective } = allowStatusDirective
|
||||
? extractStatusDirective(elevatedCleaned)
|
||||
: { cleaned: elevatedCleaned, hasDirective: false };
|
||||
const {
|
||||
cleaned: modelCleaned,
|
||||
rawModel,
|
||||
@@ -167,8 +161,6 @@ export function isDirectiveOnly(params: {
|
||||
)
|
||||
return false;
|
||||
const stripped = stripStructuralPrefixes(cleanedBody ?? "");
|
||||
const noMentions = isGroup
|
||||
? stripMentions(stripped, ctx, cfg, agentId)
|
||||
: stripped;
|
||||
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg, agentId) : stripped;
|
||||
return noMentions.length === 0;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import {
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import {
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_PROVIDER,
|
||||
} from "../../agents/defaults.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
@@ -23,10 +19,7 @@ import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { resolveProfileOverride } from "./directive-handling.auth.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import {
|
||||
formatElevatedEvent,
|
||||
formatReasoningEvent,
|
||||
} from "./directive-handling.shared.js";
|
||||
import { formatElevatedEvent, formatReasoningEvent } from "./directive-handling.shared.js";
|
||||
import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
|
||||
|
||||
export async function persistInlineDirectives(params: {
|
||||
@@ -78,16 +71,14 @@ export async function persistInlineDirectives(params: {
|
||||
(sessionEntry.elevatedLevel as ElevatedLevel | undefined) ??
|
||||
(agentCfg?.elevatedDefault as ElevatedLevel | undefined) ??
|
||||
(elevatedAllowed ? ("on" as ElevatedLevel) : ("off" as ElevatedLevel));
|
||||
const prevReasoningLevel =
|
||||
(sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? "off";
|
||||
const prevReasoningLevel = (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? "off";
|
||||
let elevatedChanged =
|
||||
directives.hasElevatedDirective &&
|
||||
directives.elevatedLevel !== undefined &&
|
||||
elevatedEnabled &&
|
||||
elevatedAllowed;
|
||||
let reasoningChanged =
|
||||
directives.hasReasoningDirective &&
|
||||
directives.reasoningLevel !== undefined;
|
||||
directives.hasReasoningDirective && directives.reasoningLevel !== undefined;
|
||||
let updated = false;
|
||||
|
||||
if (directives.hasThinkDirective && directives.thinkLevel) {
|
||||
@@ -124,8 +115,7 @@ export async function persistInlineDirectives(params: {
|
||||
sessionEntry.elevatedLevel = directives.elevatedLevel;
|
||||
elevatedChanged =
|
||||
elevatedChanged ||
|
||||
(directives.elevatedLevel !== prevElevatedLevel &&
|
||||
directives.elevatedLevel !== undefined);
|
||||
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
@@ -156,8 +146,7 @@ export async function persistInlineDirectives(params: {
|
||||
profileOverride = profileResolved.profileId;
|
||||
}
|
||||
const isDefault =
|
||||
resolved.ref.provider === defaultProvider &&
|
||||
resolved.ref.model === defaultModel;
|
||||
resolved.ref.provider === defaultProvider && resolved.ref.model === defaultModel;
|
||||
if (isDefault) {
|
||||
delete sessionEntry.providerOverride;
|
||||
delete sessionEntry.modelOverride;
|
||||
@@ -174,13 +163,10 @@ export async function persistInlineDirectives(params: {
|
||||
model = resolved.ref.model;
|
||||
const nextLabel = `${provider}/${model}`;
|
||||
if (nextLabel !== initialModelLabel) {
|
||||
enqueueSystemEvent(
|
||||
formatModelSwitchEvent(nextLabel, resolved.alias),
|
||||
{
|
||||
sessionKey,
|
||||
contextKey: `model:${nextLabel}`,
|
||||
},
|
||||
);
|
||||
enqueueSystemEvent(formatModelSwitchEvent(nextLabel, resolved.alias), {
|
||||
sessionKey,
|
||||
contextKey: `model:${nextLabel}`,
|
||||
});
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
@@ -201,16 +187,14 @@ export async function persistInlineDirectives(params: {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
if (elevatedChanged) {
|
||||
const nextElevated = (sessionEntry.elevatedLevel ??
|
||||
"off") as ElevatedLevel;
|
||||
const nextElevated = (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel;
|
||||
enqueueSystemEvent(formatElevatedEvent(nextElevated), {
|
||||
sessionKey,
|
||||
contextKey: "mode:elevated",
|
||||
});
|
||||
}
|
||||
if (reasoningChanged) {
|
||||
const nextReasoning = (sessionEntry.reasoningLevel ??
|
||||
"off") as ReasoningLevel;
|
||||
const nextReasoning = (sessionEntry.reasoningLevel ?? "off") as ReasoningLevel;
|
||||
enqueueSystemEvent(formatReasoningEvent(nextReasoning), {
|
||||
sessionKey,
|
||||
contextKey: "mode:reasoning",
|
||||
@@ -222,17 +206,11 @@ export async function persistInlineDirectives(params: {
|
||||
return {
|
||||
provider,
|
||||
model,
|
||||
contextTokens:
|
||||
agentCfg?.contextTokens ??
|
||||
lookupContextTokens(model) ??
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
contextTokens: agentCfg?.contextTokens ?? lookupContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDefaultModel(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
agentId?: string;
|
||||
}): {
|
||||
export function resolveDefaultModel(params: { cfg: ClawdbotConfig; agentId?: string }): {
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
|
||||
@@ -29,11 +29,8 @@ export function maybeHandleQueueDirective(params: {
|
||||
sessionEntry: params.sessionEntry,
|
||||
});
|
||||
const debounceLabel =
|
||||
typeof settings.debounceMs === "number"
|
||||
? `${settings.debounceMs}ms`
|
||||
: "default";
|
||||
const capLabel =
|
||||
typeof settings.cap === "number" ? String(settings.cap) : "default";
|
||||
typeof settings.debounceMs === "number" ? `${settings.debounceMs}ms` : "default";
|
||||
const capLabel = typeof settings.cap === "number" ? String(settings.cap) : "default";
|
||||
const dropLabel = settings.dropPolicy ?? "default";
|
||||
return {
|
||||
text: withOptions(
|
||||
@@ -44,23 +41,13 @@ export function maybeHandleQueueDirective(params: {
|
||||
}
|
||||
|
||||
const queueModeInvalid =
|
||||
!directives.queueMode &&
|
||||
!directives.queueReset &&
|
||||
Boolean(directives.rawQueueMode);
|
||||
!directives.queueMode && !directives.queueReset && Boolean(directives.rawQueueMode);
|
||||
const queueDebounceInvalid =
|
||||
directives.rawDebounce !== undefined &&
|
||||
typeof directives.debounceMs !== "number";
|
||||
const queueCapInvalid =
|
||||
directives.rawCap !== undefined && typeof directives.cap !== "number";
|
||||
const queueDropInvalid =
|
||||
directives.rawDrop !== undefined && !directives.dropPolicy;
|
||||
directives.rawDebounce !== undefined && typeof directives.debounceMs !== "number";
|
||||
const queueCapInvalid = directives.rawCap !== undefined && typeof directives.cap !== "number";
|
||||
const queueDropInvalid = directives.rawDrop !== undefined && !directives.dropPolicy;
|
||||
|
||||
if (
|
||||
queueModeInvalid ||
|
||||
queueDebounceInvalid ||
|
||||
queueCapInvalid ||
|
||||
queueDropInvalid
|
||||
) {
|
||||
if (queueModeInvalid || queueDebounceInvalid || queueCapInvalid || queueDropInvalid) {
|
||||
const errors: string[] = [];
|
||||
if (queueModeInvalid) {
|
||||
errors.push(
|
||||
|
||||
@@ -37,9 +37,7 @@ export function formatElevatedUnavailableText(params: {
|
||||
);
|
||||
const failures = params.failures ?? [];
|
||||
if (failures.length > 0) {
|
||||
lines.push(
|
||||
`Failing gates: ${failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`,
|
||||
);
|
||||
lines.push(`Failing gates: ${failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`);
|
||||
} else {
|
||||
lines.push(
|
||||
"Fix-it keys: tools.elevated.enabled, tools.elevated.allowFrom.<provider>, agents.list[].tools.elevated.*",
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
export { applyInlineDirectivesFastLane } from "./directive-handling.fast-lane.js";
|
||||
export * from "./directive-handling.impl.js";
|
||||
export type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
export {
|
||||
isDirectiveOnly,
|
||||
parseInlineDirectives,
|
||||
} from "./directive-handling.parse.js";
|
||||
export {
|
||||
persistInlineDirectives,
|
||||
resolveDefaultModel,
|
||||
} from "./directive-handling.persist.js";
|
||||
export { isDirectiveOnly, parseInlineDirectives } from "./directive-handling.parse.js";
|
||||
export { persistInlineDirectives, resolveDefaultModel } from "./directive-handling.persist.js";
|
||||
export { formatDirectiveAck } from "./directive-handling.shared.js";
|
||||
|
||||
@@ -16,17 +16,14 @@ type ExtractedLevel<T> = {
|
||||
hasDirective: boolean;
|
||||
};
|
||||
|
||||
const escapeRegExp = (value: string) =>
|
||||
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
const matchLevelDirective = (
|
||||
body: string,
|
||||
names: string[],
|
||||
): { start: number; end: number; rawLevel?: string } | null => {
|
||||
const namePattern = names.map(escapeRegExp).join("|");
|
||||
const match = body.match(
|
||||
new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i"),
|
||||
);
|
||||
const match = body.match(new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i"));
|
||||
if (!match || match.index === undefined) return null;
|
||||
const start = match.index;
|
||||
let end = match.index + match[0].length;
|
||||
@@ -76,9 +73,7 @@ const extractSimpleDirective = (
|
||||
const match = body.match(
|
||||
new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)(?:\\s*:\\s*)?`, "i"),
|
||||
);
|
||||
const cleaned = match
|
||||
? body.replace(match[0], " ").replace(/\s+/g, " ").trim()
|
||||
: body.trim();
|
||||
const cleaned = match ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() : body.trim();
|
||||
return {
|
||||
cleaned,
|
||||
hasDirective: Boolean(match),
|
||||
@@ -92,11 +87,7 @@ export function extractThinkDirective(body?: string): {
|
||||
hasDirective: boolean;
|
||||
} {
|
||||
if (!body) return { cleaned: "", hasDirective: false };
|
||||
const extracted = extractLevelDirective(
|
||||
body,
|
||||
["thinking", "think", "t"],
|
||||
normalizeThinkLevel,
|
||||
);
|
||||
const extracted = extractLevelDirective(body, ["thinking", "think", "t"], normalizeThinkLevel);
|
||||
return {
|
||||
cleaned: extracted.cleaned,
|
||||
thinkLevel: extracted.level,
|
||||
@@ -112,11 +103,7 @@ export function extractVerboseDirective(body?: string): {
|
||||
hasDirective: boolean;
|
||||
} {
|
||||
if (!body) return { cleaned: "", hasDirective: false };
|
||||
const extracted = extractLevelDirective(
|
||||
body,
|
||||
["verbose", "v"],
|
||||
normalizeVerboseLevel,
|
||||
);
|
||||
const extracted = extractLevelDirective(body, ["verbose", "v"], normalizeVerboseLevel);
|
||||
return {
|
||||
cleaned: extracted.cleaned,
|
||||
verboseLevel: extracted.level,
|
||||
@@ -132,11 +119,7 @@ export function extractElevatedDirective(body?: string): {
|
||||
hasDirective: boolean;
|
||||
} {
|
||||
if (!body) return { cleaned: "", hasDirective: false };
|
||||
const extracted = extractLevelDirective(
|
||||
body,
|
||||
["elevated", "elev"],
|
||||
normalizeElevatedLevel,
|
||||
);
|
||||
const extracted = extractLevelDirective(body, ["elevated", "elev"], normalizeElevatedLevel);
|
||||
return {
|
||||
cleaned: extracted.cleaned,
|
||||
elevatedLevel: extracted.level,
|
||||
@@ -152,11 +135,7 @@ export function extractReasoningDirective(body?: string): {
|
||||
hasDirective: boolean;
|
||||
} {
|
||||
if (!body) return { cleaned: "", hasDirective: false };
|
||||
const extracted = extractLevelDirective(
|
||||
body,
|
||||
["reasoning", "reason"],
|
||||
normalizeReasoningLevel,
|
||||
);
|
||||
const extracted = extractLevelDirective(body, ["reasoning", "reason"], normalizeReasoningLevel);
|
||||
return {
|
||||
cleaned: extracted.cleaned,
|
||||
reasoningLevel: extracted.level,
|
||||
|
||||
@@ -17,14 +17,7 @@ vi.mock("./route-reply.js", () => ({
|
||||
isRoutableChannel: (channel: string | undefined) =>
|
||||
Boolean(
|
||||
channel &&
|
||||
[
|
||||
"telegram",
|
||||
"slack",
|
||||
"discord",
|
||||
"signal",
|
||||
"imessage",
|
||||
"whatsapp",
|
||||
].includes(channel),
|
||||
["telegram", "slack", "discord", "signal", "imessage", "whatsapp"].includes(channel),
|
||||
),
|
||||
routeReply: mocks.routeReply,
|
||||
}));
|
||||
|
||||
@@ -37,9 +37,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
const originatingTo = ctx.OriginatingTo;
|
||||
const currentSurface = (ctx.Surface ?? ctx.Provider)?.toLowerCase();
|
||||
const shouldRouteToOriginating =
|
||||
isRoutableChannel(originatingChannel) &&
|
||||
originatingTo &&
|
||||
originatingChannel !== currentSurface;
|
||||
isRoutableChannel(originatingChannel) && originatingTo && originatingChannel !== currentSurface;
|
||||
|
||||
/**
|
||||
* Helper to send a payload via route-reply (async).
|
||||
@@ -66,9 +64,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
abortSignal,
|
||||
});
|
||||
if (!result.ok) {
|
||||
logVerbose(
|
||||
`dispatch-from-config: route-reply failed: ${result.error ?? "unknown error"}`,
|
||||
);
|
||||
logVerbose(`dispatch-from-config: route-reply failed: ${result.error ?? "unknown error"}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,11 +125,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
cfg,
|
||||
);
|
||||
|
||||
const replies = replyResult
|
||||
? Array.isArray(replyResult)
|
||||
? replyResult
|
||||
: [replyResult]
|
||||
: [];
|
||||
const replies = replyResult ? (Array.isArray(replyResult) ? replyResult : [replyResult]) : [];
|
||||
|
||||
let queuedFinal = false;
|
||||
let routedFinalCount = 0;
|
||||
|
||||
@@ -48,10 +48,7 @@ describe("createFollowupRunner compaction", () => {
|
||||
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: {
|
||||
onAgentEvent?: (evt: {
|
||||
stream: string;
|
||||
data: Record<string, unknown>;
|
||||
}) => void;
|
||||
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
|
||||
}) => {
|
||||
params.onAgentEvent?.({
|
||||
stream: "compaction",
|
||||
@@ -102,9 +99,7 @@ describe("createFollowupRunner compaction", () => {
|
||||
await runner(queued);
|
||||
|
||||
expect(onBlockReply).toHaveBeenCalled();
|
||||
expect(onBlockReply.mock.calls[0][0].text).toContain(
|
||||
"Auto-compaction complete",
|
||||
);
|
||||
expect(onBlockReply.mock.calls[0][0].text).toContain("Auto-compaction complete");
|
||||
expect(sessionStore.main.compactionCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,9 +103,7 @@ describe("createFollowupRunner messaging tool dedupe", () => {
|
||||
runEmbeddedPiAgentMock.mockResolvedValueOnce({
|
||||
payloads: [{ text: "hello world!" }],
|
||||
messagingToolSentTexts: ["different message"],
|
||||
messagingToolSentTargets: [
|
||||
{ tool: "slack", provider: "slack", to: "channel:C1" },
|
||||
],
|
||||
messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }],
|
||||
meta: {},
|
||||
});
|
||||
|
||||
|
||||
@@ -66,14 +66,10 @@ export function createFollowupRunner(params: {
|
||||
* session's current dispatcher. This ensures replies go back to
|
||||
* where the message originated.
|
||||
*/
|
||||
const sendFollowupPayloads = async (
|
||||
payloads: ReplyPayload[],
|
||||
queued: FollowupRun,
|
||||
) => {
|
||||
const sendFollowupPayloads = async (payloads: ReplyPayload[], queued: FollowupRun) => {
|
||||
// Check if we should route to originating channel.
|
||||
const { originatingChannel, originatingTo } = queued;
|
||||
const shouldRouteToOriginating =
|
||||
isRoutableChannel(originatingChannel) && originatingTo;
|
||||
const shouldRouteToOriginating = isRoutableChannel(originatingChannel) && originatingTo;
|
||||
|
||||
if (!shouldRouteToOriginating && !opts?.onBlockReply) {
|
||||
logVerbose("followup queue: no onBlockReply handler; dropping payloads");
|
||||
@@ -168,8 +164,7 @@ export function createFollowupRunner(params: {
|
||||
blockReplyBreak: queued.run.blockReplyBreak,
|
||||
onAgentEvent: (evt) => {
|
||||
if (evt.stream !== "compaction") return;
|
||||
const phase =
|
||||
typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||
const willRetry = Boolean(evt.data.willRetry);
|
||||
if (phase === "end" && !willRetry) {
|
||||
autoCompactionCompleted = true;
|
||||
@@ -182,9 +177,7 @@ export function createFollowupRunner(params: {
|
||||
fallbackModel = fallbackResult.model;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
defaultRuntime.error?.(
|
||||
`Followup agent failed before reply: ${message}`,
|
||||
);
|
||||
defaultRuntime.error?.(`Followup agent failed before reply: ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,16 +187,13 @@ export function createFollowupRunner(params: {
|
||||
const text = payload.text;
|
||||
if (!text || !text.includes("HEARTBEAT_OK")) return [payload];
|
||||
const stripped = stripHeartbeatToken(text, { mode: "message" });
|
||||
const hasMedia =
|
||||
Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
if (stripped.shouldSkip && !hasMedia) return [];
|
||||
return [{ ...payload, text: stripped.text }];
|
||||
});
|
||||
const replyToChannel =
|
||||
queued.originatingChannel ??
|
||||
(queued.run.messageProvider?.toLowerCase() as
|
||||
| OriginatingChannelType
|
||||
| undefined);
|
||||
(queued.run.messageProvider?.toLowerCase() as OriginatingChannelType | undefined);
|
||||
const replyToMode = resolveReplyToMode(
|
||||
queued.run.config,
|
||||
replyToChannel,
|
||||
@@ -247,8 +237,7 @@ export function createFollowupRunner(params: {
|
||||
|
||||
if (storePath && sessionKey) {
|
||||
const usage = runResult.meta.agentMeta?.usage;
|
||||
const modelUsed =
|
||||
runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||
const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||
const contextTokensUsed =
|
||||
agentCfgContextTokens ??
|
||||
lookupContextTokens(modelUsed) ??
|
||||
@@ -263,13 +252,11 @@ export function createFollowupRunner(params: {
|
||||
update: async (entry) => {
|
||||
const input = usage.input ?? 0;
|
||||
const output = usage.output ?? 0;
|
||||
const promptTokens =
|
||||
input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
const promptTokens = input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
return {
|
||||
inputTokens: input,
|
||||
outputTokens: output,
|
||||
totalTokens:
|
||||
promptTokens > 0 ? promptTokens : (usage.total ?? input),
|
||||
totalTokens: promptTokens > 0 ? promptTokens : (usage.total ?? input),
|
||||
modelProvider: fallbackProvider ?? entry.modelProvider,
|
||||
model: modelUsed,
|
||||
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
||||
@@ -278,9 +265,7 @@ export function createFollowupRunner(params: {
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`failed to persist followup usage update: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`failed to persist followup usage update: ${String(err)}`);
|
||||
}
|
||||
} else if (modelUsed || contextTokensUsed) {
|
||||
try {
|
||||
@@ -295,9 +280,7 @@ export function createFollowupRunner(params: {
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`failed to persist followup model/context update: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`failed to persist followup model/context update: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user