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 let runid: String
public let sessionkey: String

View File

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

View File

@@ -25,8 +25,6 @@ describe("sanitizeUserFacingText", () => {
it("sanitizes raw API error payloads", () => {
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}';
expect(sanitizeUserFacingText(raw)).toBe(
"The AI service returned an error. Please try again.",
);
expect(sanitizeUserFacingText(raw)).toBe("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
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 (
"Message ordering conflict - please try again. " +
"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
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") {
// 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.
log.warn(
`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 {
try {
await activeSession.prompt(params.prompt, { images: params.images });

View File

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

View File

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

View File

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

View File

@@ -49,7 +49,10 @@ export function findChromeExecutableMac(): BrowserExecutable | null {
},
{
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",

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 ids/aliases in `src/channels/registry.ts`
function resolveCoreChannels(): ChannelPlugin[] {
return [
telegramPlugin,
whatsappPlugin,
discordPlugin,
slackPlugin,
signalPlugin,
imessagePlugin,
];
return [telegramPlugin, whatsappPlugin, discordPlugin, slackPlugin, signalPlugin, imessagePlugin];
}
function listPluginChannels(): ChannelPlugin[] {
@@ -80,12 +73,5 @@ export function normalizeChannelId(raw?: string | null): ChannelId | null {
return plugin?.id ?? null;
}
export {
discordPlugin,
imessagePlugin,
signalPlugin,
slackPlugin,
telegramPlugin,
whatsappPlugin,
};
export { discordPlugin, imessagePlugin, signalPlugin, slackPlugin, telegramPlugin, whatsappPlugin };
export type { ChannelId, ChannelPlugin } from "./types.js";

View File

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

View File

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

View File

@@ -8,10 +8,18 @@ import {
upsertAuthProfile,
} from "../../agents/auth-profiles.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 { 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 { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js";
import { applyAuthProfileConfig } from "../onboard-auth.js";
@@ -21,7 +29,11 @@ import { createVpsAwareOAuthHandlers } from "../oauth-flow.js";
import { updateConfig } from "./shared.js";
import { resolvePluginProviders } from "../../plugins/providers.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";
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
@@ -334,14 +346,16 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
const prompter = createClackPrompter();
const selectedProvider =
resolveProviderMatch(providers, opts.provider) ??
(await prompter.select({
message: "Select a provider",
options: providers.map((provider) => ({
value: provider.id,
label: provider.label,
hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined,
})),
}).then((id) => resolveProviderMatch(providers, String(id))));
(await prompter
.select({
message: "Select a provider",
options: providers.map((provider) => ({
value: provider.id,
label: provider.label,
hint: provider.docsPath ? `Docs: ${provider.docsPath}` : undefined,
})),
})
.then((id) => resolveProviderMatch(providers, String(id))));
if (!selectedProvider) {
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) ??
(selectedProvider.auth.length === 1
? selectedProvider.auth[0]
: await prompter.select({
message: `Auth method for ${selectedProvider.label}`,
options: selectedProvider.auth.map((method) => ({
value: method.id,
label: method.label,
hint: method.hint,
})),
}).then((id) =>
selectedProvider.auth.find((method) => method.id === String(id)),
));
: await prompter
.select({
message: `Auth method for ${selectedProvider.label}`,
options: selectedProvider.auth.map((method) => ({
value: method.id,
label: method.label,
hint: method.hint,
})),
})
.then((id) => selectedProvider.auth.find((method) => method.id === String(id))));
if (!chosenMethod) {
throw new Error("Unknown auth method. Use --method <id> to select one.");

View File

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

View File

@@ -317,7 +317,10 @@ export async function setupChannels(
};
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) => {
const status = statusByChannel.get(entry.id);

View File

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

View File

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

View File

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