fix: stabilize ci checks

This commit is contained in:
Peter Steinberger
2026-01-19 00:34:16 +00:00
parent 2f6b5ffdfe
commit d9384785a3
35 changed files with 197 additions and 268 deletions

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
src/canvas-host/a2ui/a2ui.bundle.js

View File

@@ -424,14 +424,18 @@ public struct PollParams: Codable, Sendable {
public struct AgentParams: Codable, Sendable { public struct AgentParams: Codable, Sendable {
public let message: String public let message: String
public let agentid: String?
public let to: String? public let to: String?
public let replyto: String?
public let sessionid: String? public let sessionid: String?
public let sessionkey: String? public let sessionkey: String?
public let thinking: String? public let thinking: String?
public let deliver: Bool? public let deliver: Bool?
public let attachments: [AnyCodable]? public let attachments: [AnyCodable]?
public let channel: String? public let channel: String?
public let replychannel: String?
public let accountid: String? public let accountid: String?
public let replyaccountid: String?
public let timeout: Int? public let timeout: Int?
public let lane: String? public let lane: String?
public let extrasystemprompt: String? public let extrasystemprompt: String?
@@ -441,14 +445,18 @@ public struct AgentParams: Codable, Sendable {
public init( public init(
message: String, message: String,
agentid: String?,
to: String?, to: String?,
replyto: String?,
sessionid: String?, sessionid: String?,
sessionkey: String?, sessionkey: String?,
thinking: String?, thinking: String?,
deliver: Bool?, deliver: Bool?,
attachments: [AnyCodable]?, attachments: [AnyCodable]?,
channel: String?, channel: String?,
replychannel: String?,
accountid: String?, accountid: String?,
replyaccountid: String?,
timeout: Int?, timeout: Int?,
lane: String?, lane: String?,
extrasystemprompt: String?, extrasystemprompt: String?,
@@ -457,14 +465,18 @@ public struct AgentParams: Codable, Sendable {
spawnedby: String? spawnedby: String?
) { ) {
self.message = message self.message = message
self.agentid = agentid
self.to = to self.to = to
self.replyto = replyto
self.sessionid = sessionid self.sessionid = sessionid
self.sessionkey = sessionkey self.sessionkey = sessionkey
self.thinking = thinking self.thinking = thinking
self.deliver = deliver self.deliver = deliver
self.attachments = attachments self.attachments = attachments
self.channel = channel self.channel = channel
self.replychannel = replychannel
self.accountid = accountid self.accountid = accountid
self.replyaccountid = replyaccountid
self.timeout = timeout self.timeout = timeout
self.lane = lane self.lane = lane
self.extrasystemprompt = extrasystemprompt self.extrasystemprompt = extrasystemprompt
@@ -474,14 +486,18 @@ public struct AgentParams: Codable, Sendable {
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case message case message
case agentid = "agentId"
case to case to
case replyto = "replyTo"
case sessionid = "sessionId" case sessionid = "sessionId"
case sessionkey = "sessionKey" case sessionkey = "sessionKey"
case thinking case thinking
case deliver case deliver
case attachments case attachments
case channel case channel
case replychannel = "replyChannel"
case accountid = "accountId" case accountid = "accountId"
case replyaccountid = "replyAccountId"
case timeout case timeout
case lane case lane
case extrasystemprompt = "extraSystemPrompt" case extrasystemprompt = "extraSystemPrompt"

View File

@@ -22,8 +22,7 @@ import { getApiKeyForModel } from "./model-auth.js";
import { normalizeProviderId, parseModelRef } from "./model-selection.js"; import { normalizeProviderId, parseModelRef } from "./model-selection.js";
import { ensureClawdbotModelsJson } from "./models-config.js"; import { ensureClawdbotModelsJson } from "./models-config.js";
const LIVE = const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
const SETUP_TOKEN_RAW = process.env.CLAWDBOT_LIVE_SETUP_TOKEN?.trim() ?? ""; const SETUP_TOKEN_RAW = process.env.CLAWDBOT_LIVE_SETUP_TOKEN?.trim() ?? "";
const SETUP_TOKEN_VALUE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_VALUE?.trim() ?? ""; const SETUP_TOKEN_VALUE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_VALUE?.trim() ?? "";
const SETUP_TOKEN_PROFILE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? ""; const SETUP_TOKEN_PROFILE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? "";

View File

@@ -22,11 +22,7 @@ vi.mock("../config/config.js", async (importOriginal) => {
import { createClawdbotTools } from "./clawdbot-tools.js"; import { createClawdbotTools } from "./clawdbot-tools.js";
const waitForCalls = async ( const waitForCalls = async (getCount: () => number, count: number, timeoutMs = 2000) => {
getCount: () => number,
count: number,
timeoutMs = 2000,
) => {
const start = Date.now(); const start = Date.now();
while (getCount() < count) { while (getCount() < count) {
if (Date.now() - start > timeoutMs) { if (Date.now() - start > timeoutMs) {
@@ -254,18 +250,9 @@ describe("sessions tools", () => {
runId: "run-1", runId: "run-1",
delivery: { status: "pending", mode: "announce" }, delivery: { status: "pending", mode: "announce" },
}); });
await waitForCalls( await waitForCalls(() => calls.filter((call) => call.method === "agent").length, 4);
() => calls.filter((call) => call.method === "agent").length, await waitForCalls(() => calls.filter((call) => call.method === "agent.wait").length, 4);
4, await waitForCalls(() => calls.filter((call) => call.method === "chat.history").length, 4);
);
await waitForCalls(
() => calls.filter((call) => call.method === "agent.wait").length,
4,
);
await waitForCalls(
() => calls.filter((call) => call.method === "chat.history").length,
4,
);
const waitPromise = tool.execute("call6", { const waitPromise = tool.execute("call6", {
sessionKey: "main", sessionKey: "main",
@@ -279,18 +266,9 @@ describe("sessions tools", () => {
delivery: { status: "pending", mode: "announce" }, delivery: { status: "pending", mode: "announce" },
}); });
expect(typeof (waited.details as { runId?: string }).runId).toBe("string"); expect(typeof (waited.details as { runId?: string }).runId).toBe("string");
await waitForCalls( await waitForCalls(() => calls.filter((call) => call.method === "agent").length, 8);
() => calls.filter((call) => call.method === "agent").length, await waitForCalls(() => calls.filter((call) => call.method === "agent.wait").length, 8);
8, await waitForCalls(() => calls.filter((call) => call.method === "chat.history").length, 8);
);
await waitForCalls(
() => calls.filter((call) => call.method === "agent.wait").length,
8,
);
await waitForCalls(
() => calls.filter((call) => call.method === "chat.history").length,
8,
);
const agentCalls = calls.filter((call) => call.method === "agent"); const agentCalls = calls.filter((call) => call.method === "agent");
const waitCalls = calls.filter((call) => call.method === "agent.wait"); const waitCalls = calls.filter((call) => call.method === "agent.wait");

View File

@@ -3,8 +3,7 @@ import { describe, expect, it } from "vitest";
import { isTruthyEnvValue } from "../infra/env.js"; import { isTruthyEnvValue } from "../infra/env.js";
const GEMINI_KEY = process.env.GEMINI_API_KEY ?? ""; const GEMINI_KEY = process.env.GEMINI_API_KEY ?? "";
const LIVE = const LIVE = isTruthyEnvValue(process.env.GEMINI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
isTruthyEnvValue(process.env.GEMINI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
const describeLive = LIVE && GEMINI_KEY ? describe : describe.skip; const describeLive = LIVE && GEMINI_KEY ? describe : describe.skip;

View File

@@ -5,8 +5,7 @@ import { isTruthyEnvValue } from "../infra/env.js";
const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? ""; const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? "";
const MINIMAX_BASE_URL = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic"; const MINIMAX_BASE_URL = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic";
const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1"; const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1";
const LIVE = const LIVE = isTruthyEnvValue(process.env.MINIMAX_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
isTruthyEnvValue(process.env.MINIMAX_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
const describeLive = LIVE && MINIMAX_KEY ? describe : describe.skip; const describeLive = LIVE && MINIMAX_KEY ? describe : describe.skip;

View File

@@ -15,8 +15,7 @@ import { getApiKeyForModel } from "./model-auth.js";
import { ensureClawdbotModelsJson } from "./models-config.js"; import { ensureClawdbotModelsJson } from "./models-config.js";
import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js"; import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js";
const LIVE = const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
const DIRECT_ENABLED = Boolean(process.env.CLAWDBOT_LIVE_MODELS?.trim()); const DIRECT_ENABLED = Boolean(process.env.CLAWDBOT_LIVE_MODELS?.trim());
const REQUIRE_PROFILE_KEYS = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_REQUIRE_PROFILE_KEYS); const REQUIRE_PROFILE_KEYS = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_REQUIRE_PROFILE_KEYS);

View File

@@ -6,8 +6,7 @@ import type { ClawdbotConfig } from "../config/config.js";
import { applyExtraParamsToAgent } from "./pi-embedded-runner.js"; import { applyExtraParamsToAgent } from "./pi-embedded-runner.js";
const OPENAI_KEY = process.env.OPENAI_API_KEY ?? ""; const OPENAI_KEY = process.env.OPENAI_API_KEY ?? "";
const LIVE = const LIVE = isTruthyEnvValue(process.env.OPENAI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
isTruthyEnvValue(process.env.OPENAI_LIVE_TEST) || isTruthyEnvValue(process.env.LIVE);
const describeLive = LIVE && OPENAI_KEY ? describe : describe.skip; const describeLive = LIVE && OPENAI_KEY ? describe : describe.skip;

View File

@@ -18,10 +18,7 @@ import {
updateSessionStore, updateSessionStore,
} from "../../config/sessions.js"; } from "../../config/sessions.js";
import { logVerbose } from "../../globals.js"; import { logVerbose } from "../../globals.js";
import { import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
emitAgentEvent,
registerAgentRunContext,
} from "../../infra/agent-events.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { import {
isMarkdownCapableMessageChannel, isMarkdownCapableMessageChannel,
@@ -32,20 +29,11 @@ import type { TemplateContext } from "../templating.js";
import type { VerboseLevel } from "../thinking.js"; import type { VerboseLevel } from "../thinking.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js"; import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js";
import { import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js";
buildThreadingToolContext, import { createBlockReplyPayloadKey, type BlockReplyPipeline } from "./block-reply-pipeline.js";
resolveEnforceFinalTag,
} from "./agent-runner-utils.js";
import {
createBlockReplyPayloadKey,
type BlockReplyPipeline,
} from "./block-reply-pipeline.js";
import type { FollowupRun } from "./queue.js"; import type { FollowupRun } from "./queue.js";
import { parseReplyDirectives } from "./reply-directives.js"; import { parseReplyDirectives } from "./reply-directives.js";
import { import { applyReplyTagsToPayload, isRenderablePayload } from "./reply-payloads.js";
applyReplyTagsToPayload,
isRenderablePayload,
} from "./reply-payloads.js";
import type { TypingSignaler } from "./typing-mode.js"; import type { TypingSignaler } from "./typing-mode.js";
export type AgentRunLoopResult = export type AgentRunLoopResult =
@@ -108,12 +96,9 @@ export async function runAgentTurnWithFallback(params: {
while (true) { while (true) {
try { try {
const allowPartialStream = !( const allowPartialStream = !(
params.followupRun.run.reasoningLevel === "stream" && params.followupRun.run.reasoningLevel === "stream" && params.opts?.onReasoningStream
params.opts?.onReasoningStream
); );
const normalizeStreamingText = ( const normalizeStreamingText = (payload: ReplyPayload): { text?: string; skip: boolean } => {
payload: ReplyPayload,
): { text?: string; skip: boolean } => {
if (!allowPartialStream) return { skip: true }; if (!allowPartialStream) return { skip: true };
let text = payload.text; let text = payload.text;
if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) { if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) {
@@ -137,9 +122,7 @@ export async function runAgentTurnWithFallback(params: {
if (!sanitized.trim()) return { skip: true }; if (!sanitized.trim()) return { skip: true };
return { text: sanitized, skip: false }; return { text: sanitized, skip: false };
}; };
const handlePartialForTyping = async ( const handlePartialForTyping = async (payload: ReplyPayload): Promise<string | undefined> => {
payload: ReplyPayload,
): Promise<string | undefined> => {
const { text, skip } = normalizeStreamingText(payload); const { text, skip } = normalizeStreamingText(payload);
if (skip || !text) return undefined; if (skip || !text) return undefined;
await params.typingSignals.signalTextDelta(text); await params.typingSignals.signalTextDelta(text);
@@ -174,10 +157,7 @@ export async function runAgentTurnWithFallback(params: {
startedAt, startedAt,
}, },
}); });
const cliSessionId = getCliSessionId( const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
params.getActiveSessionEntry(),
provider,
);
return runCliAgent({ return runCliAgent({
sessionId: params.followupRun.run.sessionId, sessionId: params.followupRun.run.sessionId,
sessionKey: params.sessionKey, sessionKey: params.sessionKey,
@@ -227,8 +207,7 @@ export async function runAgentTurnWithFallback(params: {
return runEmbeddedPiAgent({ return runEmbeddedPiAgent({
sessionId: params.followupRun.run.sessionId, sessionId: params.followupRun.run.sessionId,
sessionKey: params.sessionKey, sessionKey: params.sessionKey,
messageProvider: messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
params.sessionCtx.Provider?.trim().toLowerCase() || undefined,
agentAccountId: params.sessionCtx.AccountId, agentAccountId: params.sessionCtx.AccountId,
// Provider threading context for tool auto-injection // Provider threading context for tool auto-injection
...buildThreadingToolContext({ ...buildThreadingToolContext({
@@ -244,10 +223,7 @@ export async function runAgentTurnWithFallback(params: {
prompt: params.commandBody, prompt: params.commandBody,
extraSystemPrompt: params.followupRun.run.extraSystemPrompt, extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
ownerNumbers: params.followupRun.run.ownerNumbers, ownerNumbers: params.followupRun.run.ownerNumbers,
enforceFinalTag: resolveEnforceFinalTag( enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
params.followupRun.run,
provider,
),
provider, provider,
model, model,
authProfileId, authProfileId,
@@ -264,9 +240,7 @@ export async function runAgentTurnWithFallback(params: {
params.sessionCtx.Provider, params.sessionCtx.Provider,
); );
if (!channel) return "markdown"; if (!channel) return "markdown";
return isMarkdownCapableMessageChannel(channel) return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
? "markdown"
: "plain";
})(), })(),
bashElevated: params.followupRun.run.bashElevated, bashElevated: params.followupRun.run.bashElevated,
timeoutMs: params.followupRun.run.timeoutMs, timeoutMs: params.followupRun.run.timeoutMs,
@@ -276,11 +250,7 @@ export async function runAgentTurnWithFallback(params: {
onPartialReply: allowPartialStream onPartialReply: allowPartialStream
? async (payload) => { ? async (payload) => {
const textForTyping = await handlePartialForTyping(payload); const textForTyping = await handlePartialForTyping(payload);
if ( if (!params.opts?.onPartialReply || textForTyping === undefined) return;
!params.opts?.onPartialReply ||
textForTyping === undefined
)
return;
await params.opts.onPartialReply({ await params.opts.onPartialReply({
text: textForTyping, text: textForTyping,
mediaUrls: payload.mediaUrls, mediaUrls: payload.mediaUrls,
@@ -291,8 +261,7 @@ export async function runAgentTurnWithFallback(params: {
await params.typingSignals.signalMessageStart(); await params.typingSignals.signalMessageStart();
}, },
onReasoningStream: onReasoningStream:
params.typingSignals.shouldStartOnReasoning || params.typingSignals.shouldStartOnReasoning || params.opts?.onReasoningStream
params.opts?.onReasoningStream
? async (payload) => { ? async (payload) => {
await params.typingSignals.signalReasoningDelta(); await params.typingSignals.signalReasoningDelta();
await params.opts?.onReasoningStream?.({ await params.opts?.onReasoningStream?.({
@@ -305,16 +274,14 @@ export async function runAgentTurnWithFallback(params: {
// Trigger typing when tools start executing. // Trigger typing when tools start executing.
// Must await to ensure typing indicator starts before tool summaries are emitted. // Must await to ensure typing indicator starts before tool summaries are emitted.
if (evt.stream === "tool") { if (evt.stream === "tool") {
const phase = const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
typeof evt.data.phase === "string" ? evt.data.phase : "";
if (phase === "start" || phase === "update") { if (phase === "start" || phase === "update") {
await params.typingSignals.signalToolStart(); await params.typingSignals.signalToolStart();
} }
} }
// Track auto-compaction completion // Track auto-compaction completion
if (evt.stream === "compaction") { if (evt.stream === "compaction") {
const phase = const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
typeof evt.data.phase === "string" ? evt.data.phase : "";
const willRetry = Boolean(evt.data.willRetry); const willRetry = Boolean(evt.data.willRetry);
if (phase === "end" && !willRetry) { if (phase === "end" && !willRetry) {
autoCompactionCompleted = true; autoCompactionCompleted = true;
@@ -338,22 +305,14 @@ export async function runAgentTurnWithFallback(params: {
params.sessionCtx.MessageSid, params.sessionCtx.MessageSid,
); );
// Let through payloads with audioAsVoice flag even if empty (need to track it) // Let through payloads with audioAsVoice flag even if empty (need to track it)
if ( if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice) return;
!isRenderablePayload(taggedPayload) && const parsed = parseReplyDirectives(taggedPayload.text ?? "", {
!payload.audioAsVoice currentMessageId: params.sessionCtx.MessageSid,
) silentToken: SILENT_REPLY_TOKEN,
return; });
const parsed = parseReplyDirectives(
taggedPayload.text ?? "",
{
currentMessageId: params.sessionCtx.MessageSid,
silentToken: SILENT_REPLY_TOKEN,
},
);
const cleaned = parsed.text || undefined; const cleaned = parsed.text || undefined;
const hasRenderableMedia = const hasRenderableMedia =
Boolean(taggedPayload.mediaUrl) || Boolean(taggedPayload.mediaUrl) || (taggedPayload.mediaUrls?.length ?? 0) > 0;
(taggedPayload.mediaUrls?.length ?? 0) > 0;
// Skip empty payloads unless they have audioAsVoice flag (need to track it) // Skip empty payloads unless they have audioAsVoice flag (need to track it)
if ( if (
!cleaned && !cleaned &&
@@ -367,35 +326,25 @@ export async function runAgentTurnWithFallback(params: {
const blockPayload: ReplyPayload = params.applyReplyToMode({ const blockPayload: ReplyPayload = params.applyReplyToMode({
...taggedPayload, ...taggedPayload,
text: cleaned, text: cleaned,
audioAsVoice: Boolean( audioAsVoice: Boolean(parsed.audioAsVoice || payload.audioAsVoice),
parsed.audioAsVoice || payload.audioAsVoice,
),
replyToId: taggedPayload.replyToId ?? parsed.replyToId, replyToId: taggedPayload.replyToId ?? parsed.replyToId,
replyToTag: taggedPayload.replyToTag || parsed.replyToTag, replyToTag: taggedPayload.replyToTag || parsed.replyToTag,
replyToCurrent: replyToCurrent: taggedPayload.replyToCurrent || parsed.replyToCurrent,
taggedPayload.replyToCurrent || parsed.replyToCurrent,
}); });
void params.typingSignals void params.typingSignals
.signalTextDelta(cleaned ?? taggedPayload.text) .signalTextDelta(cleaned ?? taggedPayload.text)
.catch((err) => { .catch((err) => {
logVerbose( logVerbose(`block reply typing signal failed: ${String(err)}`);
`block reply typing signal failed: ${String(err)}`,
);
}); });
// Use pipeline if available (block streaming enabled), otherwise send directly // Use pipeline if available (block streaming enabled), otherwise send directly
if ( if (params.blockStreamingEnabled && params.blockReplyPipeline) {
params.blockStreamingEnabled &&
params.blockReplyPipeline
) {
params.blockReplyPipeline.enqueue(blockPayload); params.blockReplyPipeline.enqueue(blockPayload);
} else { } else {
// Send directly when flushing before tool execution (no streaming). // Send directly when flushing before tool execution (no streaming).
// Track sent key to avoid duplicate in final payloads. // Track sent key to avoid duplicate in final payloads.
directlySentBlockKeys.add( directlySentBlockKeys.add(createBlockReplyPayloadKey(blockPayload));
createBlockReplyPayloadKey(blockPayload),
);
await params.opts?.onBlockReply?.(blockPayload); await params.opts?.onBlockReply?.(blockPayload);
} }
} }
@@ -456,9 +405,7 @@ export async function runAgentTurnWithFallback(params: {
}; };
} }
if (embeddedError?.kind === "role_ordering") { if (embeddedError?.kind === "role_ordering") {
const didReset = await params.resetSessionAfterRoleOrderingConflict( const didReset = await params.resetSessionAfterRoleOrderingConflict(embeddedError.message);
embeddedError.message,
);
if (didReset) { if (didReset) {
return { return {
kind: "final", kind: "final",
@@ -476,10 +423,8 @@ export async function runAgentTurnWithFallback(params: {
isContextOverflowError(message) || isContextOverflowError(message) ||
/context.*overflow|too large|context window/i.test(message); /context.*overflow|too large|context window/i.test(message);
const isCompactionFailure = isCompactionFailureError(message); const isCompactionFailure = isCompactionFailureError(message);
const isSessionCorruption = const isSessionCorruption = /function call turn comes immediately after/i.test(message);
/function call turn comes immediately after/i.test(message); const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
const isRoleOrderingError =
/incorrect role information|roles must alternate/i.test(message);
if ( if (
isCompactionFailure && isCompactionFailure &&
@@ -495,8 +440,7 @@ export async function runAgentTurnWithFallback(params: {
}; };
} }
if (isRoleOrderingError) { if (isRoleOrderingError) {
const didReset = const didReset = await params.resetSessionAfterRoleOrderingConflict(message);
await params.resetSessionAfterRoleOrderingConflict(message);
if (didReset) { if (didReset) {
return { return {
kind: "final", kind: "final",
@@ -523,8 +467,7 @@ export async function runAgentTurnWithFallback(params: {
try { try {
// Delete transcript file if it exists // Delete transcript file if it exists
if (corruptedSessionId) { if (corruptedSessionId) {
const transcriptPath = const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId);
resolveSessionTranscriptPath(corruptedSessionId);
try { try {
fs.unlinkSync(transcriptPath); fs.unlinkSync(transcriptPath);
} catch { } catch {
@@ -574,7 +517,6 @@ export async function runAgentTurnWithFallback(params: {
fallbackModel, fallbackModel,
didLogHeartbeatStrip, didLogHeartbeatStrip,
autoCompactionCompleted, autoCompactionCompleted,
directlySentBlockKeys: directlySentBlockKeys: directlySentBlockKeys.size > 0 ? directlySentBlockKeys : undefined,
directlySentBlockKeys.size > 0 ? directlySentBlockKeys : undefined,
}; };
} }

View File

@@ -196,8 +196,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
durationMs: 1, durationMs: 1,
error: { error: {
kind: "context_overflow", kind: "context_overflow",
message: message: 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
}, },
}, },
})); }));

View File

@@ -1,8 +1,7 @@
import { describe, it } from "vitest"; import { describe, it } from "vitest";
import { isTruthyEnvValue } from "../infra/env.js"; import { isTruthyEnvValue } from "../infra/env.js";
const LIVE = const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
const CDP_URL = process.env.CLAWDBOT_LIVE_BROWSER_CDP_URL?.trim() || ""; const CDP_URL = process.env.CLAWDBOT_LIVE_BROWSER_CDP_URL?.trim() || "";
const describeLive = LIVE && CDP_URL ? describe : describe.skip; const describeLive = LIVE && CDP_URL ? describe : describe.skip;

View File

@@ -180,7 +180,7 @@ async function promptDiscordAllowFrom(params: {
}): Promise<ClawdbotConfig> { }): Promise<ClawdbotConfig> {
const accountId = const accountId =
params.accountId && normalizeAccountId(params.accountId) params.accountId && normalizeAccountId(params.accountId)
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID ? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
: resolveDefaultDiscordAccountId(params.cfg); : resolveDefaultDiscordAccountId(params.cfg);
const resolved = resolveDiscordAccount({ cfg: params.cfg, accountId }); const resolved = resolveDiscordAccount({ cfg: params.cfg, accountId });
const token = resolved.token; const token = resolved.token;
@@ -249,9 +249,7 @@ async function promptDiscordAllowFrom(params: {
continue; continue;
} }
const ids = results.map((res) => res.id as string); const ids = results.map((res) => res.id as string);
const unique = [ const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids])];
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
];
return setDiscordAllowFrom(params.cfg, unique); return setDiscordAllowFrom(params.cfg, unique);
} }
} }

View File

@@ -80,7 +80,7 @@ async function promptIMessageAllowFrom(params: {
}): Promise<ClawdbotConfig> { }): Promise<ClawdbotConfig> {
const accountId = const accountId =
params.accountId && normalizeAccountId(params.accountId) params.accountId && normalizeAccountId(params.accountId)
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID ? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
: resolveDefaultIMessageAccountId(params.cfg); : resolveDefaultIMessageAccountId(params.cfg);
const resolved = resolveIMessageAccount({ cfg: params.cfg, accountId }); const resolved = resolveIMessageAccount({ cfg: params.cfg, accountId });
const existing = resolved.config.allowFrom ?? []; const existing = resolved.config.allowFrom ?? [];

View File

@@ -85,7 +85,7 @@ async function promptSignalAllowFrom(params: {
}): Promise<ClawdbotConfig> { }): Promise<ClawdbotConfig> {
const accountId = const accountId =
params.accountId && normalizeAccountId(params.accountId) params.accountId && normalizeAccountId(params.accountId)
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID ? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
: resolveDefaultSignalAccountId(params.cfg); : resolveDefaultSignalAccountId(params.cfg);
const resolved = resolveSignalAccount({ cfg: params.cfg, accountId }); const resolved = resolveSignalAccount({ cfg: params.cfg, accountId });
const existing = resolved.config.allowFrom ?? []; const existing = resolved.config.allowFrom ?? [];

View File

@@ -232,7 +232,7 @@ async function promptSlackAllowFrom(params: {
}): Promise<ClawdbotConfig> { }): Promise<ClawdbotConfig> {
const accountId = const accountId =
params.accountId && normalizeAccountId(params.accountId) params.accountId && normalizeAccountId(params.accountId)
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID ? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
: resolveDefaultSlackAccountId(params.cfg); : resolveDefaultSlackAccountId(params.cfg);
const resolved = resolveSlackAccount({ cfg: params.cfg, accountId }); const resolved = resolveSlackAccount({ cfg: params.cfg, accountId });
const token = resolved.config.userToken ?? resolved.config.botToken ?? ""; const token = resolved.config.userToken ?? resolved.config.botToken ?? "";
@@ -299,9 +299,7 @@ async function promptSlackAllowFrom(params: {
continue; continue;
} }
const ids = results.map((res) => res.id as string); const ids = results.map((res) => res.id as string);
const unique = [ const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids])];
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
];
return setSlackAllowFrom(params.cfg, unique); return setSlackAllowFrom(params.cfg, unique);
} }
} }

View File

@@ -80,9 +80,10 @@ async function promptTelegramAllowFrom(params: {
const username = stripped.startsWith("@") ? stripped : `@${stripped}`; const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`; const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
const res = await fetch(url); const res = await fetch(url);
const data = (await res.json().catch(() => null)) as const data = (await res.json().catch(() => null)) as {
| { ok?: boolean; result?: { id?: number | string } } ok?: boolean;
| null; result?: { id?: number | string };
} | null;
const id = data?.ok ? data?.result?.id : undefined; const id = data?.ok ? data?.result?.id : undefined;
if (typeof id === "number" || typeof id === "string") return String(id); if (typeof id === "number" || typeof id === "string") return String(id);
return null; return null;
@@ -164,7 +165,7 @@ async function promptTelegramAllowFromForAccount(params: {
}): Promise<ClawdbotConfig> { }): Promise<ClawdbotConfig> {
const accountId = const accountId =
params.accountId && normalizeAccountId(params.accountId) params.accountId && normalizeAccountId(params.accountId)
? normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID ? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
: resolveDefaultTelegramAccountId(params.cfg); : resolveDefaultTelegramAccountId(params.cfg);
return promptTelegramAllowFrom({ return promptTelegramAllowFrom({
cfg: params.cfg, cfg: params.cfg,

View File

@@ -17,16 +17,9 @@ describe("argv helpers", () => {
}); });
it("extracts command path ignoring flags and terminator", () => { it("extracts command path ignoring flags and terminator", () => {
expect(getCommandPath(["node", "clawdbot", "status", "--json"], 2)).toEqual([ expect(getCommandPath(["node", "clawdbot", "status", "--json"], 2)).toEqual(["status"]);
"status", expect(getCommandPath(["node", "clawdbot", "agents", "list"], 2)).toEqual(["agents", "list"]);
]); expect(getCommandPath(["node", "clawdbot", "status", "--", "ignored"], 2)).toEqual(["status"]);
expect(getCommandPath(["node", "clawdbot", "agents", "list"], 2)).toEqual([
"agents",
"list",
]);
expect(getCommandPath(["node", "clawdbot", "status", "--", "ignored"], 2)).toEqual([
"status",
]);
}); });
it("returns primary command", () => { it("returns primary command", () => {

View File

@@ -87,37 +87,34 @@ describe("gateway SIGTERM", () => {
const out: string[] = []; const out: string[] = [];
const err: string[] = []; const err: string[] = [];
const bunBin = process.env.BUN_INSTALL const nodeBin = process.execPath;
? path.join(process.env.BUN_INSTALL, "bin", "bun") const args = [
: "bun"; "--import",
"tsx",
"src/entry.ts",
"gateway",
"--port",
String(port),
"--bind",
"loopback",
"--allow-unconfigured",
];
child = spawn( child = spawn(nodeBin, args, {
bunBin, cwd: process.cwd(),
[ env: {
"src/entry.ts", ...process.env,
"gateway", CLAWDBOT_STATE_DIR: stateDir,
"--port", CLAWDBOT_CONFIG_PATH: configPath,
String(port), CLAWDBOT_SKIP_CHANNELS: "1",
"--bind", CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
"loopback", CLAWDBOT_SKIP_CANVAS_HOST: "1",
"--allow-unconfigured", // Avoid port collisions with other test processes that may also start a bridge server.
], CLAWDBOT_BRIDGE_HOST: "127.0.0.1",
{ CLAWDBOT_BRIDGE_PORT: "0",
cwd: process.cwd(),
env: {
...process.env,
CLAWDBOT_STATE_DIR: stateDir,
CLAWDBOT_CONFIG_PATH: configPath,
CLAWDBOT_SKIP_CHANNELS: "1",
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
CLAWDBOT_SKIP_CANVAS_HOST: "1",
// Avoid port collisions with other test processes that may also start a bridge server.
CLAWDBOT_BRIDGE_HOST: "127.0.0.1",
CLAWDBOT_BRIDGE_PORT: "0",
},
stdio: ["ignore", "pipe", "pipe"],
}, },
); stdio: ["ignore", "pipe", "pipe"],
});
const proc = child; const proc = child;
if (!proc) throw new Error("failed to spawn gateway"); if (!proc) throw new Error("failed to spawn gateway");

View File

@@ -149,9 +149,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
`(requested: ${status.requestedProvider})`, `(requested: ${status.requestedProvider})`,
)}`, )}`,
`${label("Model")} ${info(status.model)}`, `${label("Model")} ${info(status.model)}`,
status.sources?.length status.sources?.length ? `${label("Sources")} ${info(status.sources.join(", "))}` : null,
? `${label("Sources")} ${info(status.sources.join(", "))}`
: null,
`${label("Indexed")} ${success(`${status.files} files · ${status.chunks} chunks`)}`, `${label("Indexed")} ${success(`${status.files} files · ${status.chunks} chunks`)}`,
`${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`, `${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`,
`${label("Store")} ${info(status.dbPath)}`, `${label("Store")} ${info(status.dbPath)}`,
@@ -222,9 +220,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
status.cache.enabled && typeof status.cache.entries === "number" status.cache.enabled && typeof status.cache.entries === "number"
? ` (${status.cache.entries} entries)` ? ` (${status.cache.entries} entries)`
: ""; : "";
lines.push( lines.push(`${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState)}${suffix}`);
`${label("Embedding cache")} ${colorize(rich, cacheColor, cacheState)}${suffix}`,
);
if (status.cache.enabled && typeof status.cache.maxEntries === "number") { if (status.cache.enabled && typeof status.cache.maxEntries === "number") {
lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`); lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`);
} }

View File

@@ -33,11 +33,7 @@ export function registerAgentCommands(program: Command, args: { agentChannelOpti
"Run the embedded agent locally (requires model provider API keys in your shell)", "Run the embedded agent locally (requires model provider API keys in your shell)",
false, false,
) )
.option( .option("--deliver", "Send the agent's reply back to the selected channel", false)
"--deliver",
"Send the agent's reply back to the selected channel",
false,
)
.option("--json", "Output result as JSON", false) .option("--json", "Output result as JSON", false)
.option( .option(
"--timeout <seconds>", "--timeout <seconds>",

View File

@@ -78,4 +78,4 @@ describe("registerSubCliCommands", () => {
expect(registerNodesCli).toHaveBeenCalledTimes(1); expect(registerNodesCli).toHaveBeenCalledTimes(1);
expect(nodesAction).toHaveBeenCalledTimes(1); expect(nodesAction).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@@ -51,7 +51,6 @@ export type AgentCliOpts = {
local?: boolean; local?: boolean;
}; };
function parseTimeoutSeconds(opts: { cfg: ReturnType<typeof loadConfig>; timeout?: string }) { function parseTimeoutSeconds(opts: { cfg: ReturnType<typeof loadConfig>; timeout?: string }) {
const raw = const raw =
opts.timeout !== undefined opts.timeout !== undefined

View File

@@ -586,13 +586,11 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
// module scope. `CLAWDBOT_CONFIG_PATH` (and friends) are expected to work even // module scope. `CLAWDBOT_CONFIG_PATH` (and friends) are expected to work even
// when set after the module has been imported (tests, one-off scripts, etc.). // when set after the module has been imported (tests, one-off scripts, etc.).
const DEFAULT_CONFIG_CACHE_MS = 200; const DEFAULT_CONFIG_CACHE_MS = 200;
let configCache: let configCache: {
| { configPath: string;
configPath: string; expiresAt: number;
expiresAt: number; config: ClawdbotConfig;
config: ClawdbotConfig; } | null = null;
}
| null = null;
function resolveConfigCacheMs(env: NodeJS.ProcessEnv): number { function resolveConfigCacheMs(env: NodeJS.ProcessEnv): number {
const raw = env.CLAWDBOT_CONFIG_CACHE_MS?.trim(); const raw = env.CLAWDBOT_CONFIG_CACHE_MS?.trim();

View File

@@ -12,8 +12,7 @@ import { GatewayClient } from "./client.js";
import { renderCatNoncePngBase64 } from "./live-image-probe.js"; import { renderCatNoncePngBase64 } from "./live-image-probe.js";
import { startGatewayServer } from "./server.js"; import { startGatewayServer } from "./server.js";
const LIVE = const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
const CLI_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND); const CLI_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND);
const CLI_IMAGE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_PROBE); const CLI_IMAGE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_PROBE);
const CLI_RESUME = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_RESUME_PROBE); const CLI_RESUME = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_CLI_BACKEND_RESUME_PROBE);

View File

@@ -31,8 +31,7 @@ import { GatewayClient } from "./client.js";
import { renderCatNoncePngBase64 } from "./live-image-probe.js"; import { renderCatNoncePngBase64 } from "./live-image-probe.js";
import { startGatewayServer } from "./server.js"; import { startGatewayServer } from "./server.js";
const LIVE = const LIVE = isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
isTruthyEnvValue(process.env.LIVE) || isTruthyEnvValue(process.env.CLAWDBOT_LIVE_TEST);
const GATEWAY_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY); const GATEWAY_LIVE = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY);
const ZAI_FALLBACK = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY_ZAI_FALLBACK); const ZAI_FALLBACK = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY_ZAI_FALLBACK);
const PROVIDERS = parseFilter(process.env.CLAWDBOT_LIVE_GATEWAY_PROVIDERS); const PROVIDERS = parseFilter(process.env.CLAWDBOT_LIVE_GATEWAY_PROVIDERS);

View File

@@ -427,11 +427,15 @@ vi.mock("../config/config.js", async () => {
} }
const fileAgents = const fileAgents =
fileConfig.agents && typeof fileConfig.agents === "object" && !Array.isArray(fileConfig.agents) fileConfig.agents &&
typeof fileConfig.agents === "object" &&
!Array.isArray(fileConfig.agents)
? (fileConfig.agents as Record<string, unknown>) ? (fileConfig.agents as Record<string, unknown>)
: {}; : {};
const fileDefaults = const fileDefaults =
fileAgents.defaults && typeof fileAgents.defaults === "object" && !Array.isArray(fileAgents.defaults) fileAgents.defaults &&
typeof fileAgents.defaults === "object" &&
!Array.isArray(fileAgents.defaults)
? (fileAgents.defaults as Record<string, unknown>) ? (fileAgents.defaults as Record<string, unknown>)
: {}; : {};
const defaults = { const defaults = {
@@ -449,7 +453,9 @@ vi.mock("../config/config.js", async () => {
: undefined; : undefined;
const fileChannels = const fileChannels =
fileConfig.channels && typeof fileConfig.channels === "object" && !Array.isArray(fileConfig.channels) fileConfig.channels &&
typeof fileConfig.channels === "object" &&
!Array.isArray(fileConfig.channels)
? ({ ...(fileConfig.channels as Record<string, unknown>) } as Record<string, unknown>) ? ({ ...(fileConfig.channels as Record<string, unknown>) } as Record<string, unknown>)
: {}; : {};
const overrideChannels = const overrideChannels =
@@ -459,7 +465,9 @@ vi.mock("../config/config.js", async () => {
const mergedChannels = { ...fileChannels, ...overrideChannels }; const mergedChannels = { ...fileChannels, ...overrideChannels };
if (testState.allowFrom !== undefined) { if (testState.allowFrom !== undefined) {
const existing = const existing =
mergedChannels.whatsapp && typeof mergedChannels.whatsapp === "object" && !Array.isArray(mergedChannels.whatsapp) mergedChannels.whatsapp &&
typeof mergedChannels.whatsapp === "object" &&
!Array.isArray(mergedChannels.whatsapp)
? (mergedChannels.whatsapp as Record<string, unknown>) ? (mergedChannels.whatsapp as Record<string, unknown>)
: {}; : {};
mergedChannels.whatsapp = { mergedChannels.whatsapp = {
@@ -470,18 +478,23 @@ vi.mock("../config/config.js", async () => {
const channels = Object.keys(mergedChannels).length > 0 ? mergedChannels : undefined; const channels = Object.keys(mergedChannels).length > 0 ? mergedChannels : undefined;
const fileSession = const fileSession =
fileConfig.session && typeof fileConfig.session === "object" && !Array.isArray(fileConfig.session) fileConfig.session &&
typeof fileConfig.session === "object" &&
!Array.isArray(fileConfig.session)
? (fileConfig.session as Record<string, unknown>) ? (fileConfig.session as Record<string, unknown>)
: {}; : {};
const session: Record<string, unknown> = { const session: Record<string, unknown> = {
...fileSession, ...fileSession,
mainKey: fileSession.mainKey ?? "main", mainKey: fileSession.mainKey ?? "main",
}; };
if (typeof testState.sessionStorePath === "string") session.store = testState.sessionStorePath; if (typeof testState.sessionStorePath === "string")
session.store = testState.sessionStorePath;
if (testState.sessionConfig) Object.assign(session, testState.sessionConfig); if (testState.sessionConfig) Object.assign(session, testState.sessionConfig);
const fileGateway = const fileGateway =
fileConfig.gateway && typeof fileConfig.gateway === "object" && !Array.isArray(fileConfig.gateway) fileConfig.gateway &&
typeof fileConfig.gateway === "object" &&
!Array.isArray(fileConfig.gateway)
? ({ ...(fileConfig.gateway as Record<string, unknown>) } as Record<string, unknown>) ? ({ ...(fileConfig.gateway as Record<string, unknown>) } as Record<string, unknown>)
: {}; : {};
if (testState.gatewayBind) fileGateway.bind = testState.gatewayBind; if (testState.gatewayBind) fileGateway.bind = testState.gatewayBind;
@@ -489,14 +502,16 @@ vi.mock("../config/config.js", async () => {
const gateway = Object.keys(fileGateway).length > 0 ? fileGateway : undefined; const gateway = Object.keys(fileGateway).length > 0 ? fileGateway : undefined;
const fileCanvasHost = const fileCanvasHost =
fileConfig.canvasHost && typeof fileConfig.canvasHost === "object" && !Array.isArray(fileConfig.canvasHost) fileConfig.canvasHost &&
typeof fileConfig.canvasHost === "object" &&
!Array.isArray(fileConfig.canvasHost)
? ({ ...(fileConfig.canvasHost as Record<string, unknown>) } as Record<string, unknown>) ? ({ ...(fileConfig.canvasHost as Record<string, unknown>) } as Record<string, unknown>)
: {}; : {};
if (typeof testState.canvasHostPort === "number") fileCanvasHost.port = testState.canvasHostPort; if (typeof testState.canvasHostPort === "number")
fileCanvasHost.port = testState.canvasHostPort;
const canvasHost = Object.keys(fileCanvasHost).length > 0 ? fileCanvasHost : undefined; const canvasHost = Object.keys(fileCanvasHost).length > 0 ? fileCanvasHost : undefined;
const hooks = const hooks = testState.hooksConfig ?? (fileConfig.hooks as HooksConfig | undefined);
testState.hooksConfig ?? (fileConfig.hooks as HooksConfig | undefined);
const fileCron = const fileCron =
fileConfig.cron && typeof fileConfig.cron === "object" && !Array.isArray(fileConfig.cron) fileConfig.cron && typeof fileConfig.cron === "object" && !Array.isArray(fileConfig.cron)

View File

@@ -93,7 +93,8 @@ const parseUsageEntry = (entry: Record<string, unknown>): ParsedUsageEntry | nul
const role = message?.role; const role = message?.role;
if (role !== "assistant") return null; if (role !== "assistant") return null;
const usageRaw = (message?.usage as UsageLike | undefined) ?? (entry.usage as UsageLike | undefined); const usageRaw =
(message?.usage as UsageLike | undefined) ?? (entry.usage as UsageLike | undefined);
const usage = normalizeUsage(usageRaw); const usage = normalizeUsage(usageRaw);
if (!usage) return null; if (!usage) return null;
@@ -123,10 +124,7 @@ const applyUsageTotals = (totals: CostUsageTotals, usage: NormalizedUsage) => {
totals.cacheWrite += usage.cacheWrite ?? 0; totals.cacheWrite += usage.cacheWrite ?? 0;
const totalTokens = const totalTokens =
usage.total ?? usage.total ??
(usage.input ?? 0) + (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
(usage.output ?? 0) +
(usage.cacheRead ?? 0) +
(usage.cacheWrite ?? 0);
totals.totalTokens += totalTokens; totals.totalTokens += totalTokens;
}; };

View File

@@ -45,10 +45,7 @@ async function main() {
{ startGatewayServer }, { startGatewayServer },
{ setGatewayWsLogStyle }, { setGatewayWsLogStyle },
{ setVerbose }, { setVerbose },
{ { consumeGatewaySigusr1RestartAuthorization, isGatewaySigusr1RestartExternallyAllowed },
consumeGatewaySigusr1RestartAuthorization,
isGatewaySigusr1RestartExternallyAllowed,
},
{ defaultRuntime }, { defaultRuntime },
{ enableConsoleCapture, setConsoleTimestampPrefix }, { enableConsoleCapture, setConsoleTimestampPrefix },
] = await Promise.all([ ] = await Promise.all([

View File

@@ -75,7 +75,7 @@ describe("loadClawdbotPlugins", () => {
expect(enabled?.status).toBe("loaded"); expect(enabled?.status).toBe("loaded");
}); });
it("loads bundled telegram plugin when enabled", () => { it("loads bundled telegram plugin when enabled", { timeout: 120_000 }, () => {
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = path.join(process.cwd(), "extensions"); process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = path.join(process.cwd(), "extensions");
const registry = loadClawdbotPlugins({ const registry = loadClawdbotPlugins({

View File

@@ -327,18 +327,7 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
const pluginSdkAlias = resolvePluginSdkAlias(); const pluginSdkAlias = resolvePluginSdkAlias();
const jiti = createJiti(import.meta.url, { const jiti = createJiti(import.meta.url, {
interopDefault: true, interopDefault: true,
extensions: [ extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
".ts",
".tsx",
".mts",
".cts",
".mtsx",
".ctsx",
".js",
".mjs",
".cjs",
".json",
],
...(pluginSdkAlias ? { alias: { "clawdbot/plugin-sdk": pluginSdkAlias } } : {}), ...(pluginSdkAlias ? { alias: { "clawdbot/plugin-sdk": pluginSdkAlias } } : {}),
}); });
@@ -393,9 +382,7 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
try { try {
mod = jiti(candidate.source) as ClawdbotPluginModule; mod = jiti(candidate.source) as ClawdbotPluginModule;
} catch (err) { } catch (err) {
logger.error( logger.error(`[plugins] ${record.id} failed to load from ${record.source}: ${String(err)}`);
`[plugins] ${record.id} failed to load from ${record.source}: ${String(err)}`,
);
record.status = "error"; record.status = "error";
record.error = String(err); record.error = String(err);
registry.plugins.push(record); registry.plugins.push(record);
@@ -480,9 +467,7 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
}); });
if (!validatedConfig.ok) { if (!validatedConfig.ok) {
logger.error( logger.error(`[plugins] ${record.id} invalid config: ${validatedConfig.errors?.join(", ")}`);
`[plugins] ${record.id} invalid config: ${validatedConfig.errors?.join(", ")}`,
);
record.status = "error"; record.status = "error";
record.error = `invalid config: ${validatedConfig.errors?.join(", ")}`; record.error = `invalid config: ${validatedConfig.errors?.join(", ")}`;
registry.plugins.push(record); registry.plugins.push(record);

View File

@@ -28,10 +28,9 @@ export const normalizeAllowFromWithStore = (params: {
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
storeAllowFrom?: string[]; storeAllowFrom?: string[];
}): NormalizedAllowFrom => { }): NormalizedAllowFrom => {
const combined = [ const combined = [...(params.allowFrom ?? []), ...(params.storeAllowFrom ?? [])]
...(params.allowFrom ?? []), .map((value) => String(value).trim())
...(params.storeAllowFrom ?? []), .filter(Boolean);
].map((value) => String(value).trim()).filter(Boolean);
return normalizeAllowFrom(combined); return normalizeAllowFrom(combined);
}; };

View File

@@ -13,8 +13,16 @@ const mockTheme: SearchableSelectListTheme = {
}; };
const testItems = [ const testItems = [
{ value: "anthropic/claude-3-opus", label: "anthropic/claude-3-opus", description: "Claude 3 Opus" }, {
{ value: "anthropic/claude-3-sonnet", label: "anthropic/claude-3-sonnet", description: "Claude 3 Sonnet" }, value: "anthropic/claude-3-opus",
label: "anthropic/claude-3-opus",
description: "Claude 3 Opus",
},
{
value: "anthropic/claude-3-sonnet",
label: "anthropic/claude-3-sonnet",
description: "Claude 3 Sonnet",
},
{ value: "openai/gpt-4", label: "openai/gpt-4", description: "GPT-4" }, { value: "openai/gpt-4", label: "openai/gpt-4", description: "GPT-4" },
{ value: "openai/gpt-4-turbo", label: "openai/gpt-4-turbo", description: "GPT-4 Turbo" }, { value: "openai/gpt-4-turbo", label: "openai/gpt-4-turbo", description: "GPT-4 Turbo" },
{ value: "google/gemini-pro", label: "google/gemini-pro", description: "Gemini Pro" }, { value: "google/gemini-pro", label: "google/gemini-pro", description: "Gemini Pro" },
@@ -50,7 +58,11 @@ describe("SearchableSelectList", () => {
const items = [ const items = [
{ value: "openrouter/auto", label: "openrouter/auto", description: "Routes to best" }, { value: "openrouter/auto", label: "openrouter/auto", description: "Routes to best" },
{ value: "opus-direct", label: "opus-direct", description: "Direct opus model" }, { value: "opus-direct", label: "opus-direct", description: "Direct opus model" },
{ value: "anthropic/claude-3-opus", label: "anthropic/claude-3-opus", description: "Claude 3 Opus" }, {
value: "anthropic/claude-3-opus",
label: "anthropic/claude-3-opus",
description: "Claude 3 Opus",
},
]; ];
const list = new SearchableSelectList(items, 5, mockTheme); const list = new SearchableSelectList(items, 5, mockTheme);
@@ -66,7 +78,11 @@ describe("SearchableSelectList", () => {
it("exact label match beats description match", () => { it("exact label match beats description match", () => {
const items = [ const items = [
{ value: "provider/other", label: "provider/other", description: "This mentions opus in description" }, {
value: "provider/other",
label: "provider/other",
description: "This mentions opus in description",
},
{ value: "provider/opus-model", label: "provider/opus-model", description: "Something else" }, { value: "provider/opus-model", label: "provider/opus-model", description: "Something else" },
]; ];
const list = new SearchableSelectList(items, 5, mockTheme); const list = new SearchableSelectList(items, 5, mockTheme);

View File

@@ -98,7 +98,11 @@ export class SearchableSelectList implements Component {
exactLabel.sort(this.compareByScore); exactLabel.sort(this.compareByScore);
wordBoundary.sort(this.compareByScore); wordBoundary.sort(this.compareByScore);
descriptionMatches.sort(this.compareByScore); descriptionMatches.sort(this.compareByScore);
const fuzzyMatches = fuzzyFilter(fuzzyCandidates, query, (i) => `${i.label} ${i.description ?? ""}`); const fuzzyMatches = fuzzyFilter(
fuzzyCandidates,
query,
(i) => `${i.label} ${i.description ?? ""}`,
);
return [ return [
...exactLabel.map((s) => s.item), ...exactLabel.map((s) => s.item),
...wordBoundary.map((s) => s.item), ...wordBoundary.map((s) => s.item),
@@ -133,7 +137,10 @@ export class SearchableSelectList implements Component {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
private compareByScore = (a: { item: SelectItem; score: number }, b: { item: SelectItem; score: number }) => { private compareByScore = (
a: { item: SelectItem; score: number },
b: { item: SelectItem; score: number },
) => {
if (a.score !== b.score) return a.score - b.score; if (a.score !== b.score) return a.score - b.score;
return this.getItemLabel(a.item).localeCompare(this.getItemLabel(b.item)); return this.getItemLabel(a.item).localeCompare(this.getItemLabel(b.item));
}; };
@@ -190,7 +197,10 @@ export class SearchableSelectList implements Component {
// Calculate visible range with scrolling // Calculate visible range with scrolling
const startIndex = Math.max( const startIndex = Math.max(
0, 0,
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible), Math.min(
this.selectedIndex - Math.floor(this.maxVisible / 2),
this.filteredItems.length - this.maxVisible,
),
); );
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length); const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
@@ -211,7 +221,12 @@ export class SearchableSelectList implements Component {
return lines; return lines;
} }
private renderItemLine(item: SelectItem, isSelected: boolean, width: number, query: string): string { private renderItemLine(
item: SelectItem,
isSelected: boolean,
width: number,
query: string,
): string {
const prefix = isSelected ? "→ " : " "; const prefix = isSelected ? "→ " : " ";
const prefixWidth = prefix.length; const prefixWidth = prefix.length;
const displayValue = this.getItemLabel(item); const displayValue = this.getItemLabel(item);

View File

@@ -7,7 +7,11 @@ import {
import { normalizeAgentId } from "../routing/session-key.js"; import { normalizeAgentId } from "../routing/session-key.js";
import { helpText, parseCommand } from "./commands.js"; import { helpText, parseCommand } from "./commands.js";
import type { ChatLog } from "./components/chat-log.js"; import type { ChatLog } from "./components/chat-log.js";
import { createSearchableSelectList, createSelectList, createSettingsList } from "./components/selectors.js"; import {
createSearchableSelectList,
createSelectList,
createSettingsList,
} from "./components/selectors.js";
import type { GatewayChatClient } from "./gateway-chat.js"; import type { GatewayChatClient } from "./gateway-chat.js";
import { formatStatusSummary } from "./tui-status-summary.js"; import { formatStatusSummary } from "./tui-status-summary.js";
import type { import type {

View File

@@ -22,10 +22,7 @@ import { editorTheme, theme } from "./theme/theme.js";
import { createCommandHandlers } from "./tui-command-handlers.js"; import { createCommandHandlers } from "./tui-command-handlers.js";
import { createEventHandlers } from "./tui-event-handlers.js"; import { createEventHandlers } from "./tui-event-handlers.js";
import { formatTokens } from "./tui-formatters.js"; import { formatTokens } from "./tui-formatters.js";
import { import { buildWaitingStatusMessage, defaultWaitingPhrases } from "./tui-waiting.js";
buildWaitingStatusMessage,
defaultWaitingPhrases,
} from "./tui-waiting.js";
import { createOverlayHandlers } from "./tui-overlays.js"; import { createOverlayHandlers } from "./tui-overlays.js";
import { createSessionActions } from "./tui-session-actions.js"; import { createSessionActions } from "./tui-session-actions.js";
import type { import type {
@@ -335,8 +332,7 @@ export async function runTui(opts: TuiOptions) {
// Pick a phrase once per waiting session. // Pick a phrase once per waiting session.
if (!waitingPhrase) { if (!waitingPhrase) {
const idx = Math.floor(Math.random() * defaultWaitingPhrases.length); const idx = Math.floor(Math.random() * defaultWaitingPhrases.length);
waitingPhrase = waitingPhrase = defaultWaitingPhrases[idx] ?? defaultWaitingPhrases[0] ?? "waiting";
defaultWaitingPhrases[idx] ?? defaultWaitingPhrases[0] ?? "waiting";
} }
waitingTick = 0; waitingTick = 0;