chore: format and sync protocol outputs

This commit is contained in:
Peter Steinberger
2026-01-16 03:30:56 +00:00
parent a5d8f89b53
commit abcca86e4e
18 changed files with 117 additions and 95 deletions

View File

@@ -1679,6 +1679,27 @@ public struct ChatAbortParams: Codable, Sendable {
} }
} }
public struct ChatInjectParams: Codable, Sendable {
public let sessionkey: String
public let message: String
public let label: String?
public init(
sessionkey: String,
message: String,
label: String?
) {
self.sessionkey = sessionkey
self.message = message
self.label = label
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case message
case label
}
}
public struct ChatEvent: Codable, Sendable { public struct ChatEvent: Codable, Sendable {
public let runid: String public let runid: String
public let sessionkey: String public let sessionkey: String

View File

@@ -126,15 +126,7 @@ describe("cleanupSuspendedCliProcesses", () => {
await cleanupSuspendedCliProcesses( await cleanupSuspendedCliProcesses(
{ {
command: "codex", command: "codex",
resumeArgs: [ resumeArgs: ["exec", "resume", "{sessionId}", "--color", "never", "--sandbox", "read-only"],
"exec",
"resume",
"{sessionId}",
"--color",
"never",
"--sandbox",
"read-only",
],
} as CliBackendConfig, } as CliBackendConfig,
1, 1,
); );

View File

@@ -25,8 +25,6 @@ describe("sanitizeUserFacingText", () => {
it("sanitizes raw API error payloads", () => { it("sanitizes raw API error payloads", () => {
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}'; const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}';
expect(sanitizeUserFacingText(raw)).toBe( expect(sanitizeUserFacingText(raw)).toBe("The AI service returned an error. Please try again.");
"The AI service returned an error. Please try again.",
);
}); });
}); });

View File

@@ -230,7 +230,11 @@ export function formatAssistantErrorText(
} }
// Catch role ordering errors - including JSON-wrapped and "400" prefix variants // Catch role ordering errors - including JSON-wrapped and "400" prefix variants
if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(raw)) { if (
/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(
raw,
)
) {
return ( return (
"Message ordering conflict - please try again. " + "Message ordering conflict - please try again. " +
"If this persists, use /new to start a fresh session." "If this persists, use /new to start a fresh session."

View File

@@ -422,7 +422,8 @@ export async function runEmbeddedAttempt(
// Check if last message is a user message to prevent consecutive user turns // Check if last message is a user message to prevent consecutive user turns
const lastMsg = activeSession.messages[activeSession.messages.length - 1]; const lastMsg = activeSession.messages[activeSession.messages.length - 1];
const lastMsgRole = lastMsg && typeof lastMsg === "object" ? (lastMsg as { role?: unknown }).role : undefined; const lastMsgRole =
lastMsg && typeof lastMsg === "object" ? (lastMsg as { role?: unknown }).role : undefined;
if (lastMsgRole === "user") { if (lastMsgRole === "user") {
// Last message was a user message. Adding another user message would create // Last message was a user message. Adding another user message would create
@@ -433,9 +434,11 @@ export async function runEmbeddedAttempt(
// Skip this prompt to prevent "400 Incorrect role information" error. // Skip this prompt to prevent "400 Incorrect role information" error.
log.warn( log.warn(
`Skipping prompt because last message is a user message (would create consecutive user turns). ` + `Skipping prompt because last message is a user message (would create consecutive user turns). ` +
`runId=${params.runId} sessionId=${params.sessionId}` `runId=${params.runId} sessionId=${params.sessionId}`,
);
promptError = new Error(
"Incorrect role information: consecutive user messages would violate role ordering",
); );
promptError = new Error("Incorrect role information: consecutive user messages would violate role ordering");
} else { } else {
try { try {
await activeSession.prompt(params.prompt, { images: params.images }); await activeSession.prompt(params.prompt, { images: params.images });

View File

@@ -27,10 +27,7 @@ function buildSkillsSection(params: {
]; ];
} }
function buildMemorySection(params: { function buildMemorySection(params: { isMinimal: boolean; availableTools: Set<string> }) {
isMinimal: boolean;
availableTools: Set<string>;
}) {
if (params.isMinimal) return []; if (params.isMinimal) return [];
if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) { if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
return []; return [];

View File

@@ -156,10 +156,7 @@ export async function buildStatusReply(params: {
usageProviders.add(currentUsageProvider); usageProviders.add(currentUsageProvider);
} }
const usageByProvider = new Map<string, string>(); const usageByProvider = new Map<string, string>();
let usageSummaryCache: let usageSummaryCache: Awaited<ReturnType<typeof loadProviderUsageSummary>> | null | undefined;
| Awaited<ReturnType<typeof loadProviderUsageSummary>>
| null
| undefined;
if (usageProviders.size > 0) { if (usageProviders.size > 0) {
try { try {
usageSummaryCache = await loadProviderUsageSummary({ usageSummaryCache = await loadProviderUsageSummary({

View File

@@ -43,9 +43,9 @@ export function applyReplyTagsToPayload(
export function isRenderablePayload(payload: ReplyPayload): boolean { export function isRenderablePayload(payload: ReplyPayload): boolean {
return Boolean( return Boolean(
payload.text || payload.text ||
payload.mediaUrl || payload.mediaUrl ||
(payload.mediaUrls && payload.mediaUrls.length > 0) || (payload.mediaUrls && payload.mediaUrls.length > 0) ||
payload.audioAsVoice, payload.audioAsVoice,
); );
} }

View File

@@ -49,7 +49,10 @@ export function findChromeExecutableMac(): BrowserExecutable | null {
}, },
{ {
kind: "edge", kind: "edge",
path: path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"), path: path.join(
os.homedir(),
"Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
),
}, },
{ {
kind: "chromium", kind: "chromium",

View File

@@ -19,14 +19,7 @@ import { getActivePluginRegistry } from "../../plugins/runtime.js";
// - add an entry to `src/channels/dock.ts` for shared behavior (capabilities, allowFrom, threading, …) // - add an entry to `src/channels/dock.ts` for shared behavior (capabilities, allowFrom, threading, …)
// - add ids/aliases in `src/channels/registry.ts` // - add ids/aliases in `src/channels/registry.ts`
function resolveCoreChannels(): ChannelPlugin[] { function resolveCoreChannels(): ChannelPlugin[] {
return [ return [telegramPlugin, whatsappPlugin, discordPlugin, slackPlugin, signalPlugin, imessagePlugin];
telegramPlugin,
whatsappPlugin,
discordPlugin,
slackPlugin,
signalPlugin,
imessagePlugin,
];
} }
function listPluginChannels(): ChannelPlugin[] { function listPluginChannels(): ChannelPlugin[] {
@@ -80,12 +73,5 @@ export function normalizeChannelId(raw?: string | null): ChannelId | null {
return plugin?.id ?? null; return plugin?.id ?? null;
} }
export { export { discordPlugin, imessagePlugin, signalPlugin, slackPlugin, telegramPlugin, whatsappPlugin };
discordPlugin,
imessagePlugin,
signalPlugin,
slackPlugin,
telegramPlugin,
whatsappPlugin,
};
export type { ChannelId, ChannelPlugin } from "./types.js"; export type { ChannelId, ChannelPlugin } from "./types.js";

View File

@@ -4,14 +4,14 @@ import {
githubCopilotLoginCommand, githubCopilotLoginCommand,
modelsAliasesAddCommand, modelsAliasesAddCommand,
modelsAliasesListCommand, modelsAliasesListCommand,
modelsAliasesRemoveCommand, modelsAliasesRemoveCommand,
modelsAuthAddCommand, modelsAuthAddCommand,
modelsAuthLoginCommand, modelsAuthLoginCommand,
modelsAuthOrderClearCommand, modelsAuthOrderClearCommand,
modelsAuthOrderGetCommand, modelsAuthOrderGetCommand,
modelsAuthOrderSetCommand, modelsAuthOrderSetCommand,
modelsAuthPasteTokenCommand, modelsAuthPasteTokenCommand,
modelsAuthSetupTokenCommand, modelsAuthSetupTokenCommand,
modelsFallbacksAddCommand, modelsFallbacksAddCommand,
modelsFallbacksClearCommand, modelsFallbacksClearCommand,
modelsFallbacksListCommand, modelsFallbacksListCommand,

View File

@@ -119,11 +119,14 @@ describe("runConfigureWizard", () => {
mocks.clackText.mockResolvedValue(""); mocks.clackText.mockResolvedValue("");
mocks.clackConfirm.mockResolvedValue(false); mocks.clackConfirm.mockResolvedValue(false);
await runConfigureWizard({ command: "configure" }, { await runConfigureWizard(
log: vi.fn(), { command: "configure" },
error: vi.fn(), {
exit: vi.fn(), log: vi.fn(),
}); error: vi.fn(),
exit: vi.fn(),
},
);
expect(mocks.writeConfigFile).toHaveBeenCalledWith( expect(mocks.writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({

View File

@@ -8,10 +8,18 @@ import {
upsertAuthProfile, upsertAuthProfile,
} from "../../agents/auth-profiles.js"; } from "../../agents/auth-profiles.js";
import { normalizeProviderId } from "../../agents/model-selection.js"; import { normalizeProviderId } from "../../agents/model-selection.js";
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import {
resolveAgentDir,
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import { parseDurationMs } from "../../cli/parse-duration.js"; import { parseDurationMs } from "../../cli/parse-duration.js";
import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, type ClawdbotConfig } from "../../config/config.js"; import {
CONFIG_PATH_CLAWDBOT,
readConfigFileSnapshot,
type ClawdbotConfig,
} from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js";
import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js"; import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js";
import { applyAuthProfileConfig } from "../onboard-auth.js"; import { applyAuthProfileConfig } from "../onboard-auth.js";
@@ -21,7 +29,11 @@ import { createVpsAwareOAuthHandlers } from "../oauth-flow.js";
import { updateConfig } from "./shared.js"; import { updateConfig } from "./shared.js";
import { resolvePluginProviders } from "../../plugins/providers.js"; import { resolvePluginProviders } from "../../plugins/providers.js";
import { createClackPrompter } from "../../wizard/clack-prompter.js"; import { createClackPrompter } from "../../wizard/clack-prompter.js";
import type { ProviderAuthMethod, ProviderAuthResult, ProviderPlugin } from "../../plugins/types.js"; import type {
ProviderAuthMethod,
ProviderAuthResult,
ProviderPlugin,
} from "../../plugins/types.js";
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js"; import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
const confirm = (params: Parameters<typeof clackConfirm>[0]) => const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
@@ -334,14 +346,16 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
const prompter = createClackPrompter(); const prompter = createClackPrompter();
const selectedProvider = const selectedProvider =
resolveProviderMatch(providers, opts.provider) ?? resolveProviderMatch(providers, opts.provider) ??
(await prompter.select({ (await prompter
message: "Select a provider", .select({
options: providers.map((provider) => ({ message: "Select a provider",
value: provider.id, options: providers.map((provider) => ({
label: provider.label, value: provider.id,
hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined, label: provider.label,
})), hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined,
}).then((id) => resolveProviderMatch(providers, String(id)))); })),
})
.then((id) => resolveProviderMatch(providers, String(id))));
if (!selectedProvider) { if (!selectedProvider) {
throw new Error("Unknown provider. Use --provider <id> to pick a provider plugin."); throw new Error("Unknown provider. Use --provider <id> to pick a provider plugin.");
@@ -351,16 +365,16 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
pickAuthMethod(selectedProvider, opts.method) ?? pickAuthMethod(selectedProvider, opts.method) ??
(selectedProvider.auth.length === 1 (selectedProvider.auth.length === 1
? selectedProvider.auth[0] ? selectedProvider.auth[0]
: await prompter.select({ : await prompter
message: `Auth method for ${selectedProvider.label}`, .select({
options: selectedProvider.auth.map((method) => ({ message: `Auth method for ${selectedProvider.label}`,
value: method.id, options: selectedProvider.auth.map((method) => ({
label: method.label, value: method.id,
hint: method.hint, label: method.label,
})), hint: method.hint,
}).then((id) => })),
selectedProvider.auth.find((method) => method.id === String(id)), })
)); .then((id) => selectedProvider.auth.find((method) => method.id === String(id))));
if (!chosenMethod) { if (!chosenMethod) {
throw new Error("Unknown auth method. Use --method <id> to select one."); throw new Error("Unknown auth method. Use --method <id> to select one.");

View File

@@ -177,9 +177,7 @@ describe("setupChannels", () => {
}, },
); );
expect(select).toHaveBeenCalledWith( expect(select).toHaveBeenCalledWith(expect.objectContaining({ message: "Select a channel" }));
expect.objectContaining({ message: "Select a channel" }),
);
expect(multiselect).not.toHaveBeenCalled(); expect(multiselect).not.toHaveBeenCalled();
}); });
}); });

View File

@@ -317,7 +317,10 @@ export async function setupChannels(
}; };
const buildSelectionOptions = ( const buildSelectionOptions = (
entries: Array<{ id: ChannelChoice; meta: { id: string; label: string; selectionLabel?: string } }>, entries: Array<{
id: ChannelChoice;
meta: { id: string; label: string; selectionLabel?: string };
}>,
) => ) =>
entries.map((entry) => { entries.map((entry) => {
const status = statusByChannel.get(entry.id); const status = statusByChannel.get(entry.id);

View File

@@ -108,9 +108,7 @@ export function resolveHeartbeatIntervalMs(
} }
export function resolveHeartbeatPrompt(cfg: ClawdbotConfig, heartbeat?: HeartbeatConfig) { export function resolveHeartbeatPrompt(cfg: ClawdbotConfig, heartbeat?: HeartbeatConfig) {
return resolveHeartbeatPromptText( return resolveHeartbeatPromptText(heartbeat?.prompt ?? cfg.agents?.defaults?.heartbeat?.prompt);
heartbeat?.prompt ?? cfg.agents?.defaults?.heartbeat?.prompt,
);
} }
function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig, heartbeat?: HeartbeatConfig) { function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig, heartbeat?: HeartbeatConfig) {
@@ -127,9 +125,7 @@ function resolveHeartbeatSession(cfg: ClawdbotConfig, agentId?: string) {
const scope = sessionCfg?.scope ?? "per-sender"; const scope = sessionCfg?.scope ?? "per-sender";
const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg)); const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg));
const sessionKey = const sessionKey =
scope === "global" scope === "global" ? "global" : resolveAgentMainSessionKey({ cfg, agentId: resolvedAgentId });
? "global"
: resolveAgentMainSessionKey({ cfg, agentId: resolvedAgentId });
const storeAgentId = scope === "global" ? resolveDefaultAgentId(cfg) : resolvedAgentId; const storeAgentId = scope === "global" ? resolveDefaultAgentId(cfg) : resolvedAgentId;
const storePath = resolveStorePath(sessionCfg?.store, { agentId: storeAgentId }); const storePath = resolveStorePath(sessionCfg?.store, { agentId: storeAgentId });
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
@@ -337,8 +333,10 @@ export async function runHeartbeatOnce(opts: {
// Suppress duplicate heartbeats (same payload) within a short window. // Suppress duplicate heartbeats (same payload) within a short window.
// This prevents "nagging" when nothing changed but the model repeats the same items. // This prevents "nagging" when nothing changed but the model repeats the same items.
const prevHeartbeatText = typeof entry?.lastHeartbeatText === "string" ? entry.lastHeartbeatText : ""; const prevHeartbeatText =
const prevHeartbeatAt = typeof entry?.lastHeartbeatSentAt === "number" ? entry.lastHeartbeatSentAt : undefined; typeof entry?.lastHeartbeatText === "string" ? entry.lastHeartbeatText : "";
const prevHeartbeatAt =
typeof entry?.lastHeartbeatSentAt === "number" ? entry.lastHeartbeatSentAt : undefined;
const isDuplicateMain = const isDuplicateMain =
!shouldSkipMain && !shouldSkipMain &&
!mediaUrls.length && !mediaUrls.length &&

View File

@@ -210,10 +210,11 @@ export async function runMessageAction(
const to = readStringParam(params, "to", { required: true }); const to = readStringParam(params, "to", { required: true });
// Allow message to be omitted when sending media-only (e.g., voice notes) // Allow message to be omitted when sending media-only (e.g., voice notes)
const mediaHint = readStringParam(params, "media", { trim: false }); const mediaHint = readStringParam(params, "media", { trim: false });
let message = readStringParam(params, "message", { let message =
required: !mediaHint, // Only require message if no media hint readStringParam(params, "message", {
allowEmpty: true, required: !mediaHint, // Only require message if no media hint
}) ?? ""; allowEmpty: true,
}) ?? "";
const parsed = parseReplyDirectives(message); const parsed = parseReplyDirectives(message);
message = parsed.text; message = parsed.text;

View File

@@ -34,7 +34,11 @@ export type GatewaySessionList = {
ts: number; ts: number;
path: string; path: string;
count: number; count: number;
defaults?: { model?: string | null; modelProvider?: string | null; contextTokens?: number | null }; defaults?: {
model?: string | null;
modelProvider?: string | null;
contextTokens?: number | null;
};
sessions: Array<{ sessions: Array<{
key: string; key: string;
sessionId?: string; sessionId?: string;