refactor: remove vestigial runtime indirection (#124554)

* refactor: remove vestigial indirection

* test: update Slack runtime API guard
This commit is contained in:
Peter Steinberger 2026-08-16 05:31:36 -07:00 committed by GitHub
parent 66db70133b
commit f43544f752
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 267 additions and 426 deletions

View file

@ -5,32 +5,35 @@ export {
type SlackActionContext,
} from "./src/action-runtime.js";
export { listSlackDirectoryGroupsLive, listSlackDirectoryPeersLive } from "./src/directory-live.js";
export {
listEnabledSlackAccounts,
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
} from "./src/accounts.js";
export {
deleteSlackMessage,
editSlackMessage,
getSlackMemberInfo,
listEnabledSlackAccounts,
listSlackAccountIds,
listSlackEmojis,
listSlackPins,
listSlackReactions,
monitorSlackProvider,
pinSlackMessage,
probeSlack,
reactSlackMessage,
readSlackMessages,
removeOwnSlackReactions,
removeSlackReaction,
resolveDefaultSlackAccountId,
resolveSlackAccount,
resolveSlackAppToken,
resolveSlackBotToken,
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
sendMessageSlack,
sendSlackMessage,
unpinSlackMessage,
} from "./src/index.js";
} from "./src/actions.js";
export {
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
} from "./src/group-policy.js";
export { monitorSlackProvider } from "./src/monitor.js";
export { probeSlack } from "./src/probe.js";
export { sendMessageSlack } from "./src/send.js";
export { resolveSlackAppToken, resolveSlackBotToken } from "./src/token.js";
export {
resolveSlackChannelAllowlist,
type SlackChannelLookup,

View file

@ -1,27 +0,0 @@
// Slack plugin entrypoint registers its OpenClaw integration.
export {
listEnabledSlackAccounts,
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
} from "./accounts.js";
export {
deleteSlackMessage,
editSlackMessage,
getSlackMemberInfo,
listSlackEmojis,
listSlackPins,
listSlackReactions,
pinSlackMessage,
reactSlackMessage,
readSlackMessages,
removeOwnSlackReactions,
removeSlackReaction,
sendSlackMessage,
unpinSlackMessage,
} from "./actions.js";
export { monitorSlackProvider } from "./monitor.js";
export { probeSlack } from "./probe.js";
export { sendMessageSlack } from "./send.js";
export { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./group-policy.js";
export { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";

View file

@ -1,4 +1,5 @@
// Voice Call plugin entrypoint registers its OpenClaw integration.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { ErrorCodes, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
@ -28,7 +29,6 @@ import {
validateProviderConfig,
type VoiceCallConfig,
} from "./src/config.js";
import type { CoreConfig } from "./src/core-bridge.js";
import { createVoiceCallContinueOperationStore } from "./src/gateway-continue-operation.js";
const VOICE_CALL_WRITE_METHOD_SCOPE = { scope: "operator.write" as const };
@ -176,7 +176,7 @@ export default definePluginEntry({
};
const continueOperationStore = createVoiceCallContinueOperationStore({
config,
coreConfig: api.config as CoreConfig,
coreConfig: api.config as OpenClawConfig,
});
const ensureRuntime = async (): Promise<VoiceCallRuntime> => {
@ -233,7 +233,7 @@ export default definePluginEntry({
const runtimePromise = createVoiceCallRuntime({
config,
coreConfig: api.config as CoreConfig,
coreConfig: api.config as OpenClawConfig,
fullConfig: api.config,
agentRuntime: api.runtime.agent,
stateRuntime: api.runtime.state,

View file

@ -1,11 +0,0 @@
// Voice Call plugin module implements core bridge behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { OpenClawPluginApi } from "../api.js";
// Narrow core runtime/config contracts consumed by the voice-call plugin.
/** Core config subset read by voice-call helpers. */
export type CoreConfig = OpenClawConfig;
/** Agent runtime API subset exposed through the plugin SDK. */
export type CoreAgentDeps = OpenClawPluginApi["runtime"]["agent"];

View file

@ -1,9 +1,9 @@
// Voice Call plugin module implements gateway continue operation behavior.
import { randomUUID } from "node:crypto";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import type { VoiceCallConfig } from "./config.js";
import type { CoreConfig } from "./core-bridge.js";
import type { VoiceCallRuntime } from "./runtime.js";
import { TELEPHONY_DEFAULT_TTS_TIMEOUT_MS } from "./telephony-tts.js";
@ -75,7 +75,7 @@ type VoiceCallContinueOperationRequest = {
/** Create a process-local operation store for gateway continue-call polling. */
export function createVoiceCallContinueOperationStore(params: {
config: VoiceCallConfig;
coreConfig: CoreConfig;
coreConfig: OpenClawConfig;
}) {
const operations = new Map<string, VoiceCallContinueOperation>();

View file

@ -4,8 +4,8 @@ import { tmpdir } from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "../api.js";
import type { VoiceCallConfig } from "./config.js";
import type { CoreAgentDeps } from "./core-bridge.js";
import { buildRealtimeVoiceInstructions } from "./realtime-agent-context.js";
import { createVoiceCallBaseConfig } from "./test-fixtures.js";
@ -45,7 +45,7 @@ function createConfig(overrides?: Partial<VoiceCallConfig["realtime"]>): VoiceCa
return config;
}
function createAgentRuntime(workspaceDir: string): CoreAgentDeps {
function createAgentRuntime(workspaceDir: string): OpenClawPluginApi["runtime"]["agent"] {
return {
resolveAgentIdentity: vi.fn(() => ({
name: "Claw Voice",
@ -55,7 +55,7 @@ function createAgentRuntime(workspaceDir: string): CoreAgentDeps {
creature: "operator",
})),
resolveAgentWorkspaceDir: vi.fn(() => workspaceDir),
} as unknown as CoreAgentDeps;
} as unknown as OpenClawPluginApi["runtime"]["agent"];
}
describe("buildRealtimeVoiceInstructions", () => {

View file

@ -4,8 +4,8 @@ import { buildRealtimeVoiceAgentConsultPolicyInstructions } from "openclaw/plugi
import { root } from "openclaw/plugin-sdk/security-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { OpenClawPluginApi } from "../api.js";
import type { VoiceCallConfig } from "./config.js";
import type { CoreAgentDeps } from "./core-bridge.js";
// Builds compact agent context injected into realtime voice sessions.
@ -60,7 +60,7 @@ export async function buildRealtimeVoiceInstructions(params: {
baseInstructions: string;
config: VoiceCallConfig;
coreConfig: OpenClawConfig;
agentRuntime: CoreAgentDeps;
agentRuntime: OpenClawPluginApi["runtime"]["agent"];
agentId: string;
}): Promise<string> {
const { config } = params;

View file

@ -1,7 +1,8 @@
// Voice Call tests cover response generator plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "../api.js";
import { VoiceCallConfigSchema } from "./config.js";
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
import { generateVoiceResponse } from "./response-generator.js";
type TestSessionEntry = {
@ -109,13 +110,13 @@ function createAgentRuntime(
run: (signal: AbortSignal) => Promise<unknown>,
) => await run(new AbortController().signal),
);
const resolveAgentDir = vi.fn((_cfg: CoreConfig, agentId: string) => {
const resolveAgentDir = vi.fn((_cfg: OpenClawConfig, agentId: string) => {
return `/tmp/openclaw/agents/${agentId}`;
});
const resolveAgentWorkspaceDir = vi.fn((_cfg: CoreConfig, agentId: string) => {
const resolveAgentWorkspaceDir = vi.fn((_cfg: OpenClawConfig, agentId: string) => {
return `/tmp/openclaw/workspace/${agentId}`;
});
const resolveAgentIdentity = vi.fn((_cfg: CoreConfig, agentId: string) => ({
const resolveAgentIdentity = vi.fn((_cfg: OpenClawConfig, agentId: string) => ({
name: `${agentId} tester`,
}));
const resolveStorePath = vi.fn((_store: string | undefined, params: { agentId?: string }) => {
@ -150,7 +151,7 @@ function createAgentRuntime(
runWithWorkAdmission,
resolveSessionFilePath,
},
} as unknown as CoreAgentDeps;
} as unknown as OpenClawPluginApi["runtime"]["agent"];
return {
runtime,
@ -192,7 +193,7 @@ function requireFirstMockCall(calls: readonly unknown[][], label: string): unkno
async function runGenerateVoiceResponse(
payloads: Array<Record<string, unknown>>,
overrides?: {
runtime?: CoreAgentDeps;
runtime?: OpenClawPluginApi["runtime"]["agent"];
transcript?: Array<{ speaker: "user" | "bot"; text: string }>;
onEarlyText?: (text: string) => Promise<boolean>;
},
@ -200,7 +201,7 @@ async function runGenerateVoiceResponse(
const voiceConfig = VoiceCallConfigSchema.parse({
responseTimeoutMs: 5000,
});
const coreConfig = {} as CoreConfig;
const coreConfig = {} as OpenClawConfig;
const runtime = overrides?.runtime ?? createAgentRuntime(payloads).runtime;
const result = await generateVoiceResponse({
@ -531,7 +532,7 @@ describe("generateVoiceResponse", () => {
const result = await generateVoiceResponse({
voiceConfig,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: runtime,
callId: "call-123",
from: "+15550001111",
@ -580,7 +581,7 @@ describe("generateVoiceResponse", () => {
const result = await generateVoiceResponse({
voiceConfig,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: runtime,
callId: "call-123",
from: "+15550001111",
@ -626,7 +627,7 @@ describe("generateVoiceResponse", () => {
const result = await generateVoiceResponse({
voiceConfig,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: runtime,
callId: "call-123",
sessionKey,
@ -657,7 +658,7 @@ describe("generateVoiceResponse", () => {
const result = await generateVoiceResponse({
voiceConfig,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: runtime,
callId: "call-123",
sessionKey: "voice:call:call-123",
@ -687,7 +688,7 @@ describe("generateVoiceResponse", () => {
await generateVoiceResponse({
voiceConfig,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: runtime,
callId: "call-123",
sessionKey: "meet-room-1",
@ -713,7 +714,7 @@ describe("generateVoiceResponse", () => {
const generate = (sessionKey: string) =>
generateVoiceResponse({
voiceConfig,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: runtime,
callId: "call-123",
sessionKey,
@ -773,7 +774,7 @@ describe("generateVoiceResponse", () => {
resolveStorePath,
sessionStore,
} = createAgentRuntime([{ text: '{"spoken":"Default agent."}' }]);
const coreConfig = {} as CoreConfig;
const coreConfig = {} as OpenClawConfig;
await generateVoiceResponse({
voiceConfig: VoiceCallConfigSchema.parse({ responseTimeoutMs: 5000 }),
@ -818,7 +819,7 @@ describe("generateVoiceResponse", () => {
resolveStorePath,
sessionStore,
} = createAgentRuntime([{ text: '{"spoken":"Voice agent."}' }]);
const coreConfig = {} as CoreConfig;
const coreConfig = {} as OpenClawConfig;
const result = await generateVoiceResponse({
voiceConfig: VoiceCallConfigSchema.parse({
@ -864,7 +865,7 @@ describe("generateVoiceResponse", () => {
await generateVoiceResponse({
voiceConfig: VoiceCallConfigSchema.parse({ agentId: "voice", responseTimeoutMs: 5000 }),
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: runtime,
callId: "call-123",
agentId: "support",
@ -894,7 +895,7 @@ describe("generateVoiceResponse", () => {
},
],
},
} as CoreConfig;
} as OpenClawConfig;
const result = await generateVoiceResponse({
voiceConfig: VoiceCallConfigSchema.parse({

View file

@ -5,6 +5,7 @@
import crypto from "node:crypto";
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
applyModelOverrideWithAuthProfileCompatibility,
ModelSelectionLockedError,
@ -16,8 +17,8 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { OpenClawPluginApi } from "../api.js";
import { resolveVoiceCallSessionKey, type VoiceCallConfig } from "./config.js";
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
import { resolveCallAgentId } from "./resolve-call-agent-id.js";
import { resolveVoiceResponseModel } from "./response-model.js";
@ -25,9 +26,9 @@ type VoiceResponseParams = {
/** Voice call config */
voiceConfig: VoiceCallConfig;
/** Core OpenClaw config */
coreConfig: CoreConfig;
coreConfig: OpenClawConfig;
/** Injected host agent runtime */
agentRuntime: CoreAgentDeps;
agentRuntime: OpenClawPluginApi["runtime"]["agent"];
/** Call ID for session tracking */
callId: string;
/** Persisted call session key */
@ -70,7 +71,10 @@ function readExplicitToolsAllow(value: unknown): string[] | undefined {
return filterStringEntries(allow);
}
function resolveVoiceAgentToolsAllow(config: CoreConfig, agentId: string): string[] | undefined {
function resolveVoiceAgentToolsAllow(
config: OpenClawConfig,
agentId: string,
): string[] | undefined {
const agents = isRecord(config.agents) ? config.agents : undefined;
const list = Array.isArray(agents?.list) ? agents.list : [];
const agent = list.find((entry) => isRecord(entry) && entry.id === agentId);

View file

@ -1,7 +1,7 @@
// Voice Call tests cover response model plugin behavior.
import { describe, expect, it } from "vitest";
import type { OpenClawPluginApi } from "../api.js";
import { VoiceCallConfigSchema } from "./config.js";
import type { CoreAgentDeps } from "./core-bridge.js";
import { resolveVoiceResponseModel } from "./response-model.js";
const agentRuntime = {
@ -9,7 +9,7 @@ const agentRuntime = {
provider: "together",
model: "Qwen/Qwen2.5-7B-Instruct-Turbo",
},
} as unknown as CoreAgentDeps;
} as unknown as OpenClawPluginApi["runtime"]["agent"];
describe("resolveVoiceResponseModel", () => {
it("falls back to the runtime default model", () => {

View file

@ -1,13 +1,13 @@
// Voice Call plugin module implements response model behavior.
import type { OpenClawPluginApi } from "../api.js";
import type { VoiceCallConfig } from "./config.js";
import type { CoreAgentDeps } from "./core-bridge.js";
// Resolves the model used for voice-call text response generation.
/** Resolve provider/model fields from explicit voice config or agent defaults. */
export function resolveVoiceResponseModel(params: {
voiceConfig: VoiceCallConfig;
agentRuntime: CoreAgentDeps;
agentRuntime: OpenClawPluginApi["runtime"]["agent"];
}): {
modelRef: string;
provider: string;

View file

@ -3,7 +3,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { VoiceCallConfig } from "./config.js";
import type { CoreConfig } from "./core-bridge.js";
import { createVoiceCallBaseConfig } from "./test-fixtures.js";
const mocks = vi.hoisted(() => ({
@ -293,7 +292,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
const runtime = await createVoiceCallRuntime({
config: createBaseConfig(),
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: {} as never,
});
@ -320,7 +319,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
});
it("passes fullConfig to the webhook server for streaming provider resolution", async () => {
const coreConfig = { tts: { provider: "openai" } } as CoreConfig;
const coreConfig = { tts: { provider: "openai" } } as OpenClawConfig;
const fullConfig = {
plugins: {
entries: {
@ -359,7 +358,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
const runtime = await createVoiceCallRuntime({
config,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
fullConfig,
agentRuntime: {
resolveAgentIdentity,
@ -407,7 +406,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
await expect(
createVoiceCallRuntime({
config: createExternalProviderConfig({ provider }),
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: {} as never,
}),
).rejects.toThrow(`${provider} requires a publicly reachable webhook URL`);
@ -426,7 +425,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
provider: "twilio",
publicUrl,
}),
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: {} as never,
}),
).rejects.toThrow("twilio requires a publicly reachable webhook URL");
@ -439,7 +438,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
provider: "twilio",
publicUrl: "https://voice.example.com/voice/webhook",
}),
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: {} as never,
});
@ -461,7 +460,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
provider: "twilio",
publicUrl: "https://voice.example.com/voice/webhook",
}),
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: {} as never,
logger,
});
@ -520,7 +519,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
await createVoiceCallRuntime({
config,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: agentRuntime as never,
});
@ -595,7 +594,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
await createVoiceCallRuntime({
config,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: agentRuntime as never,
});
@ -643,7 +642,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
await createVoiceCallRuntime({
config,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: agentRuntime as never,
});
@ -694,7 +693,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
await createVoiceCallRuntime({
config,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: agentRuntime as never,
});
@ -747,7 +746,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
await createVoiceCallRuntime({
config,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: agentRuntime as never,
});
@ -813,7 +812,7 @@ describe("createVoiceCallRuntime lifecycle", () => {
await createVoiceCallRuntime({
config,
coreConfig: {} as CoreConfig,
coreConfig: {} as OpenClawConfig,
agentRuntime: agentRuntime as never,
});

View file

@ -14,6 +14,7 @@ import {
type ResolvedRealtimeVoiceProvider,
} from "openclaw/plugin-sdk/realtime-voice";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import type { OpenClawPluginApi } from "../api.js";
import type { VoiceCallConfig } from "./config.js";
import {
resolveVoiceCallEffectiveConfig,
@ -23,7 +24,6 @@ import {
resolveVoiceCallConfig,
validateProviderConfig,
} from "./config.js";
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
import { CallManager } from "./manager.js";
import type { VoiceCallProvider } from "./providers/base.js";
import type { TwilioProvider } from "./providers/twilio.js";
@ -257,7 +257,7 @@ function listRealtimeAgentIds(config: VoiceCallConfig, coreConfig: OpenClawConfi
async function createRealtimeInstructionsResolver(params: {
config: VoiceCallConfig & { agentId: string };
coreConfig: OpenClawConfig;
agentRuntime: CoreAgentDeps;
agentRuntime: OpenClawPluginApi["runtime"]["agent"];
}): Promise<(call: CallRecord) => string> {
const genericConfig: VoiceCallConfig = {
...params.config,
@ -297,9 +297,9 @@ async function createRealtimeInstructionsResolver(params: {
export async function createVoiceCallRuntime(params: {
config: VoiceCallConfig;
coreConfig: CoreConfig;
coreConfig: OpenClawConfig;
fullConfig?: OpenClawConfig;
agentRuntime: CoreAgentDeps;
agentRuntime: OpenClawPluginApi["runtime"]["agent"];
stateRuntime?: VoiceCallStateRuntime["state"];
ttsRuntime?: TelephonyTtsRuntime;
logger?: Logger;

View file

@ -23,6 +23,7 @@ import {
readRequestBodyWithLimit,
requestBodyErrorToText,
} from "../api.js";
import type { OpenClawPluginApi } from "../api.js";
import { isAllowlistedCaller, normalizePhoneNumber } from "./allowlist.js";
import {
normalizeVoiceCallConfig,
@ -30,7 +31,6 @@ import {
resolveVoiceCallNumberRouteKeyForCall,
type VoiceCallConfig,
} from "./config.js";
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
import { getHeader } from "./http-headers.js";
import type { CallManager } from "./manager.js";
import type { MediaStreamConfig } from "./media-stream.js";
@ -183,9 +183,9 @@ export class VoiceCallWebhookServer {
private config: VoiceCallConfig;
private manager: CallManager;
private provider: VoiceCallProvider;
private coreConfig: CoreConfig | null;
private coreConfig: OpenClawConfig | null;
private fullConfig: OpenClawConfig | null;
private agentRuntime: CoreAgentDeps | null;
private agentRuntime: OpenClawPluginApi["runtime"]["agent"] | null;
private logger: Logger;
private stopStaleCallReaper: (() => void) | null = null;
private readonly webhookInFlightLimiter = createWebhookInFlightLimiter();
@ -203,9 +203,9 @@ export class VoiceCallWebhookServer {
config: VoiceCallConfig,
manager: CallManager,
provider: VoiceCallProvider,
coreConfig?: CoreConfig,
coreConfig?: OpenClawConfig,
fullConfig?: OpenClawConfig,
agentRuntime?: CoreAgentDeps,
agentRuntime?: OpenClawPluginApi["runtime"]["agent"],
logger?: Logger,
) {
this.config = normalizeVoiceCallConfig(config);

View file

@ -55,56 +55,28 @@ type NativeCommandProviderLookupOptions = {
includeBundledChannelFallback?: boolean;
};
/** Resolves provider-specific native command names while preserving registry defaults. */
function resolveNativeName(
command: ChatCommandDefinition,
function createNativeCommandNameMapper(
provider?: string,
options?: NativeCommandProviderLookupOptions,
): string | undefined {
if (!command.nativeName) {
return undefined;
}
if (!provider) {
return command.nativeName;
}
const channelPlugin =
options?.includeBundledChannelFallback === false
? getLoadedChannelPlugin(provider)
: getChannelPlugin(provider);
return (
channelPlugin?.commands?.resolveNativeCommandName?.({
commandKey: command.key,
defaultName: command.nativeName,
}) ?? command.nativeName
);
}
function toNativeCommandSpec(
command: ChatCommandDefinition,
provider?: string,
options?: NativeCommandProviderLookupOptions,
): NativeCommandSpec {
const spec: NativeCommandSpec = {
name: resolveNativeName(command, provider, options) ?? command.key,
description: command.description,
acceptsArgs: Boolean(command.acceptsArgs),
args: command.args,
): (command: ChatCommandDefinition) => Array<{ name: string; normalizedName?: string }> {
// Registry state is lifecycle-owned, so resolve the adapter once per list or lookup operation.
const resolveNativeCommandName = !provider
? undefined
: (options?.includeBundledChannelFallback === false
? getLoadedChannelPlugin(provider)
: getChannelPlugin(provider)
)?.commands?.resolveNativeCommandName;
return (command) => {
const primary = command.nativeName
? (resolveNativeCommandName?.({
commandKey: command.key,
defaultName: command.nativeName,
}) ?? command.nativeName)
: undefined;
return [primary, ...(command.nativeAliases ?? [])]
.filter((name): name is string => Boolean(name))
.map((name) => ({ name, normalizedName: normalizeOptionalLowercaseString(name) }));
};
if (command.descriptionLocalizations) {
spec.descriptionLocalizations = command.descriptionLocalizations;
}
return spec;
}
function resolveNativeNames(
command: ChatCommandDefinition,
provider?: string,
options?: NativeCommandProviderLookupOptions,
): string[] {
const primary = resolveNativeName(command, provider, options);
return [primary, ...(command.nativeAliases ?? [])].filter((name): name is string =>
Boolean(name),
);
}
function supportsNativeProvider(command: ChatCommandDefinition, provider?: string): boolean {
@ -125,28 +97,28 @@ function listNativeSpecsFromCommands(
provider?: string,
options?: NativeCommandProviderLookupOptions,
): NativeCommandSpec[] {
const mapNativeCommandNames = createNativeCommandNameMapper(provider, options);
return commands
.filter(
(command) =>
command.scope !== "text" && command.nativeName && supportsNativeProvider(command, provider),
)
.flatMap((command) => {
const spec = toNativeCommandSpec(command, provider, options);
return resolveNativeNames(command, provider, options).map((name, index) => {
return mapNativeCommandNames(command).map(({ name }, index) => {
const nativeSpec: NativeCommandSpec = {
name,
description: spec.description,
acceptsArgs: spec.acceptsArgs,
description: command.description,
acceptsArgs: Boolean(command.acceptsArgs),
};
// Native aliases carry the same payload shape but are marked for channel registration.
if (index > 0) {
nativeSpec.isAlias = true;
}
if (spec.args) {
nativeSpec.args = spec.args;
if (command.args) {
nativeSpec.args = command.args;
}
if (spec.descriptionLocalizations) {
nativeSpec.descriptionLocalizations = spec.descriptionLocalizations;
if (command.descriptionLocalizations) {
nativeSpec.descriptionLocalizations = command.descriptionLocalizations;
}
return nativeSpec;
});
@ -222,13 +194,12 @@ export function findCommandByNativeName(
if (!normalized) {
return undefined;
}
const mapNativeCommandNames = createNativeCommandNameMapper(provider, options);
return getChatCommands().find(
(command) =>
command.scope !== "text" &&
supportsNativeProvider(command, provider) &&
[resolveNativeName(command, provider, options), ...(command.nativeAliases ?? [])].some(
(nameLocal) => normalizeOptionalLowercaseString(nameLocal) === normalized,
),
mapNativeCommandNames(command).some(({ normalizedName }) => normalizedName === normalized),
);
}

View file

@ -1,33 +0,0 @@
// Public channel ingress/message-access barrel. Keep this as the narrow import
// point for callers that need access decisions without plugin internals.
export { defineStableChannelIngressIdentity } from "./runtime-identity.js";
export {
channelIngressRoutes,
createChannelIngressResolver,
resolveChannelMessageIngress,
resolveStableChannelMessageIngress,
} from "./runtime.js";
export { readChannelIngressStoreAllowFromForDmPolicy } from "./store-allow-from.js";
export type {
ChannelIngressAccessGroupMembershipResolver,
ChannelIngressCommandPresetInput,
ChannelIngressConfigInput,
ChannelIngressContextBinding,
ChannelIngressEventPresetInput,
ChannelIngressIdentityAlias,
ChannelIngressIdentityDescriptor,
ChannelIngressIdentityField,
ChannelIngressIdentitySubjectInput,
ChannelIngressRouteAccess,
ChannelIngressRouteDescriptor,
ChannelIngressResolver,
ChannelIngressResolverMessageParams,
ChannelMessageIngressCommandInput,
CreateChannelIngressResolverParams,
ResolvedChannelMessageIngress,
ResolveChannelMessageIngressParams,
ResolveStableChannelMessageIngressParams,
StableChannelIngressIdentityParams,
} from "./runtime-types.js";
export type * from "./types.js";

View file

@ -1,13 +1,13 @@
// Message access tests cover channel message visibility and permission helpers.
import { describe, expect, it } from "vitest";
import { decideChannelIngress } from "./decision.js";
import { resolveChannelIngressState } from "./state.js";
import type {
ChannelIngressPolicyInput,
ChannelIngressStateInput,
InternalChannelIngressAdapter,
InternalChannelIngressSubject,
} from "./index.js";
import { resolveChannelIngressState } from "./state.js";
} from "./types.js";
const subject = (value: string): InternalChannelIngressSubject => ({
identifiers: [{ opaqueId: "subject-1", kind: "stable-id", value }],

View file

@ -1,74 +0,0 @@
// Public barrel for channel message delivery, live preview, receipt, receive, and recovery
// contracts used by channel plugins and core delivery code.
export { deriveDurableFinalDeliveryRequirements } from "./capabilities.js";
export { defineChannelMessageAdapter } from "./adapter.js";
export { createChannelMessageAdapterFromOutbound } from "./outbound-bridge.js";
export { createDurableInboundReceiveJournalFromQueue } from "./durable-receive.js";
export { INGRESS_CLAIM_PROCESS_ID, processPidFromOwnerId } from "./ingress-claim-owner.js";
export {
bindIngressLifecycleToReplyOptions,
createChannelIngressDrain,
DEFAULT_INGRESS_ADOPTION_STALL_MS,
} from "./ingress-drain.js";
export {
CHANNEL_INGRESS_RETENTION_DEFAULTS,
createChannelIngressError,
createChannelIngressMonitor,
} from "./ingress-monitor.js";
export {
DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS,
DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
} from "./ingress-retry-policy.js";
export {
verifyChannelMessageAdapterCapabilityProofs,
verifyChannelMessageLiveCapabilityAdapterProofs,
verifyChannelMessageLiveFinalizerProofs,
verifyChannelMessageReceiveAckPolicyAdapterProofs,
verifyDurableFinalCapabilityProofs,
} from "./contracts.js";
export {
createPreviewMessageReceipt,
defineFinalizableLivePreviewAdapter,
deliverWithFinalizableLivePreviewAdapter,
} from "./live.js";
export {
createMessageReceiptFromOutboundResults,
listMessageReceiptPlatformIds,
resolveMessageReceiptPrimaryId,
} from "./receipt.js";
export { createMessageReceiveContext } from "./receive.js";
export {
createChannelReplyPipeline,
createReplyPrefixContext,
createReplyPrefixOptions,
createTypingCallbacks,
resolveChannelSourceReplyDeliveryMode,
} from "./reply-pipeline.js";
export type { ChannelIngressDrain } from "./ingress-drain.js";
export type {
ChannelIngressMonitorDeliveryResult,
ChannelIngressMonitorLifecycle,
} from "./ingress-monitor.js";
export type {
ChannelIngressQueue,
ChannelIngressQueueClaim,
ChannelIngressQueueClaimRef,
ChannelIngressQueueCorruptClaim,
ChannelIngressQueueRecord,
} from "./ingress-queue.js";
export type { MessageAckPolicy, MessageReceiveContext } from "./receive.js";
export type {
ChannelMessageAdapterShape,
ChannelMessageDurableFinalAdapter,
ChannelMessageSendMediaContext,
ChannelMessageSendPayloadContext,
ChannelMessageSendResult,
ChannelMessageSendTextContext,
ChannelMessageUnknownSendContext,
ChannelMessageUnknownSendReconciliationResult,
MessageReceipt,
MessageReceiptPart,
MessageReceiptPartKind,
MessageReceiptSourceResult,
} from "./types.js";

View file

@ -109,15 +109,6 @@ vi.mock("../../config/paths.js", () => ({
resolveIsNixMode: resolveIsNixModeMock,
}));
vi.mock("../../commands/gateway-install-token.persist.runtime.js", () => ({
readConfigFileSnapshot: readConfigFileSnapshotMock,
readConfigFileSnapshotForWrite: vi.fn(async () => ({
snapshot: await readConfigFileSnapshotMock(),
writeOptions: { expectedConfigPath: "/tmp/openclaw.json" },
})),
replaceConfigFile: replaceConfigFileMock,
}));
vi.mock("../../config/types.secrets.js", () => ({
hasConfiguredSecretInput: hasConfiguredSecretInputMock,
resolveSecretInputRef: resolveSecretInputRefMock,

View file

@ -309,6 +309,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
explicitToken: opts.token,
autoGenerateWhenMissing: true,
persistGeneratedToken: true,
persistence: { readConfigFileSnapshotForWrite, replaceConfigFile },
});
if (tokenResolution.unavailableReason) {
fail(`Gateway install blocked: ${tokenResolution.unavailableReason}`);

View file

@ -1,3 +0,0 @@
/** Runtime persistence seam for gateway install token config writes. */
export { readConfigFileSnapshotForWrite } from "../config/io.js";
export { replaceConfigFile } from "../config/mutate.js";

View file

@ -19,11 +19,6 @@ const resolveSecretRefValuesMock = vi.hoisted(() => vi.fn());
const secretRefKeyMock = vi.hoisted(() => vi.fn(() => "env:default:OPENCLAW_GATEWAY_TOKEN"));
const randomTokenMock = vi.hoisted(() => vi.fn(() => "generated-token"));
vi.mock("./gateway-install-token.persist.runtime.js", () => ({
readConfigFileSnapshotForWrite: readConfigFileSnapshotForWriteMock,
replaceConfigFile: replaceConfigFileMock,
}));
vi.mock("../gateway/auth.js", () => ({
resolveGatewayAuth: resolveGatewayAuthMock,
}));
@ -32,7 +27,8 @@ vi.mock("../gateway/auth-install-policy.js", () => ({
shouldRequireGatewayTokenForInstall: shouldRequireGatewayTokenForInstallMock,
}));
vi.mock("../secrets/ref-contract.js", () => ({
vi.mock("../secrets/ref-contract.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../secrets/ref-contract.js")>()),
secretRefKey: secretRefKeyMock,
}));
@ -52,6 +48,11 @@ function firstReplaceConfigRequest(): unknown {
return call[0];
}
const persistence = {
readConfigFileSnapshotForWrite: readConfigFileSnapshotForWriteMock,
replaceConfigFile: replaceConfigFileMock,
};
describe("resolveGatewayInstallToken", () => {
beforeEach(() => {
vi.clearAllMocks();
@ -135,6 +136,7 @@ describe("resolveGatewayInstallToken", () => {
env: {} as NodeJS.ProcessEnv,
autoGenerateWhenMissing: true,
persistGeneratedToken: true,
persistence,
});
expect(result.token).toBeUndefined();
@ -171,6 +173,7 @@ describe("resolveGatewayInstallToken", () => {
env: {} as NodeJS.ProcessEnv,
autoGenerateWhenMissing: true,
persistGeneratedToken: true,
persistence,
});
expect(result.warnings.join("\n")).toContain("saving to config");
@ -214,6 +217,7 @@ describe("resolveGatewayInstallToken", () => {
env: {} as NodeJS.ProcessEnv,
autoGenerateWhenMissing: true,
persistGeneratedToken: true,
persistence,
});
expect(result.token).toBeUndefined();
@ -240,6 +244,7 @@ describe("resolveGatewayInstallToken", () => {
env: {} as NodeJS.ProcessEnv,
autoGenerateWhenMissing: true,
persistGeneratedToken: true,
persistence,
});
expect(result.token).toBeUndefined();

View file

@ -1,7 +1,8 @@
/** Resolves the gateway token used when installing or updating the managed service. */
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { formatCliCommand } from "../cli/command-format.js";
import type { ConfigWriteOptions } from "../config/io.js";
import { readConfigFileSnapshotForWrite, type ConfigWriteOptions } from "../config/io.js";
import { replaceConfigFile } from "../config/mutate.js";
import type { OpenClawConfig } from "../config/types.js";
import type { ConfigFileSnapshot } from "../config/types.openclaw.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
@ -13,11 +14,17 @@ import {
formatUnsafeGatewayTailscaleNoAuthMessage,
isUnsafeGatewayTailscaleNoAuth,
} from "../shared/gateway-tailscale-auth-policy.js";
import {
import { randomToken } from "./random-token.js";
type GatewayInstallTokenPersistence = {
readConfigFileSnapshotForWrite: typeof readConfigFileSnapshotForWrite;
replaceConfigFile: typeof replaceConfigFile;
};
const defaultGatewayInstallTokenPersistence: GatewayInstallTokenPersistence = {
readConfigFileSnapshotForWrite,
replaceConfigFile,
} from "./gateway-install-token.persist.runtime.js";
import { randomToken } from "./random-token.js";
};
type GatewayInstallTokenOptions = {
config: OpenClawConfig;
@ -27,6 +34,7 @@ type GatewayInstallTokenOptions = {
explicitToken?: string;
autoGenerateWhenMissing?: boolean;
persistGeneratedToken?: boolean;
persistence?: GatewayInstallTokenPersistence;
};
type GatewayInstallTokenResolution = {
@ -42,6 +50,7 @@ async function maybePersistAutoGeneratedGatewayInstallToken(params: {
configSnapshot?: ConfigFileSnapshot;
configWriteOptions?: ConfigWriteOptions;
warnings: string[];
persistence: GatewayInstallTokenPersistence;
}): Promise<string | undefined> {
try {
const prepared =
@ -50,7 +59,7 @@ async function maybePersistAutoGeneratedGatewayInstallToken(params: {
snapshot: params.configSnapshot,
writeOptions: params.configWriteOptions,
}
: await readConfigFileSnapshotForWrite();
: await params.persistence.readConfigFileSnapshotForWrite();
const snapshot = params.configSnapshot ?? prepared.snapshot;
if (snapshot.exists && !snapshot.valid) {
params.warnings.push(
@ -70,7 +79,7 @@ async function maybePersistAutoGeneratedGatewayInstallToken(params: {
: normalizeOptionalString(baseConfig.gateway.auth.token);
// Only persist a generated plaintext token when config has no token or SecretRef.
if (!existingTokenRef && !baseConfigToken) {
await replaceConfigFile({
await params.persistence.replaceConfigFile({
nextConfig: {
...baseConfig,
gateway: {
@ -202,6 +211,7 @@ export async function resolveGatewayInstallToken(
configSnapshot: options.configSnapshot,
configWriteOptions: options.configWriteOptions,
warnings,
persistence: options.persistence ?? defaultGatewayInstallTokenPersistence,
});
}
}

View file

@ -1,8 +0,0 @@
/** Runtime seams for loading model command config and secret target ids. */
export { getModelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
export {
getRuntimeConfig,
getRuntimeConfigSourceSnapshot,
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../../config/config.js";

View file

@ -1,13 +1,13 @@
/** Config loader for model commands with command-scoped secret resolution. */
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
import type { RuntimeEnv } from "../../runtime.js";
import { getModelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
import {
getRuntimeConfig,
getRuntimeConfigSourceSnapshot,
setRuntimeConfigSnapshot,
type OpenClawConfig,
getModelsCommandSecretTargetIds,
} from "./load-config.runtime.js";
} from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
/** Source and resolved config pair returned by model command config loading. */
type LoadedModelsConfig = {

View file

@ -23,15 +23,11 @@ import {
import { subscribePluginSessionsChanged } from "../plugins/gateway-events.js";
const persistGatewaySessionLifecycleEventMock = vi.fn();
const loadGatewaySessionLifecycleSnapshotMock = vi.hoisted(() => vi.fn());
const logErrorMock = vi.fn();
const normalizeLiveAssistantBufferedTextMock = vi.hoisted(() => vi.fn());
const loadGatewaySessionRow = vi.hoisted(() => vi.fn());
vi.mock("./server-chat.persist-session-lifecycle.runtime.js", () => ({
persistGatewaySessionLifecycleEvent: (...args: unknown[]) =>
persistGatewaySessionLifecycleEventMock(...args),
}));
vi.mock("../logger.js", () => ({
logError: (...args: unknown[]) => logErrorMock(...args),
}));
@ -59,10 +55,6 @@ vi.mock("../infra/heartbeat-visibility.js", () => ({
})),
}));
vi.mock("./server-chat.load-gateway-session-row.runtime.js", () => ({
loadGatewaySessionLifecycleSnapshot: vi.fn(),
}));
vi.mock("./session-utils.js", () => {
const loadSessionEntry = vi.fn(() => ({
cfg: {},
@ -76,6 +68,8 @@ vi.mock("./session-utils.js", () => {
return {
loadSessionEntry,
loadGatewaySessionEntryReadOnly: loadSessionEntry,
loadGatewaySessionLifecycleSnapshot: (...args: unknown[]) =>
loadGatewaySessionLifecycleSnapshotMock(...args),
};
});
@ -97,7 +91,6 @@ import {
resolveChatErrorKindFromError,
type AgentEventHandlerOptions,
} from "./server-chat.js";
import { loadGatewaySessionLifecycleSnapshot } from "./server-chat.load-gateway-session-row.runtime.js";
import { loadSessionEntry } from "./session-utils.js";
function waitForFast<T>(
@ -129,7 +122,7 @@ describe("agent event handler", () => {
legacyKey: undefined,
});
vi.mocked(loadGatewaySessionRow).mockReset().mockReturnValue(null);
vi.mocked(loadGatewaySessionLifecycleSnapshot)
loadGatewaySessionLifecycleSnapshotMock
.mockReset()
.mockImplementation((sessionKey, options) => ({
row: options
@ -183,7 +176,8 @@ describe("agent event handler", () => {
toolEventRecipients,
sessionEventSubscribers,
sessionMessageSubscribers,
loadGatewaySessionLifecycleSnapshotForEvent: loadGatewaySessionLifecycleSnapshot,
loadGatewaySessionLifecycleSnapshotForEvent: loadGatewaySessionLifecycleSnapshotMock,
persistGatewaySessionLifecycleEventForEvent: persistGatewaySessionLifecycleEventMock,
lifecycleErrorRetryGraceMs: params?.lifecycleErrorRetryGraceMs,
isChatSendRunActive: params?.isChatSendRunActive,
clearTrackedActiveRun: params?.clearTrackedActiveRun ?? clearTrackedActiveRun,
@ -2384,7 +2378,7 @@ describe("agent event handler", () => {
])(
"projects older lifecycle timestamps only for the owning run ($eventRunId)",
async ({ eventRunId, expectedStartedAt }) => {
vi.mocked(loadGatewaySessionLifecycleSnapshot).mockReturnValue({
loadGatewaySessionLifecycleSnapshotMock.mockReturnValue({
lifecycleRunId: "run-current",
row: {
key: "session-owned",

View file

@ -1,3 +0,0 @@
// Runtime barrel for loading Gateway session rows from chat paths without
// pulling the rest of session-utils into static startup imports.
export { loadGatewaySessionLifecycleSnapshot } from "./session-utils.js";

View file

@ -1,3 +0,0 @@
// Runtime barrel for persisting session lifecycle events from chat paths while
// keeping lifecycle-state behind a narrow lazy import boundary.
export { persistGatewaySessionLifecycleEvent } from "./session-lifecycle-state.js";

View file

@ -52,18 +52,20 @@ import type {
SessionMessageSubscriberRegistry,
ToolEventRecipientRegistry,
} from "./server-chat-state.js";
import { loadGatewaySessionLifecycleSnapshot } from "./server-chat.load-gateway-session-row.runtime.js";
import { persistGatewaySessionLifecycleEvent } from "./server-chat.persist-session-lifecycle.runtime.js";
import { hasSessionChangeReceivers } from "./session-change-receivers.js";
import { buildGatewaySessionEventRow } from "./session-event-payload.js";
import {
deriveGatewaySessionLifecycleProjectionPatch,
isRestartRecoveryLifecycleEvent,
isStaleLifecycleEventForSession,
persistGatewaySessionLifecycleEvent,
} from "./session-lifecycle-state.js";
import { tryResolveSessionCompatibilityOwnerAgentId } from "./session-request-agent.js";
import { resolveSessionSubscriptionKeys } from "./session-subscription-keys.js";
import { loadGatewaySessionEntryReadOnly } from "./session-utils.js";
import {
loadGatewaySessionEntryReadOnly,
loadGatewaySessionLifecycleSnapshot,
} from "./session-utils.js";
import { formatForLog } from "./ws-log.js";
export {
@ -338,6 +340,7 @@ export type AgentEventHandlerOptions = {
sessionEventSubscribers: SessionEventSubscriberRegistry;
sessionMessageSubscribers: SessionMessageSubscriberRegistry;
loadGatewaySessionLifecycleSnapshotForEvent?: typeof loadGatewaySessionLifecycleSnapshot;
persistGatewaySessionLifecycleEventForEvent?: typeof persistGatewaySessionLifecycleEvent;
lifecycleErrorRetryGraceMs?: number;
isChatSendRunActive?: (runId: string) => boolean;
clearTrackedActiveRun?: (params: {
@ -443,6 +446,7 @@ export function createAgentEventHandler({
sessionEventSubscribers,
sessionMessageSubscribers,
loadGatewaySessionLifecycleSnapshotForEvent = loadGatewaySessionLifecycleSnapshot,
persistGatewaySessionLifecycleEventForEvent = persistGatewaySessionLifecycleEvent,
lifecycleErrorRetryGraceMs = AGENT_LIFECYCLE_ERROR_RETRY_GRACE_MS,
isChatSendRunActive = () => false,
clearTrackedActiveRun,
@ -876,7 +880,7 @@ export function createAgentEventHandler({
if (sessionKey) {
clearTrackedActiveRun?.({ runId: evt.runId, clientRunId, sessionKey });
if (!suppressRestartRecoveryProjection && projectSessionLifecycle) {
const persistence = persistGatewaySessionLifecycleEvent({
const persistence = persistGatewaySessionLifecycleEventForEvent({
sessionKey,
agentId: sessionAgentId,
event: evt,
@ -1753,7 +1757,7 @@ export function createAgentEventHandler({
}
if (projectSessionLifecycle && sessionKey && lifecyclePhase === "start") {
void persistGatewaySessionLifecycleEvent({
void persistGatewaySessionLifecycleEventForEvent({
sessionKey,
agentId: sessionAgentId,
event: evt,

View file

@ -94,7 +94,8 @@ const CORE_GATEWAY_HANDLER_MODULES = {
),
diagnostics: () =>
import("./server-methods/diagnostics.js").then((module) => module.diagnosticsHandlers),
doctor: () => import("./server-methods/doctor.js").then((module) => module.doctorHandlers),
doctor: () =>
import("./server-methods/doctor.js").then((module) => module.createDoctorHandlers()),
environments: () =>
import("./server-methods/environments.js").then((module) => module.environmentsHandlers),
worktrees: () =>

View file

@ -1,15 +0,0 @@
/**
* Lazy boundary for doctor memory-core repair helpers.
*
* Doctor tests mock this file so the gateway method does not import bundled
* memory-core runtime code until a repair action actually needs it.
*/
export {
dedupeDreamDiaryEntries,
loadShortTermPromotionDreamingStats,
previewGroundedRemMarkdown,
removeBackfillDiaryEntries,
removeGroundedShortTermCandidates,
repairDreamingArtifacts,
writeBackfillDiaryEntries,
} from "../../plugin-sdk/memory-core-bundled-runtime.js";

View file

@ -58,7 +58,9 @@ vi.mock("../../plugins/memory-runtime.js", () => ({
getActiveMemorySearchManagerCore: getMemorySearchManager,
}));
vi.mock("./doctor.memory-core-runtime.js", () => ({
import { createDoctorHandlers } from "./doctor.js";
const doctorHandlers = createDoctorHandlers({
dedupeDreamDiaryEntries,
loadShortTermPromotionDreamingStats,
previewGroundedRemMarkdown,
@ -66,9 +68,7 @@ vi.mock("./doctor.memory-core-runtime.js", () => ({
removeBackfillDiaryEntries,
removeGroundedShortTermCandidates,
repairDreamingArtifacts,
}));
import { doctorHandlers } from "./doctor.js";
});
const makeRuntimeContext = () => ({ getRuntimeConfig: () => getRuntimeConfig() });

View file

@ -21,20 +21,23 @@ import {
resolveMemoryDreamingWorkspaces,
resolveMemoryRemDreamingConfig,
} from "../../memory-host-sdk/dreaming.js";
import * as defaultMemoryCoreRuntime from "../../plugin-sdk/memory-core-bundled-runtime.js";
import { getActiveMemorySearchManagerCore } from "../../plugins/memory-runtime.js";
import { normalizeAgentId } from "../../routing/session-key.js";
import { formatError } from "../server-utils.js";
import {
dedupeDreamDiaryEntries,
loadShortTermPromotionDreamingStats,
previewGroundedRemMarkdown,
removeBackfillDiaryEntries,
removeGroundedShortTermCandidates,
repairDreamingArtifacts,
writeBackfillDiaryEntries,
} from "./doctor.memory-core-runtime.js";
import type { GatewayRequestContext, GatewayRequestHandlers, RespondFn } from "./types.js";
type DoctorMemoryCoreRuntime = Pick<
typeof defaultMemoryCoreRuntime,
| "dedupeDreamDiaryEntries"
| "loadShortTermPromotionDreamingStats"
| "previewGroundedRemMarkdown"
| "removeBackfillDiaryEntries"
| "removeGroundedShortTermCandidates"
| "repairDreamingArtifacts"
| "writeBackfillDiaryEntries"
>;
const MANAGED_DEEP_SLEEP_CRON_NAME = "Memory Dreaming Promotion";
const MANAGED_DEEP_SLEEP_CRON_TAG = "[managed-by=memory-core.short-term-promotion]";
const DEEP_SLEEP_SYSTEM_EVENT_TEXT = "__openclaw_memory_core_short_term_promotion_dream__";
@ -381,6 +384,7 @@ function trimDreamingEntries(
async function loadDreamingStoreStats(
workspaceDir: string,
nowMs: number,
loadShortTermPromotionDreamingStats: DoctorMemoryCoreRuntime["loadShortTermPromotionDreamingStats"],
timezone?: string,
): Promise<DreamingStoreStats> {
try {
@ -699,7 +703,9 @@ const SKIPPED_MEMORY_EMBEDDING_PROBE = {
error: "memory embedding readiness not checked; run `openclaw memory status --deep` to probe",
} as const;
export const doctorHandlers: GatewayRequestHandlers = {
export const createDoctorHandlers = (
memoryCoreRuntime: DoctorMemoryCoreRuntime = defaultMemoryCoreRuntime,
): GatewayRequestHandlers => ({
"doctor.memory.status": async ({ respond, context, params }) => {
const resolved = resolveDoctorMemoryAgent(context, params, respond);
if (!resolved) {
@ -755,7 +761,12 @@ export const doctorHandlers: GatewayRequestHandlers = {
? mergeDreamingStoreStats(
await Promise.all(
allWorkspaces.map((entry) =>
loadDreamingStoreStats(entry, nowMs, dreamingConfig.timezone),
loadDreamingStoreStats(
entry,
nowMs,
memoryCoreRuntime.loadShortTermPromotionDreamingStats,
dreamingConfig.timezone,
),
),
),
)
@ -850,7 +861,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
respond(true, payload, undefined);
return;
}
const grounded = await previewGroundedRemMarkdown({
const grounded = await memoryCoreRuntime.previewGroundedRemMarkdown({
workspaceDir,
inputPaths: sourceFiles,
});
@ -871,7 +882,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
};
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
const written = await writeBackfillDiaryEntries({
const written = await memoryCoreRuntime.writeBackfillDiaryEntries({
workspaceDir,
entries,
timezone: remConfig.timezone,
@ -894,7 +905,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
return;
}
const { agentId, workspaceDir } = target;
const removed = await removeBackfillDiaryEntries({ workspaceDir });
const removed = await memoryCoreRuntime.removeBackfillDiaryEntries({ workspaceDir });
const dreamDiary = await readDreamDiary(workspaceDir);
const payload: DoctorMemoryDreamActionPayload = {
agentId,
@ -911,7 +922,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
return;
}
const { agentId, workspaceDir } = target;
const removed = await removeGroundedShortTermCandidates({ workspaceDir });
const removed = await memoryCoreRuntime.removeGroundedShortTermCandidates({ workspaceDir });
const payload: DoctorMemoryDreamActionPayload = {
agentId,
action: "resetGroundedShortTerm",
@ -925,7 +936,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
return;
}
const { agentId, workspaceDir } = target;
const repair = await repairDreamingArtifacts({ workspaceDir });
const repair = await memoryCoreRuntime.repairDreamingArtifacts({ workspaceDir });
const payload: DoctorMemoryDreamActionPayload = {
agentId,
action: "repairDreamingArtifacts",
@ -944,7 +955,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
return;
}
const { agentId, workspaceDir } = target;
const dedupe = await dedupeDreamDiaryEntries({ workspaceDir });
const dedupe = await memoryCoreRuntime.dedupeDreamDiaryEntries({ workspaceDir });
const dreamDiary = await readDreamDiary(workspaceDir);
const payload: DoctorMemoryDreamActionPayload = {
agentId,
@ -957,5 +968,5 @@ export const doctorHandlers: GatewayRequestHandlers = {
};
respond(true, payload, undefined);
},
};
});
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */

View file

@ -38,16 +38,17 @@ describe.skipIf(process.platform === "win32")("Gateway agent CLI shim", () => {
mode: 0o700,
});
const shim = await prepareGatewayAgentCliShim({
await prepareGatewayAgentCliShim({
env: testCase.profile ? { OPENCLAW_PROFILE: testCase.profile } : {},
invocation: { command: process.execPath, args: [entryPath], cwd: root },
stateDir,
});
const shimBinDir = path.join(stateDir, "tmp", "agent-cli");
const config = {
tools: { exec: { pathPrepend: [staleBinDir] } },
} satisfies OpenClawConfig;
const execConfig = resolveExecToolConfig({ cfg: config });
expect(execConfig.pathPrepend?.slice(0, 2)).toEqual([shim.binDir, staleBinDir]);
expect(execConfig.pathPrepend?.slice(0, 2)).toEqual([shimBinDir, staleBinDir]);
process.env.OPENCLAW_EXEC_SHELL_SNAPSHOT = "0";
process.env.PATH = `${staleBinDir}${path.delimiter}${process.env.PATH ?? ""}`;
@ -67,7 +68,7 @@ describe.skipIf(process.platform === "win32")("Gateway agent CLI shim", () => {
expect(JSON.parse(readExecText(result))).toEqual({
source: "gateway",
args: testCase.expectedArgs,
pathHead: shim.binDir,
pathHead: shimBinDir,
});
});
});
@ -75,7 +76,7 @@ describe.skipIf(process.platform === "win32")("Gateway agent CLI shim", () => {
it("renders a Windows PATH launcher for the running CLI", async () => {
await withTempDir("openclaw-agent-cli-shim-win-", async (root) => {
const result = await prepareGatewayAgentCliShim({
await prepareGatewayAgentCliShim({
env: { OPENCLAW_PROFILE: "work" },
invocation: {
command: "C:\\Program Files\\nodejs\\node.exe",
@ -86,8 +87,8 @@ it("renders a Windows PATH launcher for the running CLI", async () => {
stateDir: root,
});
expect(path.basename(result.executablePath)).toBe("openclaw.cmd");
expect(await fs.readFile(result.executablePath, "utf8")).toBe(
const executablePath = path.join(root, "tmp", "agent-cli", "openclaw.cmd");
expect(await fs.readFile(executablePath, "utf8")).toBe(
'@echo off\r\n"C:\\Program Files\\nodejs\\node.exe" C:\\OpenClaw\\dist\\index.js --profile work %*\r\n',
);
});

View file

@ -49,7 +49,7 @@ export async function prepareGatewayAgentCliShim(
platform?: NodeJS.Platform;
stateDir?: string;
} = {},
): Promise<{ binDir: string; executablePath: string }> {
): Promise<void> {
const env = options.env ?? process.env;
const platform = options.platform ?? process.platform;
const invocation = options.invocation ?? resolveCurrentOpenClawCliInvocation([]);
@ -70,7 +70,6 @@ export async function prepareGatewayAgentCliShim(
tempPrefix: "openclaw-agent-cli",
});
gatewayAgentCliState.binDir = binDir;
return { binDir, executablePath };
}
/** Clear a prepared launcher after startup failure; normal Gateway close resets it globally. */

View file

@ -9,41 +9,43 @@
export {
channelIngressRoutes,
createChannelIngressResolver,
defineStableChannelIngressIdentity,
readChannelIngressStoreAllowFromForDmPolicy,
resolveChannelMessageIngress,
resolveStableChannelMessageIngress,
} from "../channels/message-access/index.js";
} from "../channels/message-access/runtime.js";
export { defineStableChannelIngressIdentity } from "../channels/message-access/runtime-identity.js";
export { readChannelIngressStoreAllowFromForDmPolicy } from "../channels/message-access/store-allow-from.js";
export { resolveChannelImplicitMentions } from "../config/implicit-mentions.js";
export type {
AccessGroupMembershipFact,
ChannelIngressDecision,
ChannelIngressAccessGroupMembershipResolver,
ChannelIngressCommandPresetInput,
ChannelIngressConfigInput,
ChannelIngressContextBinding,
ChannelIngressEventInput,
ChannelIngressEventPresetInput,
ChannelIngressIdentityDescriptor,
ChannelIngressIdentityAlias,
ChannelIngressIdentityField,
ChannelIngressIdentitySubjectInput,
ChannelIngressIdentifierKind,
ChannelIngressPolicyInput,
ChannelIngressRouteAccess,
ChannelIngressRouteDescriptor,
ChannelIngressResolver,
ChannelIngressResolverMessageParams,
ChannelIngressStateInput,
ChannelIngressState,
ChannelMessageIngressCommandInput,
CreateChannelIngressResolverParams,
IngressReasonCode,
ResolvedChannelMessageIngress,
ResolveChannelMessageIngressParams,
ResolveStableChannelMessageIngressParams,
StableChannelIngressIdentityParams,
} from "../channels/message-access/index.js";
} from "../channels/message-access/runtime-types.js";
export type {
AccessGroupMembershipFact,
ChannelIngressDecision,
ChannelIngressEventInput,
ChannelIngressIdentifierKind,
ChannelIngressPolicyInput,
ChannelIngressState,
ChannelIngressStateInput,
IngressReasonCode,
} from "../channels/message-access/types.js";
export type { ResolvedChannelImplicitMentions } from "../config/implicit-mentions.js";
import {

View file

@ -2,7 +2,7 @@
* Tests channel message helper behavior and mocked runtime interactions.
*/
import { beforeAll, describe, expect, it, vi } from "vitest";
import { defineChannelMessageAdapter as defineCoreChannelMessageAdapter } from "../channels/message/index.js";
import { defineChannelMessageAdapter as defineCoreChannelMessageAdapter } from "../channels/message/adapter.js";
import {
defineChannelMessageAdapter,
type ChannelMessageDurableFinalAdapter,

View file

@ -21,25 +21,33 @@ export {
export type { OutboundMessageIdentity } from "../channels/message/outbound-echo.js";
export {
bindIngressLifecycleToReplyOptions,
CHANNEL_INGRESS_RETENTION_DEFAULTS,
createChannelIngressError,
createChannelIngressDrain,
createChannelIngressMonitor,
createReplyPrefixContext,
createReplyPrefixOptions,
createTypingCallbacks,
createChannelReplyPipeline as createChannelMessageReplyPipeline,
// Narrow drain seam by maintainer decision (#108924): factory, lifecycle binding,
// tuning constants, and processPidFromOwnerId (telegram transport display). All other
// claim/retry/adoption internals stay core-owned; test helpers live on the
// private-local plugin-state-test-runtime subpath.
DEFAULT_INGRESS_ADOPTION_STALL_MS,
DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
} from "../channels/message/ingress-drain.js";
export {
CHANNEL_INGRESS_RETENTION_DEFAULTS,
createChannelIngressError,
createChannelIngressMonitor,
} from "../channels/message/ingress-monitor.js";
export {
DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS,
DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
} from "../channels/message/ingress-retry-policy.js";
export {
INGRESS_CLAIM_PROCESS_ID,
processPidFromOwnerId,
} from "../channels/message/ingress-claim-owner.js";
export {
createChannelReplyPipeline as createChannelMessageReplyPipeline,
createReplyPrefixContext,
createReplyPrefixOptions,
createTypingCallbacks,
resolveChannelSourceReplyDeliveryMode as resolveChannelMessageSourceReplyDeliveryMode,
} from "../channels/message/index.js";
} from "../channels/message/reply-pipeline.js";
// Bare interval/stop orchestration for channels that own their typing renewal
// policy (e.g. per-message reply budgets) instead of the createTypingCallbacks lifecycle.
export { createTypingKeepaliveLoop } from "../channels/typing-lifecycle.js";
@ -151,24 +159,41 @@ export type {
ChannelProgressDraftCompositorLine,
ChannelProgressDraftCompositorSnapshot,
} from "../channels/progress-draft-compositor.js";
export { deriveDurableFinalDeliveryRequirements } from "../channels/message/capabilities.js";
export { defineChannelMessageAdapter } from "../channels/message/adapter.js";
export { createChannelMessageAdapterFromOutbound } from "../channels/message/outbound-bridge.js";
export { createDurableInboundReceiveJournalFromQueue } from "../channels/message/durable-receive.js";
export {
createChannelMessageAdapterFromOutbound,
createDurableInboundReceiveJournalFromQueue,
createMessageReceiptFromOutboundResults,
listMessageReceiptPlatformIds,
createMessageReceiveContext,
createPreviewMessageReceipt,
defineFinalizableLivePreviewAdapter,
deriveDurableFinalDeliveryRequirements,
deliverWithFinalizableLivePreviewAdapter,
defineChannelMessageAdapter,
resolveMessageReceiptPrimaryId,
verifyChannelMessageAdapterCapabilityProofs,
verifyChannelMessageLiveCapabilityAdapterProofs,
verifyChannelMessageLiveFinalizerProofs,
verifyChannelMessageReceiveAckPolicyAdapterProofs,
verifyDurableFinalCapabilityProofs,
} from "../channels/message/index.js";
} from "../channels/message/contracts.js";
export {
createPreviewMessageReceipt,
defineFinalizableLivePreviewAdapter,
deliverWithFinalizableLivePreviewAdapter,
} from "../channels/message/live.js";
export {
createMessageReceiptFromOutboundResults,
listMessageReceiptPlatformIds,
resolveMessageReceiptPrimaryId,
} from "../channels/message/receipt.js";
export { createMessageReceiveContext } from "../channels/message/receive.js";
export type { ChannelIngressDrain } from "../channels/message/ingress-drain.js";
export type {
ChannelIngressMonitorDeliveryResult,
ChannelIngressMonitorLifecycle,
} from "../channels/message/ingress-monitor.js";
export type {
ChannelIngressQueue,
ChannelIngressQueueClaim,
ChannelIngressQueueClaimRef,
ChannelIngressQueueCorruptClaim,
ChannelIngressQueueRecord,
} from "../channels/message/ingress-queue.js";
export type { MessageAckPolicy, MessageReceiveContext } from "../channels/message/receive.js";
export type {
ChannelMessageAdapterShape,
ChannelMessageDurableFinalAdapter,
@ -178,21 +203,11 @@ export type {
ChannelMessageSendTextContext,
ChannelMessageUnknownSendContext,
ChannelMessageUnknownSendReconciliationResult,
ChannelIngressDrain,
ChannelIngressMonitorDeliveryResult,
ChannelIngressMonitorLifecycle,
ChannelIngressQueue,
ChannelIngressQueueClaim,
ChannelIngressQueueClaimRef,
ChannelIngressQueueCorruptClaim,
ChannelIngressQueueRecord,
MessageAckPolicy,
MessageReceiveContext,
MessageReceipt,
MessageReceiptPart,
MessageReceiptPartKind,
MessageReceiptSourceResult,
} from "../channels/message/index.js";
} from "../channels/message/types.js";
/** Lazily forwards inbound reply delivery through the channel turn durable-delivery module. */
export const deliverInboundReplyWithMessageSendContext: ChannelDurableDeliveryModule["deliverInboundReplyWithMessageSendContextCore"] =

View file

@ -192,7 +192,13 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
[contractPluginPath({ rootDir: ROOT_DIR, pluginId: "slack", relativePath: "runtime-api.ts" })]: [
'export { handleSlackAction, slackActionRuntime, type SlackActionContext } from "./src/action-runtime.js";',
'export { listSlackDirectoryGroupsLive, listSlackDirectoryPeersLive } from "./src/directory-live.js";',
'export { deleteSlackMessage, editSlackMessage, getSlackMemberInfo, listEnabledSlackAccounts, listSlackAccountIds, listSlackEmojis, listSlackPins, listSlackReactions, monitorSlackProvider, pinSlackMessage, probeSlack, reactSlackMessage, readSlackMessages, removeOwnSlackReactions, removeSlackReaction, resolveDefaultSlackAccountId, resolveSlackAccount, resolveSlackAppToken, resolveSlackBotToken, resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy, sendMessageSlack, sendSlackMessage, unpinSlackMessage } from "./src/index.js";',
'export { listEnabledSlackAccounts, listSlackAccountIds, resolveDefaultSlackAccountId, resolveSlackAccount } from "./src/accounts.js";',
'export { deleteSlackMessage, editSlackMessage, getSlackMemberInfo, listSlackEmojis, listSlackPins, listSlackReactions, pinSlackMessage, reactSlackMessage, readSlackMessages, removeOwnSlackReactions, removeSlackReaction, sendSlackMessage, unpinSlackMessage } from "./src/actions.js";',
'export { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./src/group-policy.js";',
'export { monitorSlackProvider } from "./src/monitor.js";',
'export { probeSlack } from "./src/probe.js";',
'export { sendMessageSlack } from "./src/send.js";',
'export { resolveSlackAppToken, resolveSlackBotToken } from "./src/token.js";',
'export { resolveSlackChannelAllowlist, type SlackChannelLookup, type SlackChannelResolution } from "./src/resolve-channels.js";',
'export { resolveSlackUserAllowlist, type SlackUserLookup, type SlackUserResolution } from "./src/resolve-users.js";',
'export { registerSlackPluginHttpRoutes } from "./src/http/plugin-routes.js";',