Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions extensions/imessage/src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
setAccountEnabledInConfigSection,
type ChannelPlugin,
type ResolvedIMessageAccount,
} from "openclaw/plugin-sdk";
} from "openclaw/plugin-sdk/imessage";
import { getIMessageRuntime } from "./runtime.js";

const meta = getChatChannelMeta("imessage");
Expand Down Expand Up @@ -185,14 +185,13 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
accountId,
name: input.name,
});
const next = (
const next =
accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount({
cfg: namedConfig,
channelKey: "imessage",
})
: namedConfig
) as typeof cfg;
: namedConfig;
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
Expand Down
3 changes: 1 addition & 2 deletions extensions/imessage/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
import type { PluginRuntime } from "openclaw/plugin-sdk/imessage";
import { createPluginRuntimeStore, type PluginRuntime } from "openclaw/plugin-sdk/imessage";

const { setRuntime: setIMessageRuntime, getRuntime: getIMessageRuntime } =
createPluginRuntimeStore<PluginRuntime>("iMessage runtime not initialized");
Expand Down
84 changes: 55 additions & 29 deletions extensions/zulip/src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import {
type ResolvedZulipAccount,
} from "./zulip/accounts.js";
import { normalizeZulipBaseUrl } from "./zulip/client.js";
import { readZulipComponentSpec } from "./zulip/components.js";
import { listZulipDirectoryGroups, listZulipDirectoryPeers } from "./zulip/directory.js";
import { monitorZulipProvider } from "./zulip/monitor.js";
import { probeZulip } from "./zulip/probe.js";
import { sendZulipComponentMessage } from "./zulip/send-components.js";
import { sendMessageZulip } from "./zulip/send.js";

const loadZulipComponentsModule = () => import("./zulip/components.js");
const loadZulipDirectoryModule = () => import("./zulip/directory.js");
const loadZulipMonitorModule = () => import("./zulip/monitor.js");
const loadZulipProbeModule = () => import("./zulip/probe.js");
const loadZulipSendComponentsModule = () => import("./zulip/send-components.js");
const loadZulipSendModule = () => import("./zulip/send.js");

