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

@@ -13,6 +13,7 @@ import type {
import { normalizeHttpWebhookUrl } from "../webhook-url.js";
import {
normalizeOptionalAgentId,
normalizeOptionalSessionKey,
normalizeOptionalText,
normalizePayloadToSystemText,
normalizeRequiredName,
@@ -298,6 +299,7 @@ export function createJob(state: CronServiceState, input: CronJobCreate): CronJo
const job: CronJob = {
id,
agentId: normalizeOptionalAgentId(input.agentId),
sessionKey: normalizeOptionalSessionKey((input as { sessionKey?: unknown }).sessionKey),
name: normalizeRequiredName(input.name),
description: normalizeOptionalText(input.description),
enabled,
@@ -367,6 +369,9 @@ export function applyJobPatch(job: CronJob, patch: CronJobPatch) {
if ("agentId" in patch) {
job.agentId = normalizeOptionalAgentId((patch as { agentId?: unknown }).agentId);
}
if ("sessionKey" in patch) {
job.sessionKey = normalizeOptionalSessionKey((patch as { sessionKey?: unknown }).sessionKey);
}
assertSupportedJobSpec(job);
assertDeliverySupport(job);
}

View File

@@ -39,6 +39,14 @@ export function normalizeOptionalAgentId(raw: unknown) {
return normalizeAgentId(trimmed);
}
export function normalizeOptionalSessionKey(raw: unknown) {
if (typeof raw !== "string") {
return undefined;
}
const trimmed = raw.trim();
return trimmed || undefined;
}
export function inferLegacyName(job: {
schedule?: { kind?: unknown; everyMs?: unknown; expr?: unknown };
payload?: { kind?: unknown; text?: unknown; message?: unknown };

View File

@@ -43,9 +43,16 @@ export type CronServiceDeps = {
resolveSessionStorePath?: (agentId?: string) => string;
/** Path to the session store (sessions.json) for reaper use. */
sessionStorePath?: string;
enqueueSystemEvent: (text: string, opts?: { agentId?: string; contextKey?: string }) => void;
requestHeartbeatNow: (opts?: { reason?: string }) => void;
runHeartbeatOnce?: (opts?: { reason?: string; agentId?: string }) => Promise<HeartbeatRunResult>;
enqueueSystemEvent: (
text: string,
opts?: { agentId?: string; sessionKey?: string; contextKey?: string },
) => void;
requestHeartbeatNow: (opts?: { reason?: string; agentId?: string; sessionKey?: string }) => void;
runHeartbeatOnce?: (opts?: {
reason?: string;
agentId?: string;
sessionKey?: string;
}) => Promise<HeartbeatRunResult>;
/**
* WakeMode=now: max time to wait for runHeartbeatOnce to stop returning
* { status:"skipped", reason:"requests-in-flight" } before falling back to

View File

@@ -264,6 +264,15 @@ export async function ensureLoaded(
mutated = true;
}
if ("sessionKey" in raw) {
const sessionKey =
typeof raw.sessionKey === "string" ? normalizeOptionalText(raw.sessionKey) : undefined;
if (raw.sessionKey !== sessionKey) {
raw.sessionKey = sessionKey;
mutated = true;
}
}
if (typeof raw.enabled !== "boolean") {
raw.enabled = true;
mutated = true;

View File

@@ -453,6 +453,7 @@ async function executeJobCore(
}
state.deps.enqueueSystemEvent(text, {
agentId: job.agentId,
sessionKey: job.sessionKey,
contextKey: `cron:${job.id}`,
});
if (job.wakeMode === "now" && state.deps.runHeartbeatOnce) {
@@ -464,7 +465,11 @@ async function executeJobCore(
let heartbeatResult: HeartbeatRunResult;
for (;;) {
heartbeatResult = await state.deps.runHeartbeatOnce({ reason, agentId: job.agentId });
heartbeatResult = await state.deps.runHeartbeatOnce({
reason,
agentId: job.agentId,
sessionKey: job.sessionKey,
});
if (
heartbeatResult.status !== "skipped" ||
heartbeatResult.reason !== "requests-in-flight"
@@ -472,7 +477,11 @@ async function executeJobCore(
break;
}
if (state.deps.nowMs() - waitStartedAt > maxWaitMs) {
state.deps.requestHeartbeatNow({ reason });
state.deps.requestHeartbeatNow({
reason,
agentId: job.agentId,
sessionKey: job.sessionKey,
});
return { status: "ok", summary: text };
}
await delay(retryDelayMs);
@@ -486,7 +495,11 @@ async function executeJobCore(
return { status: "error", error: heartbeatResult.reason, summary: text };
}
} else {
state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
state.deps.requestHeartbeatNow({
reason: `cron:${job.id}`,
agentId: job.agentId,
sessionKey: job.sessionKey,
});
return { status: "ok", summary: text };
}
}
@@ -514,10 +527,15 @@ async function executeJobCore(
res.status === "error" ? `${prefix} (error): ${summaryText}` : `${prefix}: ${summaryText}`;
state.deps.enqueueSystemEvent(label, {
agentId: job.agentId,
sessionKey: job.sessionKey,
contextKey: `cron:${job.id}`,
});
if (job.wakeMode === "now") {
state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
state.deps.requestHeartbeatNow({
reason: `cron:${job.id}`,
agentId: job.agentId,
sessionKey: job.sessionKey,
});
}
}