fix(memory-core): add runtime cron service fallback for dreaming reconciliation (#71694)

* fix(memory-core): add runtime cron service fallback for dreaming reconciliation

When the cron service is unavailable during gateway_start (e.g., due to
a startup timing race or deferred initialization), the startupCronSource
is captured as null and never refreshed. All subsequent runtime
reconciliation attempts fail with 'cron service unavailable', even when
the cron service is fully operational.

This adds a fallback path in the runtime reconciliation that attempts to
obtain the cron service from the plugin API runtime when the startup
capture was null. This handles the case where the cron service becomes
available after the initial startup event.

Fixes #67362

* fix(memory-core): hold gateway context for runtime cron resolution

The previous attempt tried to access api.runtime.cron which doesn't exist
on the PluginRuntime type. The cron service is only accessible through
PluginHookGatewayContext.getCron().

This fix stores the gateway context from the gateway_start event and uses
it to retry cron resolution at runtime when the initial capture was null.
This handles the race condition where the cron service isn't available
during gateway_start (250ms deferred init) but is ready later.

Also refreshes the startupCron capture when the runtime retry succeeds,
so subsequent reconciliation calls resolve immediately.

Addresses review feedback on #71694
This commit is contained in:
Mara 🌿
2026-04-25 23:07:45 +02:00
committed by GitHub
parent a97fe41a9e
commit 4038f734f7

View File

@@ -663,6 +663,12 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void {
let resolveStartupCron: (() => CronServiceLike | null) | null = null;
// Hold a live reference to the gateway context so we can retry cron resolution at runtime.
// The startup capture may fail if the cron service isn't available yet (race condition in
// startGatewaySidecars — the startup event fires via setTimeout(250ms) before deps.cron is
// attached). By keeping the context, we can call getCron() again on later reconciliation
// attempts when the service is guaranteed to be ready. Fixes #67362.
let gatewayContext: { getCron?: () => CronServiceLike | null } | null = null;
let unavailableCronWarningEmitted = false;
let lastRuntimeReconcileAtMs = 0;
let lastRuntimeConfigKey: string | null = null;
@@ -707,7 +713,22 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
if (params.reason === "startup") {
resolveStartupCron = params.startupCron ?? null;
}
const cron = resolveStartupCron?.() ?? null;
let cron = resolveStartupCron?.() ?? null;
// Runtime fallback: retry resolving the cron service from the gateway context.
// This handles the case where the cron service was not yet available during
// gateway_start (250ms deferred init race in startGatewaySidecars) but is
// available now. Fixes #67362.
if (!cron && params.reason === "runtime" && gatewayContext) {
try {
cron = resolveCronServiceFromGatewayContext(gatewayContext);
if (cron) {
// Refresh the startup capture so subsequent calls resolve immediately.
resolveStartupCron = () => cron;
}
} catch {
// Ignore — fall through with cron = null
}
}
const configKey = runtimeConfigKey(config);
if (!cron && config.enabled && !unavailableCronWarningEmitted) {
// Avoid a noisy startup-path warning when the gateway has not exposed cron yet.
@@ -751,6 +772,8 @@ export function registerShortTermPromotionDreaming(api: OpenClawPluginApi): void
};
api.on("gateway_start", async (_event, ctx) => {
// Store the gateway context for runtime cron resolution retries.
gatewayContext = ctx as unknown as { getCron?: () => CronServiceLike | null };
try {
await reconcileManagedDreamingCron({
reason: "startup",