Files
openclaw/src/cron/session-target.ts
Fermin Quant c9364f03dc fix(cron): accept opaque session target keys
Fixes #64030.

Allows cron `session:` targets to carry opaque session-store keys, including slash and backslash characters, while keeping cron job IDs on the stricter UUID/non-path contract. Adds regression coverage across cron normalization, cron service persistence, gateway cron validation, and related session target handling.

Thanks @ferminquant for the fix.

Verification:
- `git diff --check origin/main...HEAD`
- `OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs src/cron/session-target.test.ts src/cron/normalize.test.ts src/cron/service.jobs.test.ts src/cron/service/store.test.ts src/gateway/server-cron.test.ts src/gateway/server.cron.test.ts src/cron/run-log.test.ts src/gateway/protocol/cron-validators.test.ts src/agents/tools/message-tool.test.ts src/agents/tools/image-tool.custom-provider-auth.regression.test.ts --reporter dot` passed: 13 files, 347 tests.
- GitHub `checks-node-agentic-agents` reran green on `51949741a333363586ddfb4445b82116c3bcea43`.

Co-authored-by: Fermin Quant <ferminquant@hotmail.com>
2026-05-26 01:39:04 +01:00

70 lines
2.1 KiB
TypeScript

const INVALID_CRON_SESSION_TARGET_ID_ERROR = "invalid cron sessionTarget session id";
export function isInvalidCronSessionTargetIdError(error: unknown): boolean {
return error instanceof Error && error.message === INVALID_CRON_SESSION_TARGET_ID_ERROR;
}
export function assertSafeCronSessionTargetId(sessionId: string): string {
const trimmed = sessionId.trim();
if (!trimmed) {
throw new Error(INVALID_CRON_SESSION_TARGET_ID_ERROR);
}
if (trimmed.includes("\0")) {
throw new Error(INVALID_CRON_SESSION_TARGET_ID_ERROR);
}
return trimmed;
}
export function resolveCronSessionTargetSessionKey(
sessionTarget?: string | null,
): string | undefined {
if (typeof sessionTarget !== "string" || !sessionTarget.startsWith("session:")) {
return undefined;
}
return assertSafeCronSessionTargetId(sessionTarget.slice(8));
}
export function resolveCronCurrentSessionTarget(params: {
sessionTarget?: string | null;
sessionKey?: string | null;
}): string | undefined {
if (params.sessionTarget !== "current") {
return params.sessionTarget ?? undefined;
}
const sessionKey = params.sessionKey?.trim();
return sessionKey ? `session:${assertSafeCronSessionTargetId(sessionKey)}` : "isolated";
}
export function resolveCronDeliverySessionKey(job: {
sessionTarget?: string | null;
sessionKey?: string | null;
}): string | undefined {
const sessionTargetKey = resolveCronSessionTargetSessionKey(job.sessionTarget);
if (sessionTargetKey) {
return sessionTargetKey;
}
return typeof job.sessionKey === "string" && job.sessionKey.trim()
? job.sessionKey.trim()
: undefined;
}
export function resolveCronNotificationSessionKey(params: {
jobId: string;
sessionKey?: string | null;
}): string {
return typeof params.sessionKey === "string" && params.sessionKey.trim()
? params.sessionKey.trim()
: `cron:${params.jobId}:failure`;
}
export function resolveCronFailureNotificationSessionKey(job: {
id: string;
sessionTarget?: string | null;
sessionKey?: string | null;
}): string {
return resolveCronNotificationSessionKey({
jobId: job.id,
sessionKey: resolveCronDeliverySessionKey(job),
});
}