Cron: route reminders by session namespace

This commit is contained in:
Vignesh Natarajan
2026-02-16 14:29:21 -08:00
committed by Peter Steinberger
parent f452a7a60b
commit f988abf202
19 changed files with 530 additions and 32 deletions

View File

@@ -259,6 +259,7 @@ function resolveHeartbeatSession(
cfg: OpenClawConfig,
agentId?: string,
heartbeat?: HeartbeatConfig,
forcedSessionKey?: string,
) {
const sessionCfg = cfg.session;
const scope = sessionCfg?.scope ?? "per-sender";
@@ -276,6 +277,31 @@ function resolveHeartbeatSession(
return { sessionKey: mainSessionKey, storePath, store, entry: mainEntry };
}
const forced = forcedSessionKey?.trim();
if (forced) {
const forcedCandidate = toAgentStoreSessionKey({
agentId: resolvedAgentId,
requestKey: forced,
mainKey: cfg.session?.mainKey,
});
const forcedCanonical = canonicalizeMainSessionAlias({
cfg,
agentId: resolvedAgentId,
sessionKey: forcedCandidate,
});
if (forcedCanonical !== "global") {
const sessionAgentId = resolveAgentIdFromSessionKey(forcedCanonical);
if (sessionAgentId === normalizeAgentId(resolvedAgentId)) {
return {
sessionKey: forcedCanonical,
storePath,
store,
entry: store[forcedCanonical],
};
}
}
}
const trimmed = heartbeat?.session?.trim() ?? "";
if (!trimmed) {
return { sessionKey: mainSessionKey, storePath, store, entry: mainEntry };
@@ -437,6 +463,7 @@ function normalizeHeartbeatReply(
export async function runHeartbeatOnce(opts: {
cfg?: OpenClawConfig;
agentId?: string;
sessionKey?: string;
heartbeat?: HeartbeatConfig;
reason?: string;
deps?: HeartbeatDeps;
@@ -493,7 +520,12 @@ export async function runHeartbeatOnce(opts: {
// The LLM prompt says "if it exists" so this is expected behavior.
}
const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat);
const { entry, sessionKey, storePath } = resolveHeartbeatSession(
cfg,
agentId,
heartbeat,
opts.sessionKey,
);
const previousUpdatedAt = entry?.updatedAt;
const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat });
const heartbeatAccountId = heartbeat?.accountId?.trim();
@@ -969,11 +1001,45 @@ export function startHeartbeatRunner(opts: {
}
const reason = params?.reason;
const requestedAgentId = params?.agentId ? normalizeAgentId(params.agentId) : undefined;
const requestedSessionKey = params?.sessionKey?.trim() || undefined;
const isInterval = reason === "interval";
const startedAt = Date.now();
const now = startedAt;
let ran = false;
if (requestedSessionKey || requestedAgentId) {
const targetAgentId = requestedAgentId ?? resolveAgentIdFromSessionKey(requestedSessionKey);
const targetAgent = state.agents.get(targetAgentId);
if (!targetAgent) {
scheduleNext();
return { status: "skipped", reason: "disabled" };
}
try {
const res = await runOnce({
cfg: state.cfg,
agentId: targetAgent.agentId,
heartbeat: targetAgent.heartbeat,
reason,
sessionKey: requestedSessionKey,
deps: { runtime: state.runtime },
});
if (res.status !== "skipped" || res.reason !== "disabled") {
advanceAgentSchedule(targetAgent, now);
}
scheduleNext();
return res.status === "ran" ? { status: "ran", durationMs: Date.now() - startedAt } : res;
} catch (err) {
const errMsg = formatErrorMessage(err);
log.error(`heartbeat runner: targeted runOnce threw unexpectedly: ${errMsg}`, {
error: errMsg,
});
advanceAgentSchedule(targetAgent, now);
scheduleNext();
return { status: "failed", reason: errMsg };
}
}
for (const agent of state.agents.values()) {
if (isInterval && now < agent.nextDueMs) {
continue;
@@ -1016,7 +1082,12 @@ export function startHeartbeatRunner(opts: {
return { status: "skipped", reason: isInterval ? "not-due" : "disabled" };
};
const wakeHandler: HeartbeatWakeHandler = async (params) => run({ reason: params.reason });
const wakeHandler: HeartbeatWakeHandler = async (params) =>
run({
reason: params.reason,
agentId: params.agentId,
sessionKey: params.sessionKey,
});
const disposeWakeHandler = setHeartbeatWakeHandler(wakeHandler);
updateConfig(state.cfg);