fix: stabilize model catalog and pi discovery auth storage compatibility

This commit is contained in:
Peter Steinberger
2026-02-18 02:09:40 +01:00
parent 653add918b
commit 6dcc052bb4
41 changed files with 184 additions and 137 deletions

View File

@@ -5,7 +5,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const sendMessageMatrixMock = vi.hoisted(() => vi.fn().mockResolvedValue({ messageId: "mx-1" }));
vi.mock("../send.js", () => ({
sendMessageMatrix: (...args: unknown[]) => sendMessageMatrixMock(...args),
sendMessageMatrix: (to: string, message: string, opts?: unknown) =>
sendMessageMatrixMock(to, message, opts),
}));
import { setMatrixRuntime } from "../../runtime.js";
@@ -20,14 +21,14 @@ describe("deliverMatrixReplies", () => {
const runtimeStub = {
config: {
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
loadConfig: () => loadConfigMock(),
},
channel: {
text: {
resolveMarkdownTableMode: (...args: unknown[]) => resolveMarkdownTableModeMock(...args),
convertMarkdownTables: (...args: unknown[]) => convertMarkdownTablesMock(...args),
resolveChunkMode: (...args: unknown[]) => resolveChunkModeMock(...args),
chunkMarkdownTextWithMode: (...args: unknown[]) => chunkMarkdownTextWithModeMock(...args),
resolveMarkdownTableMode: () => resolveMarkdownTableModeMock(),
convertMarkdownTables: (text: string) => convertMarkdownTablesMock(text),
resolveChunkMode: () => resolveChunkModeMock(),
chunkMarkdownTextWithMode: (text: string) => chunkMarkdownTextWithModeMock(text),
},
},
logging: {

View File

@@ -1,8 +1,8 @@
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import {
ANTHROPIC_SETUP_TOKEN_PREFIX,

View File

@@ -67,6 +67,14 @@ export function __setModelCatalogImportForTest(loader?: () => Promise<PiSdkModul
importPiSdk = loader ?? defaultImportPiSdk;
}
function createAuthStorage(AuthStorageLike: unknown, path: string) {
const withFactory = AuthStorageLike as { create?: (path: string) => unknown };
if (typeof withFactory.create === "function") {
return withFactory.create(path);
}
return new (AuthStorageLike as { new (path: string): unknown })(path);
}
export async function loadModelCatalog(params?: {
config?: OpenClawConfig;
useCache?: boolean;
@@ -101,12 +109,17 @@ export async function loadModelCatalog(params?: {
const piSdk = await importPiSdk();
const agentDir = resolveOpenClawAgentDir();
const { join } = await import("node:path");
const authStorage = new piSdk.AuthStorage(join(agentDir, "auth.json"));
const registry = new piSdk.ModelRegistry(authStorage, join(agentDir, "models.json")) as
| {
getAll: () => Array<DiscoveredModel>;
}
| Array<DiscoveredModel>;
const authStorage = createAuthStorage(piSdk.AuthStorage, join(agentDir, "auth.json"));
const registry = new (piSdk.ModelRegistry as unknown as {
new (
authStorage: unknown,
modelsFile: string,
):
| Array<DiscoveredModel>
| {
getAll: () => Array<DiscoveredModel>;
};
})(authStorage, join(agentDir, "models.json"));
const entries = Array.isArray(registry) ? registry : registry.getAll();
for (const entry of entries) {
const id = String(entry?.id ?? "").trim();

View File

@@ -1,8 +1,8 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import type { ModelRegistry } from "./pi-model-discovery.js";
import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js";
import { normalizeModelCompat } from "./model-compat.js";
import { normalizeProviderId } from "./model-selection.js";
import type { ModelRegistry } from "./pi-model-discovery.js";
const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex";
const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const;

View File

@@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../config/config.js";
import type { ModelCatalogEntry } from "./model-catalog.js";
import { resolveAgentModelPrimary } from "./agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import type { ModelCatalogEntry } from "./model-catalog.js";
import { normalizeGoogleModelId } from "./models-config.providers.js";
export type ModelRef = {

View File

@@ -1,12 +1,12 @@
import type { OpenClawConfig } from "../config/config.js";
import type { GatewayMessageChannel } from "../utils/message-channel.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
import type { AnyAgentTool } from "./tools/common.js";
import { resolvePluginTools } from "../plugins/tools.js";
import type { GatewayMessageChannel } from "../utils/message-channel.js";
import { resolveSessionAgentId } from "./agent-scope.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
import { createAgentsListTool } from "./tools/agents-list-tool.js";
import { createBrowserTool } from "./tools/browser-tool.js";
import { createCanvasTool } from "./tools/canvas-tool.js";
import type { AnyAgentTool } from "./tools/common.js";
import { createCronTool } from "./tools/cron-tool.js";
import { createGatewayTool } from "./tools/gateway-tool.js";
import { createImageTool } from "./tools/image-tool.js";

View File

@@ -1,3 +1,5 @@
import fs from "node:fs/promises";
import os from "node:os";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import {
createAgentSession,
@@ -5,14 +7,10 @@ import {
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
import fs from "node:fs/promises";
import os from "node:os";
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { ExecElevatedDefaults } from "../bash-tools.js";
import type { EmbeddedPiCompactResult } from "./types.js";
import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js";
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
import type { OpenClawConfig } from "../../config/config.js";
import { getMachineDisplayName } from "../../infra/machine-name.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
@@ -26,6 +24,7 @@ import { normalizeMessageChannel } from "../../utils/message-channel.js";
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
import { resolveOpenClawAgentDir } from "../agent-paths.js";
import { resolveSessionAgentIds } from "../agent-scope.js";
import type { ExecElevatedDefaults } from "../bash-tools.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
import { listChannelSupportedActions, resolveChannelMessageToolHints } from "../channel-tools.js";
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
@@ -82,6 +81,7 @@ import {
createSystemPromptOverride,
} from "./system-prompt.js";
import { splitSdkTools } from "./tool-split.js";
import type { EmbeddedPiCompactResult } from "./types.js";
import { describeUnknownError, mapThinkingLevel } from "./utils.js";
import { flushPendingToolResultsAfterIdle } from "./wait-for-idle-before-flush.js";

View File

@@ -1,9 +1,8 @@
import { EventEmitter } from "node:events";
import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
import type { SessionManager } from "@mariozechner/pi-coding-agent";
import type { TSchema } from "@sinclair/typebox";
import { EventEmitter } from "node:events";
import type { OpenClawConfig } from "../../config/config.js";
import type { TranscriptPolicy } from "../transcript-policy.js";
import { registerUnhandledRejectionHandler } from "../../infra/unhandled-rejections.js";
import {
hasInterSessionUserProvenance,
@@ -23,6 +22,7 @@ import {
stripToolResultDetails,
sanitizeToolUseResultPairing,
} from "../session-transcript-repair.js";
import type { TranscriptPolicy } from "../transcript-policy.js";
import { resolveTranscriptPolicy } from "../transcript-policy.js";
import { log } from "./logger.js";
import { describeUnknownError } from "./utils.js";

View File

@@ -1,10 +1,9 @@
import fs from "node:fs/promises";
import os from "node:os";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { ImageContent } from "@mariozechner/pi-ai";
import { streamSimple } from "@mariozechner/pi-ai";
import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
import fs from "node:fs/promises";
import os from "node:os";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
import { getMachineDisplayName } from "../../../infra/machine-name.js";
@@ -107,6 +106,7 @@ import {
shouldFlagCompactionTimeout,
} from "./compaction-timeout.js";
import { detectAndLoadPromptImages } from "./images.js";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
export function injectHistoryImagesIntoMessages(
messages: AgentMessage[],

View File

@@ -1,10 +1,10 @@
import type { ImageContent } from "@mariozechner/pi-ai";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
import type { ImageContent } from "@mariozechner/pi-ai";
import { resolveUserPath } from "../../../utils.js";
import { loadWebMedia } from "../../../web/media.js";
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
import { sanitizeImageBlocks } from "../../tool-images.js";
import { log } from "../logger.js";

View File

@@ -3,9 +3,17 @@ import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
export { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
function createAuthStorage(AuthStorageLike: unknown, path: string) {
const withFactory = AuthStorageLike as { create?: (path: string) => unknown };
if (typeof withFactory.create === "function") {
return withFactory.create(path) as AuthStorage;
}
return new (AuthStorageLike as { new (path: string): unknown })(path) as AuthStorage;
}
// Compatibility helpers for pi-coding-agent 0.50+ (discover* helpers removed).
export function discoverAuthStorage(agentDir: string): AuthStorage {
return new AuthStorage(path.join(agentDir, "auth.json"));
return createAuthStorage(AuthStorage, path.join(agentDir, "auth.json"));
}
export function discoverModels(authStorage: AuthStorage, agentDir: string): ModelRegistry {

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { describe, expect, it, vi } from "vitest";
import "./test-helpers/fast-coding-tools.js";
@@ -577,6 +577,17 @@ describe("createOpenClawCodingTools", () => {
});
it("strips truncation.content details from read results while preserving other fields", async () => {
const readResult: AgentToolResult<unknown> = {
content: [{ type: "text" as const, text: "line-0001" }],
details: {
truncation: {
truncated: true,
outputLines: 1,
firstLineExceedsLimit: false,
content: "hidden duplicate payload",
},
},
};
const baseRead: AgentTool = {
name: "read",
label: "read",
@@ -586,17 +597,7 @@ describe("createOpenClawCodingTools", () => {
offset: Type.Optional(Type.Number()),
limit: Type.Optional(Type.Number()),
}),
execute: vi.fn(async () => ({
content: [{ type: "text", text: "line-0001" }],
details: {
truncation: {
truncated: true,
outputLines: 1,
firstLineExceedsLimit: false,
content: "hidden duplicate payload",
},
},
})),
execute: vi.fn(async () => readResult),
};
const wrapped = createOpenClawReadTool(

View File

@@ -1,11 +1,11 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent";
import type { ImageSanitizationLimits } from "./image-sanitization.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
import { detectMime } from "../media/mime.js";
import { sniffMimeFromBase64 } from "../media/sniff-mime-from-base64.js";
import type { ImageSanitizationLimits } from "./image-sanitization.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import { assertSandboxPath } from "./sandbox-paths.js";
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
import { sanitizeToolResultImages } from "./tool-images.js";
// NOTE(steipete): Upstream read now does file-magic MIME detection; we keep the wrapper

View File

@@ -7,9 +7,6 @@ import {
} from "@mariozechner/pi-coding-agent";
import type { OpenClawConfig } from "../config/config.js";
import type { ToolLoopDetectionConfig } from "../config/types.tools.js";
import type { ModelAuthMode } from "./model-auth.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import type { SandboxContext } from "./sandbox.js";
import { logWarn } from "../logger.js";
import { getPluginToolMeta } from "../plugins/tools.js";
import { isSubagentSessionKey } from "../routing/session-key.js";
@@ -24,6 +21,7 @@ import {
} from "./bash-tools.js";
import { listChannelAgentTools } from "./channel-tools.js";
import { resolveImageSanitizationLimits } from "./image-sanitization.js";
import type { ModelAuthMode } from "./model-auth.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import { wrapToolWithAbortSignal } from "./pi-tools.abort.js";
import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
@@ -46,6 +44,8 @@ import {
wrapToolParamNormalization,
} from "./pi-tools.read.js";
import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import type { SandboxContext } from "./sandbox.js";
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import {
applyToolPolicyPipeline,

View File

@@ -477,7 +477,11 @@ export function buildWorkspaceSkillSnapshot(
? `⚠️ Skills truncated: included ${skillsForPrompt.length} of ${resolvedSkills.length}. Run \`openclaw skills check\` to audit.`
: "";
const prompt = [remoteNote, truncationNote, formatSkillsForPrompt(compactSkillPaths(skillsForPrompt))]
const prompt = [
remoteNote,
truncationNote,
formatSkillsForPrompt(compactSkillPaths(skillsForPrompt)),
]
.filter(Boolean)
.join("\n");
const skillFilter = normalizeSkillFilter(opts?.skillFilter);

View File

@@ -1,9 +1,9 @@
import { Type } from "@sinclair/typebox";
import crypto from "node:crypto";
import fs from "node:fs/promises";
import type { OpenClawConfig } from "../../config/config.js";
import { Type } from "@sinclair/typebox";
import { writeBase64ToFile } from "../../cli/nodes-camera.js";
import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../../cli/nodes-canvas.js";
import type { OpenClawConfig } from "../../config/config.js";
import { imageMimeFromFormat } from "../../media/mime.js";
import { resolveImageSanitizationLimits } from "../image-sanitization.js";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";

View File

@@ -1,7 +1,7 @@
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
import fs from "node:fs/promises";
import type { ImageSanitizationLimits } from "../image-sanitization.js";
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
import { detectMime } from "../../media/mime.js";
import type { ImageSanitizationLimits } from "../image-sanitization.js";
import { sanitizeToolResultImages } from "../tool-images.js";
// oxlint-disable-next-line typescript/no-explicit-any

View File

@@ -1,7 +1,6 @@
import crypto from "node:crypto";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import crypto from "node:crypto";
import type { OpenClawConfig } from "../../config/config.js";
import {
type CameraFacing,
cameraTempPath,
@@ -17,6 +16,7 @@ import {
writeScreenRecordToFile,
} from "../../cli/nodes-screen.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import type { OpenClawConfig } from "../../config/config.js";
import { imageMimeFromFormat } from "../../media/mime.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { resolveImageSanitizationLimits } from "../image-sanitization.js";

View File

@@ -1,8 +1,5 @@
import type { OpenClawConfig } from "../../config/config.js";
import type { FinalizedMsgContext } from "../templating.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js";
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { logVerbose } from "../../globals.js";
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
@@ -15,8 +12,11 @@ import {
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
import { getReplyFromConfig } from "../reply.js";
import type { FinalizedMsgContext } from "../templating.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js";
import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
import { isRoutableChannel, routeReply } from "./route-reply.js";
const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;

View File

@@ -545,6 +545,8 @@ describe("cron cli", () => {
}
if (method === "cron.list") {
return {
ok: true,
params: {},
jobs: [
{
id: "job-1",
@@ -581,6 +583,8 @@ describe("cron cli", () => {
}
if (method === "cron.list") {
return {
ok: true,
params: {},
jobs: [{ id: "job-1", schedule: { kind: "every", everyMs: 60_000 } }],
};
}

View File

@@ -1,9 +1,9 @@
import type { Command } from "commander";
import type { CronJob } from "../../cron/types.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { danger } from "../../globals.js";
import { sanitizeAgentId } from "../../routing/session-key.js";
import { defaultRuntime } from "../../runtime.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
import {

View File

@@ -1,11 +1,11 @@
import type { CronJob, CronSchedule } from "../../cron/types.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import { parseAbsoluteTimeMs } from "../../cron/parse.js";
import { resolveCronStaggerMs } from "../../cron/stagger.js";
import type { CronJob, CronSchedule } from "../../cron/types.js";
import { formatDurationHuman } from "../../infra/format-time/format-duration.ts";
import { defaultRuntime } from "../../runtime.js";
import { colorize, isRich, theme } from "../../terminal/theme.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { callGatewayFromCli } from "../gateway-rpc.js";
export const getCronChannelOptions = () =>

View File

@@ -1,7 +1,7 @@
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig, GatewayAuthConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
import {

View File

@@ -1,9 +1,9 @@
import type { OpenClawConfig } from "./types.js";
import type { ModelDefinitionConfig } from "./types.models.js";
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
import { parseModelRef } from "../agents/model-selection.js";
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
import { resolveTalkApiKey } from "./talk.js";
import type { OpenClawConfig } from "./types.js";
import type { ModelDefinitionConfig } from "./types.models.js";
type WarnState = { warned: boolean };

View File

@@ -1,4 +1,3 @@
import type { CronJobCreate, CronJobPatch } from "./types.js";
import { sanitizeAgentId } from "../routing/session-key.js";
import { isRecord } from "../utils.js";
import {
@@ -10,6 +9,7 @@ import { parseAbsoluteTimeMs } from "./parse.js";
import { migrateLegacyCronPayload } from "./payload-migration.js";
import { inferLegacyName } from "./service/normalize.js";
import { normalizeCronStaggerMs, resolveDefaultCronStaggerMs } from "./stagger.js";
import type { CronJobCreate, CronJobPatch } from "./types.js";
type UnknownRecord = Record<string, unknown>;

View File

@@ -3,12 +3,12 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { CronJob, CronJobState } from "./types.js";
import * as schedule from "./schedule.js";
import { CronService } from "./service.js";
import { computeJobNextRunAtMs } from "./service/jobs.js";
import { createCronServiceState, type CronEvent } from "./service/state.js";
import { onTimer } from "./service/timer.js";
import type { CronJob, CronJobState } from "./types.js";
const noopLogger = {
info: vi.fn(),

View File

@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest";
import type { CronServiceState } from "./service/state.js";
import type { CronJob, CronJobPatch } from "./types.js";
import { applyJobPatch, createJob } from "./service/jobs.js";
import type { CronServiceState } from "./service/state.js";
import { DEFAULT_TOP_OF_HOUR_STAGGER_MS } from "./stagger.js";
import type { CronJob, CronJobPatch } from "./types.js";
describe("applyJobPatch", () => {
it("clears delivery when switching to main session", () => {

View File

@@ -1,8 +1,8 @@
import crypto from "node:crypto";
import { describe, expect, it } from "vitest";
import type { CronJob } from "./types.js";
import { computeJobNextRunAtMs } from "./service/jobs.js";
import { DEFAULT_TOP_OF_HOUR_STAGGER_MS } from "./stagger.js";
import type { CronJob } from "./types.js";
function stableOffsetMs(jobId: string, windowMs: number) {
const digest = crypto.createHash("sha256").update(jobId).digest();

View File

@@ -1,4 +1,11 @@
import crypto from "node:crypto";
import { parseAbsoluteTimeMs } from "../parse.js";
import { computeNextRunAtMs } from "../schedule.js";
import {
normalizeCronStaggerMs,
resolveCronStaggerMs,
resolveDefaultCronStaggerMs,
} from "../stagger.js";
import type {
CronDelivery,
CronDeliveryPatch,
@@ -8,14 +15,6 @@ import type {
CronPayload,
CronPayloadPatch,
} from "../types.js";
import type { CronServiceState } from "./state.js";
import { parseAbsoluteTimeMs } from "../parse.js";
import { computeNextRunAtMs } from "../schedule.js";
import {
normalizeCronStaggerMs,
resolveCronStaggerMs,
resolveDefaultCronStaggerMs,
} from "../stagger.js";
import { normalizeHttpWebhookUrl } from "../webhook-url.js";
import {
normalizeOptionalAgentId,
@@ -24,6 +23,7 @@ import {
normalizePayloadToSystemText,
normalizeRequiredName,
} from "./normalize.js";
import type { CronServiceState } from "./state.js";
const STUCK_RUN_MS = 2 * 60 * 60 * 1000;

View File

@@ -1,6 +1,4 @@
import fs from "node:fs";
import type { CronJob } from "../types.js";
import type { CronServiceState } from "./state.js";
import {
buildDeliveryFromLegacyPayload,
hasLegacyDeliveryHints,
@@ -10,8 +8,10 @@ import { parseAbsoluteTimeMs } from "../parse.js";
import { migrateLegacyCronPayload } from "../payload-migration.js";
import { normalizeCronStaggerMs, resolveDefaultCronStaggerMs } from "../stagger.js";
import { loadCronStore, saveCronStore } from "../store.js";
import type { CronJob } from "../types.js";
import { recomputeNextRuns } from "./jobs.js";
import { inferLegacyName, normalizeOptionalText } from "./normalize.js";
import type { CronServiceState } from "./state.js";
function buildDeliveryPatchFromLegacyPayload(payload: Record<string, unknown>) {
const deliver = payload.deliver;

View File

@@ -1,8 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelId } from "../channels/plugins/types.js";
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
import type { ChannelManager, ChannelRuntimeSnapshot } from "./server-channels.js";
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
import type { ChannelManager, ChannelRuntimeSnapshot } from "./server-channels.js";
function createMockChannelManager(overrides?: Partial<ChannelManager>): ChannelManager {
return {
@@ -322,9 +322,9 @@ describe("channel-health-monitor", () => {
});
it("runs checks single-flight when restart work is still in progress", async () => {
let releaseStart: (() => void) | null = null;
let releaseStart: (() => void) | undefined;
const startGate = new Promise<void>((resolve) => {
releaseStart = resolve;
releaseStart = () => resolve();
});
const manager = createMockChannelManager({
getRuntimeSnapshot: vi.fn(() =>

View File

@@ -1,6 +1,6 @@
import type { ChannelId } from "../channels/plugins/types.js";
import type { ChannelManager } from "./server-channels.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import type { ChannelManager } from "./server-channels.js";
const log = createSubsystemLogger("gateway/health-monitor");

View File

@@ -1,4 +1,28 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { loadSessionEntry as loadSessionEntryType } from "./session-utils.js";
const buildSessionLookup = (
sessionKey: string,
entry: {
sessionId?: string;
lastChannel?: string;
lastTo?: string;
updatedAt?: number;
} = {},
): ReturnType<typeof loadSessionEntryType> => ({
cfg: { session: { mainKey: "agent:main:main" } } as OpenClawConfig,
storePath: "/tmp/sessions.json",
store: {} as ReturnType<typeof loadSessionEntryType>["store"],
entry: {
sessionId: entry.sessionId ?? `sid-${sessionKey}`,
updatedAt: entry.updatedAt ?? Date.now(),
lastChannel: entry.lastChannel,
lastTo: entry.lastTo,
},
canonicalKey: sessionKey,
legacyKey: undefined,
});
vi.mock("../infra/system-events.js", () => ({
enqueueSystemEvent: vi.fn(),
@@ -17,11 +41,7 @@ vi.mock("../config/sessions.js", () => ({
updateSessionStore: vi.fn(),
}));
vi.mock("./session-utils.js", () => ({
loadSessionEntry: vi.fn((sessionKey: string) => ({
storePath: "/tmp/sessions.json",
entry: { sessionId: `sid-${sessionKey}` },
canonicalKey: sessionKey,
})),
loadSessionEntry: vi.fn((sessionKey: string) => buildSessionLookup(sessionKey)),
pruneLegacyStoreKeys: vi.fn(),
resolveGatewaySessionStoreTarget: vi.fn(({ key }: { key: string }) => ({
canonicalKey: key,
@@ -30,12 +50,12 @@ vi.mock("./session-utils.js", () => ({
}));
import type { CliDeps } from "../cli/deps.js";
import type { HealthSummary } from "../commands/health.js";
import type { NodeEventContext } from "./server-node-events-types.js";
import { agentCommand } from "../commands/agent.js";
import type { HealthSummary } from "../commands/health.js";
import { updateSessionStore } from "../config/sessions.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import type { NodeEventContext } from "./server-node-events-types.js";
import { handleNodeEvent } from "./server-node-events.js";
import { loadSessionEntry } from "./session-utils.js";
@@ -279,11 +299,7 @@ describe("agent request events", () => {
updateSessionStoreMock.mockImplementation(async (_storePath, update) => {
update({});
});
loadSessionEntryMock.mockImplementation((sessionKey: string) => ({
storePath: "/tmp/sessions.json",
entry: { sessionId: `sid-${sessionKey}` },
canonicalKey: sessionKey,
}));
loadSessionEntryMock.mockImplementation((sessionKey: string) => buildSessionLookup(sessionKey));
});
it("disables delivery when route is unresolved instead of falling back globally", async () => {
@@ -317,12 +333,11 @@ describe("agent request events", () => {
it("reuses the current session route when delivery target is omitted", async () => {
const ctx = buildCtx();
loadSessionEntryMock.mockReturnValueOnce({
storePath: "/tmp/sessions.json",
entry: {
...buildSessionLookup("agent:main:main", {
sessionId: "sid-current",
lastChannel: "telegram",
lastTo: "123",
},
}),
canonicalKey: "agent:main:main",
});

View File

@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
import type { NodeEvent, NodeEventContext } from "./server-node-events-types.js";
import { resolveSessionAgentId } from "../agents/agent-scope.js";
import { normalizeChannelId } from "../channels/plugins/index.js";
import { createOutboundSendDeps } from "../cli/outbound-send-deps.js";
@@ -14,6 +13,7 @@ import { normalizeMainKey } from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js";
import { parseMessageWithAttachments } from "./chat-attachments.js";
import { normalizeRpcAttachmentsToChatAttachments } from "./server-methods/attachment-normalize.js";
import type { NodeEvent, NodeEventContext } from "./server-node-events-types.js";
import {
loadSessionEntry,
pruneLegacyStoreKeys,

View File

@@ -10,12 +10,13 @@ const convertMarkdownTablesMock = vi.hoisted(() => vi.fn((text: string) => text)
const resolveMarkdownTableModeMock = vi.hoisted(() => vi.fn(() => "code"));
vi.mock("../send.js", () => ({
sendMessageIMessage: (...args: unknown[]) => sendMessageIMessageMock(...args),
sendMessageIMessage: (to: string, message: string, opts?: unknown) =>
sendMessageIMessageMock(to, message, opts),
}));
vi.mock("../../auto-reply/chunk.js", () => ({
chunkTextWithMode: (...args: unknown[]) => chunkTextWithModeMock(...args),
resolveChunkMode: (...args: unknown[]) => resolveChunkModeMock(...args),
chunkTextWithMode: (text: string) => chunkTextWithModeMock(text),
resolveChunkMode: () => resolveChunkModeMock(),
}));
vi.mock("../../config/config.js", () => ({
@@ -23,11 +24,11 @@ vi.mock("../../config/config.js", () => ({
}));
vi.mock("../../config/markdown-tables.js", () => ({
resolveMarkdownTableMode: (...args: unknown[]) => resolveMarkdownTableModeMock(...args),
resolveMarkdownTableMode: () => resolveMarkdownTableModeMock(),
}));
vi.mock("../../markdown/tables.js", () => ({
convertMarkdownTables: (...args: unknown[]) => convertMarkdownTablesMock(...args),
convertMarkdownTables: (text: string) => convertMarkdownTablesMock(text),
}));
import { deliverReplies } from "./deliver.js";

View File

@@ -1,9 +1,9 @@
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { signalOutbound } from "../../channels/plugins/outbound/signal.js";
import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js";
import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js";
import type { OpenClawConfig } from "../../config/config.js";
import { STATE_DIR } from "../../config/paths.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { markdownToSignalTextChunks } from "../../signal/format.js";

View File

@@ -1,38 +1,38 @@
import type { ReplyPayload } from "../../auto-reply/types.js";
import type {
ChannelOutboundAdapter,
ChannelOutboundContext,
} from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { sendMessageDiscord } from "../../discord/send.js";
import type { sendMessageIMessage } from "../../imessage/send.js";
import type { sendMessageSlack } from "../../slack/send.js";
import type { sendMessageTelegram } from "../../telegram/send.js";
import type { sendMessageWhatsApp } from "../../web/outbound.js";
import type { OutboundIdentity } from "./identity.js";
import type { NormalizedOutboundPayload } from "./payloads.js";
import type { OutboundChannel } from "./targets.js";
import {
chunkByParagraph,
chunkMarkdownTextWithMode,
resolveChunkMode,
resolveTextChunkLimit,
} from "../../auto-reply/chunk.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveChannelMediaMaxBytes } from "../../channels/plugins/media-limits.js";
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
import type {
ChannelOutboundAdapter,
ChannelOutboundContext,
} from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
import {
appendAssistantMessageToSessionTranscript,
resolveMirroredTranscriptText,
} from "../../config/sessions.js";
import type { sendMessageDiscord } from "../../discord/send.js";
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
import type { sendMessageIMessage } from "../../imessage/send.js";
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { markdownToSignalTextChunks, type SignalTextStyleRange } from "../../signal/format.js";
import { sendMessageSignal } from "../../signal/send.js";
import type { sendMessageSlack } from "../../slack/send.js";
import type { sendMessageTelegram } from "../../telegram/send.js";
import type { sendMessageWhatsApp } from "../../web/outbound.js";
import { throwIfAborted } from "./abort.js";
import { ackDelivery, enqueueDelivery, failDelivery } from "./delivery-queue.js";
import type { OutboundIdentity } from "./identity.js";
import type { NormalizedOutboundPayload } from "./payloads.js";
import { normalizeReplyPayloadsForDelivery } from "./payloads.js";
import type { OutboundChannel } from "./targets.js";
export type { NormalizedOutboundPayload } from "./payloads.js";
export { normalizeOutboundPayloads } from "./payloads.js";

View File

@@ -1,11 +1,11 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
import {
collectProviderApiKeysForExecution,
executeWithApiKeyRotation,
} from "../agents/api-key-rotation.js";
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
import type { MsgContext } from "../auto-reply/templating.js";
import { applyTemplate } from "../auto-reply/templating.js";
import type { OpenClawConfig } from "../config/config.js";
@@ -400,6 +400,7 @@ export async function runProviderEntry(params: {
if (!provider.transcribeAudio) {
throw new Error(`Audio transcription provider "${providerId}" not available.`);
}
const transcribeAudio = provider.transcribeAudio;
const media = await params.cache.getBuffer({
attachmentIndex: params.attachmentIndex,
maxBytes,
@@ -434,7 +435,7 @@ export async function runProviderEntry(params: {
provider: providerId,
apiKeys,
execute: async (apiKey) =>
provider.transcribeAudio({
transcribeAudio({
buffer: media.buffer,
fileName: media.fileName,
mime: media.mime,
@@ -460,6 +461,7 @@ export async function runProviderEntry(params: {
if (!provider.describeVideo) {
throw new Error(`Video understanding provider "${providerId}" not available.`);
}
const describeVideo = provider.describeVideo;
const media = await params.cache.getBuffer({
attachmentIndex: params.attachmentIndex,
maxBytes,
@@ -489,7 +491,7 @@ export async function runProviderEntry(params: {
provider: providerId,
apiKeys,
execute: (apiKey) =>
provider.describeVideo({
describeVideo({
buffer: media.buffer,
fileName: media.fileName,
mime: media.mime,

View File

@@ -1,4 +1,3 @@
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
import {
collectProviderApiKeysForExecution,
executeWithApiKeyRotation,
@@ -6,6 +5,7 @@ import {
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
import { parseGeminiAuth } from "../infra/gemini-auth.js";
import { debugEmbeddingsLog } from "./embeddings-debug.js";
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
export type GeminiEmbeddingClient = {
baseUrl: string;

View File

@@ -1,10 +1,8 @@
import type { ReplyPayload } from "../../../auto-reply/types.js";
import type { SlackStreamSession } from "../../streaming.js";
import type { PreparedSlackMessage } from "./types.js";
import { resolveHumanDelayConfig } from "../../../agents/identity.js";
import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js";
import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js";
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
import type { ReplyPayload } from "../../../auto-reply/types.js";
import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js";
import { logAckFailure, logTypingFailure } from "../../../channels/logging.js";
import { createReplyPrefixOptions } from "../../../channels/reply-prefix.js";
@@ -18,9 +16,11 @@ import {
buildStatusFinalPreviewText,
resolveSlackStreamMode,
} from "../../stream-mode.js";
import type { SlackStreamSession } from "../../streaming.js";
import { appendSlackStream, startSlackStream, stopSlackStream } from "../../streaming.js";
import { resolveSlackThreadTargets } from "../../threading.js";
import { createSlackReplyDeliveryPlan, deliverReplies, resolveSlackThreadTs } from "../replies.js";
import type { PreparedSlackMessage } from "./types.js";
function hasMedia(payload: ReplyPayload): boolean {
return Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
@@ -180,9 +180,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
}
const text = payload.text.trim();
let plannedThreadTs: string | undefined;
try {
if (!streamSession) {
const streamThreadTs = replyPlan.nextThreadTs();
plannedThreadTs = streamThreadTs;
if (!streamThreadTs) {
logVerbose(
"slack-stream: no reply thread target for stream start, falling back to normal delivery",
@@ -211,7 +213,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
danger(`slack-stream: streaming API call failed: ${String(err)}, falling back`),
);
streamFailed = true;
await deliverNormally(payload, streamSession?.threadTs);
await deliverNormally(payload, streamSession?.threadTs ?? plannedThreadTs);
}
};

View File

@@ -1442,9 +1442,7 @@ describe("createForumTopicTelegram", () => {
message_thread_id: 272,
name: "Build Updates",
});
const api = { createForumTopic } as unknown as {
createForumTopic: typeof createForumTopic;
};
const api = { createForumTopic } as unknown as Bot["api"];
const result = await createForumTopicTelegram("telegram:group:-1001234567890:topic:271", "x", {
token: "tok",
@@ -1464,9 +1462,7 @@ describe("createForumTopicTelegram", () => {
message_thread_id: 300,
name: "Roadmap",
});
const api = { createForumTopic } as unknown as {
createForumTopic: typeof createForumTopic;
};
const api = { createForumTopic } as unknown as Bot["api"];
await createForumTopicTelegram("-1001234567890", "Roadmap", {
token: "tok",