Skip to content

Commit 479fe14

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 654cec2 + ac00ba1 commit 479fe14

69 files changed

Lines changed: 5206 additions & 328 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Docs: https://docs.openclaw.ai
2626
- Browser/CDP: let local attach-only `manual-cdp` profiles reuse the local loopback CDP control plane under strict default policy and remote-class probe timeouts, so tabs/snapshot stop falsely reporting a live local browser session as not running. (#65611, #66080) Thanks @mbelinky.
2727
- Cron/scheduler: stop inventing short retries when cron next-run calculation returns no valid future slot, and keep a maintenance wake armed so enabled unscheduled jobs recover without entering a refire loop. (#66019, #66083) Thanks @mbelinky.
2828
- Cron/scheduler: preserve the active error-backoff floor when maintenance repair recomputes a missing cron next-run, so recurring errored jobs do not resume early after a transient next-run resolution failure. (#66019, #66083, #66113) Thanks @mbelinky.
29+
- Outbound/delivery-queue: persist the originating outbound `session` context on queued delivery entries and replay it during recovery, so write-ahead-queued sends keep their original outbound media policy context after restart instead of evaluating against a missing session. (#66025) Thanks @eleqtrizit.
30+
- Auto-reply/queue: split collect-mode followup drains into contiguous groups by per-message authorization context (sender id, owner status, exec/bash-elevated overrides), so queued items from different senders or exec configs no longer execute under the last queued run's owner-only and exec-approval context. (#66024) Thanks @eleqtrizit.
31+
- Dreaming/memory-core: require a live queued Dreaming cron event before the heartbeat hook runs the sweep, so managed Dreaming no longer replays on later heartbeats after the scheduled run was already consumed. (#66139) Thanks @mbelinky.
2932

3033
## 2026.4.12
3134

docs/reference/secretref-user-supplied-credentials-matrix.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
"hooks.gmail.pushToken",
1010
"hooks.mappings[].sessionKey",
1111
"auth-profiles.oauth.*",
12-
"channels.discord.threadBindings.webhookToken",
1312
"channels.discord.accounts.*.threadBindings.webhookToken",
14-
"channels.whatsapp.creds.json",
15-
"channels.whatsapp.accounts.*.creds.json"
13+
"channels.discord.threadBindings.webhookToken",
14+
"channels.whatsapp.accounts.*.creds.json",
15+
"channels.whatsapp.creds.json"
1616
],
1717
"entries": [
1818
{

extensions/memory-core/src/dreaming.test.ts

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3+
import { enqueueSystemEvent, resetSystemEventsForTest } from "openclaw/plugin-sdk/infra-runtime";
34
import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core";
4-
import { describe, expect, it, vi } from "vitest";
5+
import { afterEach, describe, expect, it, vi } from "vitest";
56
import {
67
clearInternalHooks,
78
createInternalHookEvent,
@@ -21,6 +22,10 @@ import { createMemoryCoreTestHarness } from "./test-helpers.js";
2122
const constants = __testing.constants;
2223
const { createTempWorkspace } = createMemoryCoreTestHarness();
2324

25+
afterEach(() => {
26+
resetSystemEventsForTest();
27+
});
28+
2429
type CronParam = NonNullable<Parameters<typeof reconcileShortTermDreamingCronJob>[0]["cron"]>;
2530
type CronJobLike = Awaited<ReturnType<CronParam["list"]>>[number];
2631
type CronAddInput = Parameters<CronParam["add"]>[0];
@@ -138,15 +143,15 @@ function getBeforeAgentReplyHandler(
138143
onMock: ReturnType<typeof vi.fn>,
139144
): (
140145
event: { cleanedBody: string },
141-
ctx: { trigger?: string; workspaceDir?: string },
146+
ctx: { trigger?: string; workspaceDir?: string; sessionKey?: string },
142147
) => Promise<unknown> {
143148
const call = onMock.mock.calls.find(([eventName]) => eventName === "before_agent_reply");
144149
if (!call) {
145150
throw new Error("before_agent_reply hook was not registered");
146151
}
147152
return call[1] as (
148153
event: { cleanedBody: string },
149-
ctx: { trigger?: string; workspaceDir?: string },
154+
ctx: { trigger?: string; workspaceDir?: string; sessionKey?: string },
150155
) => Promise<unknown>;
151156
}
152157

@@ -1111,6 +1116,130 @@ describe("gateway startup reconciliation", () => {
11111116
clearInternalHooks();
11121117
}
11131118
});
1119+
1120+
it("only triggers managed dreaming when the queued cron event is still pending", async () => {
1121+
clearInternalHooks();
1122+
const logger = createLogger();
1123+
const harness = createCronHarness();
1124+
const onMock = vi.fn();
1125+
const api: DreamingPluginApiTestDouble = {
1126+
config: {
1127+
plugins: {
1128+
entries: {
1129+
"memory-core": {
1130+
config: {
1131+
dreaming: {
1132+
enabled: false,
1133+
},
1134+
},
1135+
},
1136+
},
1137+
},
1138+
} as OpenClawConfig,
1139+
pluginConfig: {},
1140+
logger,
1141+
runtime: {},
1142+
registerHook: (event: string, handler: Parameters<typeof registerInternalHook>[1]) => {
1143+
registerInternalHook(event, handler);
1144+
},
1145+
on: onMock,
1146+
};
1147+
1148+
try {
1149+
registerShortTermPromotionDreamingForTest(api);
1150+
await triggerInternalHook(
1151+
createInternalHookEvent("gateway", "startup", "gateway:startup", {
1152+
cfg: api.config,
1153+
deps: { cron: harness.cron },
1154+
}),
1155+
);
1156+
1157+
const sessionKey = "agent:main:main";
1158+
enqueueSystemEvent(constants.DREAMING_SYSTEM_EVENT_TEXT, {
1159+
sessionKey,
1160+
contextKey: "cron:memory-dreaming",
1161+
});
1162+
1163+
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
1164+
const first = await beforeAgentReply(
1165+
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
1166+
{ trigger: "heartbeat", workspaceDir: ".", sessionKey },
1167+
);
1168+
1169+
expect(first).toEqual({
1170+
handled: true,
1171+
reason: "memory-core: short-term dreaming disabled",
1172+
});
1173+
1174+
resetSystemEventsForTest();
1175+
1176+
const second = await beforeAgentReply(
1177+
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
1178+
{ trigger: "heartbeat", workspaceDir: ".", sessionKey },
1179+
);
1180+
1181+
expect(second).toBeUndefined();
1182+
} finally {
1183+
clearInternalHooks();
1184+
}
1185+
});
1186+
1187+
it("resolves queued managed dreaming cron events from the base session for isolated heartbeats", async () => {
1188+
clearInternalHooks();
1189+
const logger = createLogger();
1190+
const harness = createCronHarness();
1191+
const onMock = vi.fn();
1192+
const api: DreamingPluginApiTestDouble = {
1193+
config: {
1194+
plugins: {
1195+
entries: {
1196+
"memory-core": {
1197+
config: {
1198+
dreaming: {
1199+
enabled: false,
1200+
},
1201+
},
1202+
},
1203+
},
1204+
},
1205+
} as OpenClawConfig,
1206+
pluginConfig: {},
1207+
logger,
1208+
runtime: {},
1209+
registerHook: (event: string, handler: Parameters<typeof registerInternalHook>[1]) => {
1210+
registerInternalHook(event, handler);
1211+
},
1212+
on: onMock,
1213+
};
1214+
1215+
try {
1216+
registerShortTermPromotionDreamingForTest(api);
1217+
await triggerInternalHook(
1218+
createInternalHookEvent("gateway", "startup", "gateway:startup", {
1219+
cfg: api.config,
1220+
deps: { cron: harness.cron },
1221+
}),
1222+
);
1223+
1224+
enqueueSystemEvent(constants.DREAMING_SYSTEM_EVENT_TEXT, {
1225+
sessionKey: "agent:main:main",
1226+
contextKey: "cron:memory-dreaming",
1227+
});
1228+
1229+
const beforeAgentReply = getBeforeAgentReplyHandler(onMock);
1230+
const result = await beforeAgentReply(
1231+
{ cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT },
1232+
{ trigger: "heartbeat", workspaceDir: ".", sessionKey: "agent:main:main:heartbeat" },
1233+
);
1234+
1235+
expect(result).toEqual({
1236+
handled: true,
1237+
reason: "memory-core: short-term dreaming disabled",
1238+
});
1239+
} finally {
1240+
clearInternalHooks();
1241+
}
1242+
});
11141243
});
11151244

11161245
describe("short-term dreaming trigger", () => {

extensions/memory-core/src/dreaming.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { peekSystemEventEntries } from "openclaw/plugin-sdk/infra-runtime";
12
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
23
import {
34
DEFAULT_MEMORY_DREAMING_FREQUENCY as DEFAULT_MEMORY_DREAMING_CRON_EXPR,
@@ -36,6 +37,7 @@ const LEGACY_REM_SLEEP_CRON_NAME = "Memory REM Dreaming";
3637
const LEGACY_REM_SLEEP_CRON_TAG = "[managed-by=memory-core.dreaming.rem]";
3738
const LEGACY_REM_SLEEP_EVENT_TEXT = "__openclaw_memory_core_rem_sleep__";
3839
const RUNTIME_CRON_RECONCILE_INTERVAL_MS = 60_000;
40+
const HEARTBEAT_ISOLATED_SESSION_SUFFIX = ":heartbeat";
3941

4042
type Logger = Pick<OpenClawPluginApi["logger"], "info" | "warn" | "error">;
4143

@@ -344,6 +346,35 @@ function resolveStartupConfigFromEvent(event: unknown, fallback: OpenClawConfig)
344346
return startupCfg as OpenClawConfig;
345347
}
346348

349+
function resolveDreamingTriggerSessionKeys(sessionKey?: string): string[] {
350+
const normalized = normalizeTrimmedString(sessionKey);
351+
if (!normalized) {
352+
return [];
353+
}
354+
355+
const keys = [normalized];
356+
// Isolated heartbeat runs execute in a sibling `:heartbeat` session while cron
357+
// system events stay queued on the base main session.
358+
if (normalized.endsWith(HEARTBEAT_ISOLATED_SESSION_SUFFIX)) {
359+
const baseSessionKey = normalized.slice(0, -HEARTBEAT_ISOLATED_SESSION_SUFFIX.length).trim();
360+
if (baseSessionKey) {
361+
keys.push(baseSessionKey);
362+
}
363+
}
364+
365+
return Array.from(new Set(keys));
366+
}
367+
368+
function hasPendingManagedDreamingCronEvent(sessionKey?: string): boolean {
369+
return resolveDreamingTriggerSessionKeys(sessionKey).some((candidateSessionKey) =>
370+
peekSystemEventEntries(candidateSessionKey).some(
371+
(event) =>
372+
event.contextKey?.startsWith("cron:") === true &&
373+
normalizeTrimmedString(event.text) === DREAMING_SYSTEM_EVENT_TEXT,
374+
),
375+
);
376+
}
377+
347378
export function resolveShortTermPromotionDreamingConfig(params: {
348379
pluginConfig?: Record<string, unknown>;
349380
cfg?: OpenClawConfig;
@@ -716,6 +747,12 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
716747
const config = await reconcileManagedDreamingCron({
717748
reason: "runtime",
718749
});
750+
if (
751+
!hasPendingManagedDreamingCronEvent(ctx.sessionKey) ||
752+
!includesSystemEventToken(event.cleanedBody, DREAMING_SYSTEM_EVENT_TEXT)
753+
) {
754+
return undefined;
755+
}
719756
return await runShortTermDreamingPromotionIfTriggered({
720757
cleanedBody: event.cleanedBody,
721758
trigger: ctx.trigger,

scripts/generate-bundled-channel-config-metadata.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22
import fs from "node:fs";
33
import path from "node:path";
4+
import { loadBundledPluginPublicArtifactModuleSync } from "../src/plugins/public-surface-loader.js";
45
import { loadChannelConfigSurfaceModule } from "./load-channel-config-surface.ts";
56

67
const GENERATED_BY = "scripts/generate-bundled-channel-config-metadata.ts";
@@ -63,6 +64,11 @@ type BundledChannelConfigMetadata = {
6364
description?: string;
6465
schema: Record<string, unknown>;
6566
uiHints?: Record<string, unknown>;
67+
unsupportedSecretRefSurfacePatterns?: readonly string[];
68+
};
69+
70+
type BundledChannelSecuritySurface = {
71+
unsupportedSecretRefSurfacePatterns?: readonly string[];
6672
};
6773

6874
function resolveChannelConfigSchemaModulePath(rootDir: string): string | null {
@@ -131,6 +137,34 @@ function formatTypeScriptModule(source: string, outputPath: string, repoRoot: st
131137
});
132138
}
133139

140+
function resolveChannelUnsupportedSecretRefSurfacePatterns(
141+
source: BundledPluginSource,
142+
channelId: string,
143+
): string[] {
144+
try {
145+
const surface = loadBundledPluginPublicArtifactModuleSync<BundledChannelSecuritySurface>({
146+
dirName: source.dirName,
147+
artifactBasename: "security-contract-api.js",
148+
});
149+
const prefix = `channels.${channelId}.`;
150+
return [
151+
...new Set(
152+
(surface.unsupportedSecretRefSurfacePatterns ?? []).filter(
153+
(pattern): pattern is string => typeof pattern === "string" && pattern.startsWith(prefix),
154+
),
155+
),
156+
].toSorted((left, right) => left.localeCompare(right));
157+
} catch (error) {
158+
if (
159+
error instanceof Error &&
160+
error.message.startsWith("Unable to resolve bundled plugin public surface ")
161+
) {
162+
return [];
163+
}
164+
throw error;
165+
}
166+
}
167+
134168
export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: string }) {
135169
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
136170
const sources = collectBundledPluginSources({ repoRoot, requirePackageJson: true });
@@ -156,13 +190,20 @@ export async function collectBundledChannelConfigMetadata(params?: { repoRoot?:
156190
for (const channelId of channelIds) {
157191
const label = resolveRootLabel(source, channelId);
158192
const description = resolveRootDescription(source, channelId);
193+
const unsupportedSecretRefSurfacePatterns = resolveChannelUnsupportedSecretRefSurfacePatterns(
194+
source,
195+
channelId,
196+
);
159197
entries.push({
160198
pluginId: source.manifest.id,
161199
channelId,
162200
...(label ? { label } : {}),
163201
...(description ? { description } : {}),
164202
schema: surface.schema,
165203
...(Object.keys(surface.uiHints ?? {}).length > 0 ? { uiHints: surface.uiHints } : {}),
204+
...(unsupportedSecretRefSurfacePatterns.length > 0
205+
? { unsupportedSecretRefSurfacePatterns }
206+
: {}),
166207
});
167208
}
168209
}

0 commit comments

Comments
 (0)