const meta = {
id: "zulip",
Expand Down Expand Up @@ -114,28 +115,37 @@ const zulipMessageActions: ChannelMessageActionAdapter = {

const result =
rawButtons && rawButtons.length > 0
? await sendZulipComponentMessage(
to,
message,
readZulipComponentSpec({
heading: typeof params.heading === "string" ? params.heading : undefined,
buttons: rawButtons,
}),
{
? await (async () => {
const [{ readZulipComponentSpec }, { sendZulipComponentMessage }] = await Promise.all([
loadZulipComponentsModule(),
loadZulipSendComponentsModule(),
]);
return await sendZulipComponentMessage(
to,
message,
readZulipComponentSpec({
heading: typeof params.heading === "string" ? params.heading : undefined,
buttons: rawButtons,
}),
{
cfg,
accountId: resolvedAccountId,
replyToTopic,
mediaUrl,
sessionKey,
agentId,
},
);
})()
: await (async () => {
const { sendMessageZulip } = await loadZulipSendModule();
return await sendMessageZulip(to, message, {
cfg,
accountId: resolvedAccountId,
replyToTopic,
mediaUrl,
sessionKey,
agentId,
},
)
: await sendMessageZulip(to, message, {
cfg,
accountId: resolvedAccountId,
replyToTopic,
mediaUrl,
});
});
})();

return {
content: [
Expand Down Expand Up @@ -258,10 +268,22 @@ export const zulipPlugin: ChannelPlugin<ResolvedZulipAccount> = {
actions: zulipMessageActions,
directory: {
self: async () => null,
listPeers: async (params) => listZulipDirectoryPeers(params),
listPeersLive: async (params) => listZulipDirectoryPeers(params),
listGroups: async (params) => listZulipDirectoryGroups(params),
listGroupsLive: async (params) => listZulipDirectoryGroups(params),
listPeers: async (params) => {
const { listZulipDirectoryPeers } = await loadZulipDirectoryModule();
return await listZulipDirectoryPeers(params);
},
listPeersLive: async (params) => {
const { listZulipDirectoryPeers } = await loadZulipDirectoryModule();
return await listZulipDirectoryPeers(params);
},
listGroups: async (params) => {
const { listZulipDirectoryGroups } = await loadZulipDirectoryModule();
return await listZulipDirectoryGroups(params);
},
listGroupsLive: async (params) => {
const { listZulipDirectoryGroups } = await loadZulipDirectoryModule();
return await listZulipDirectoryGroups(params);
},
},
messaging: {
normalizeTarget: (raw) => raw.trim(),
Expand Down Expand Up @@ -298,13 +320,15 @@ export const zulipPlugin: ChannelPlugin<ResolvedZulipAccount> = {
return { ok: true, to: trimmed };
},
sendText: async ({ to, text, accountId, replyToId }) => {
const { sendMessageZulip } = await loadZulipSendModule();
const result = await sendMessageZulip(to, text, {
accountId: accountId ?? undefined,
replyToTopic: replyToId ?? undefined,
});
return { channel: "zulip", ...result };
},
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }) => {
const { sendMessageZulip } = await loadZulipSendModule();
const result = await sendMessageZulip(to, text, {
accountId: accountId ?? undefined,
mediaUrl,
Expand Down Expand Up @@ -342,6 +366,7 @@ export const zulipPlugin: ChannelPlugin<ResolvedZulipAccount> = {
if (!email || !key || !baseUrl) {
return { ok: false, error: "bot credentials or baseUrl missing" };
}
const { probeZulip } = await loadZulipProbeModule();
return await probeZulip(baseUrl, email, key, timeoutMs);
},
buildAccountSnapshot: ({ account, runtime, probe }) => ({
Expand Down Expand Up @@ -441,7 +466,8 @@ export const zulipPlugin: ChannelPlugin<ResolvedZulipAccount> = {
baseUrl: account.baseUrl,
});
ctx.log?.info(`[${account.accountId}] starting zulip channel`);
return monitorZulipProvider({
const { monitorZulipProvider } = await loadZulipMonitorModule();
return await monitorZulipProvider({
botEmail: account.botEmail ?? undefined,
botApiKey: account.botApiKey ?? undefined,
baseUrl: account.baseUrl ?? undefined,
Expand Down
14 changes: 14 additions & 0 deletions src/agents/system-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ describe("buildAgentSystemPrompt", () => {
expect(tokenA).not.toBe(tokenB);
});

it("adds typo-correction and empty-memory guardrails to the memory section", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
toolNames: ["message", "memory_search", "memory_get"],
});

expect(prompt).toContain("## Memory Recall");
expect(prompt).toContain("Short typo fixes or clarifications from the user");
expect(prompt).toContain("Do not store them as memories or preferences.");
expect(prompt).toContain(
"If memory_search returns nothing useful for a coding/status question, say no relevant prior context was found and answer the actual question.",
);
});

it("omits extended sections in minimal prompt mode", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
Expand Down
2 changes: 2 additions & 0 deletions src/agents/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ function buildMemorySection(params: {
const lines = [
"## Memory Recall",
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
"Short typo fixes or clarifications from the user (for example: 'I meant X', 'typo: X', or 'foo = bar') are corrections to the active request, not durable memory. Do not store them as memories or preferences.",
"If memory_search returns nothing useful for a coding/status question, say no relevant prior context was found and answer the actual question. Do not switch to generic memory/database-status summaries.",
];
if (params.citationsMode === "off") {
lines.push(
Expand Down
52 changes: 52 additions & 0 deletions src/gateway/server-startup-timeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const ensureControlUiAssetsBuilt = vi.hoisted(() =>
vi.fn(async () => ({ ok: true, built: false })),
);

vi.mock("../infra/control-ui-assets.js", () => ({
ensureControlUiAssetsBuilt,
}));

import {
DEFAULT_CONTROL_UI_BUILD_TIMEOUT_MS,
ensureControlUiAssetsWithStartupTimeout,
resolveControlUiBuildTimeoutMs,
} from "./server-startup-timeout.js";

describe("server-startup-timeout", () => {
beforeEach(() => {
ensureControlUiAssetsBuilt.mockClear();
});

it("uses the default control UI build timeout", async () => {
await ensureControlUiAssetsWithStartupTimeout(
{ log: () => {} } as never,
{} as NodeJS.ProcessEnv,
);
expect(ensureControlUiAssetsBuilt).toHaveBeenCalledWith(expect.anything(), {
timeoutMs: DEFAULT_CONTROL_UI_BUILD_TIMEOUT_MS,
});
});

it("allows disabling the timeout with OPENCLAW_CONTROL_UI_BUILD_TIMEOUT_MS=0", async () => {
await ensureControlUiAssetsWithStartupTimeout(
{ log: () => {} } as never,
{ OPENCLAW_CONTROL_UI_BUILD_TIMEOUT_MS: "0" } as NodeJS.ProcessEnv,
);
expect(ensureControlUiAssetsBuilt).toHaveBeenCalledWith(expect.anything(), undefined);
});

it("parses timeout overrides and falls back on invalid values", () => {
expect(
resolveControlUiBuildTimeoutMs({
OPENCLAW_CONTROL_UI_BUILD_TIMEOUT_MS: "1200",
} as NodeJS.ProcessEnv),
).toBe(1200);
expect(
resolveControlUiBuildTimeoutMs({
OPENCLAW_CONTROL_UI_BUILD_TIMEOUT_MS: "abc",
} as NodeJS.ProcessEnv),
).toBe(DEFAULT_CONTROL_UI_BUILD_TIMEOUT_MS);
});
});
35 changes: 35 additions & 0 deletions src/gateway/server-startup-timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
ensureControlUiAssetsBuilt,
type EnsureControlUiAssetsResult,
} from "../infra/control-ui-assets.js";
import type { RuntimeEnv } from "../runtime.js";

export const DEFAULT_CONTROL_UI_BUILD_TIMEOUT_MS = 30_000;

export function resolveControlUiBuildTimeoutMs(
env: NodeJS.ProcessEnv = process.env,
): number | undefined {
const raw = env.OPENCLAW_CONTROL_UI_BUILD_TIMEOUT_MS?.trim();
if (!raw) {
return DEFAULT_CONTROL_UI_BUILD_TIMEOUT_MS;
}
if (raw === "0") {
return undefined;
}
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
return DEFAULT_CONTROL_UI_BUILD_TIMEOUT_MS;
}
return parsed === 0 ? undefined : parsed;
}

export async function ensureControlUiAssetsWithStartupTimeout(
runtime: RuntimeEnv,
env: NodeJS.ProcessEnv = process.env,
): Promise<EnsureControlUiAssetsResult> {
const timeoutMs = resolveControlUiBuildTimeoutMs(env);
return await ensureControlUiAssetsBuilt(
runtime,
timeoutMs === undefined ? undefined : { timeoutMs },
);
}
18 changes: 16 additions & 2 deletions src/gateway/server.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { resolveMainSessionKey } from "../config/sessions.js";
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
import {
ensureControlUiAssetsBuilt,
isPackageProvenControlUiRootSync,
resolveControlUiRootOverrideSync,
resolveControlUiRootSync,
Expand Down Expand Up @@ -96,6 +95,7 @@ import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";
import { createGatewayRuntimeState } from "./server-runtime-state.js";
import { resolveSessionKeyForRun } from "./server-session-key.js";
import { logGatewayStartup } from "./server-startup-log.js";
import { ensureControlUiAssetsWithStartupTimeout } from "./server-startup-timeout.js";
import { startGatewaySidecars } from "./server-startup.js";
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
import { createWizardSessionTracker } from "./server-wizard-sessions.js";
Expand Down Expand Up @@ -476,6 +476,9 @@ export async function startGatewayServer(
coreGatewayHandlers,
baseMethods,
});
log.info(
`gateway: plugins loaded (${pluginRegistry.plugins.length} plugins, ${pluginRegistry.channels.length} channels)`,
);
const channelLogs = Object.fromEntries(
listChannelPlugins().map((plugin) => [plugin.id, logChannels.child(plugin.id)]),
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
Expand All @@ -485,6 +488,7 @@ export async function startGatewayServer(
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
const gatewayMethods = Array.from(new Set([...baseGatewayMethods, ...channelMethods]));
let pluginServices: PluginServicesHandle | null = null;
log.info("gateway: resolving runtime config…");
const runtimeConfig = await resolveGatewayRuntimeConfig({
cfg: cfgAtStart,
port,
Expand Down Expand Up @@ -512,6 +516,9 @@ export async function startGatewayServer(
} = runtimeConfig;
let hooksConfig = runtimeConfig.hooksConfig;
const canvasHostEnabled = runtimeConfig.canvasHostEnabled;
log.info(
`gateway: runtime config resolved (bind=${bindHost}, controlUi=${controlUiEnabled ? "on" : "off"})`,
);

// Create auth rate limiters used by connect/auth flows.
const rateLimitConfig = cfgAtStart.gateway?.auth?.rateLimit;
Expand All @@ -529,13 +536,17 @@ export async function startGatewayServer(
log.warn(`gateway: controlUi.root not found at ${resolvedOverridePath}`);
}
} else if (controlUiEnabled) {
log.info("gateway: ensuring control UI assets…");
let resolvedRoot = resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
if (!resolvedRoot) {
const ensureResult = await ensureControlUiAssetsBuilt(gatewayRuntime);
const ensureResult = await ensureControlUiAssetsWithStartupTimeout(
gatewayRuntime,
process.env,
);
if (!ensureResult.ok && ensureResult.message) {
log.warn(`gateway: ${ensureResult.message}`);
}
Expand All @@ -557,13 +568,15 @@ export async function startGatewayServer(
path: resolvedRoot,
}
: { kind: "missing" };
log.info(`gateway: control UI state (${controlUiRootState.kind})`);
}

const wizardRunner = opts.wizardRunner ?? runOnboardingWizard;
const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();

const deps = createDefaultDeps();
let canvasHostServer: CanvasHostServer | null = null;
log.info("gateway: loading TLS runtime…");
const gatewayTls = await loadGatewayTlsRuntime(cfgAtStart.gateway?.tls, log.child("tls"));
if (cfgAtStart.gateway?.tls?.enabled && !gatewayTls.enabled) {
throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
Expand All @@ -579,6 +592,7 @@ export async function startGatewayServer(
channelManager,
startedAt: serverStartedAt,
});
log.info(`gateway: creating runtime state (port=${port})…`);
const {
canvasHost,
httpServer,
Expand Down
Loading
Loading