fix(cron-tool): add typed properties to job/patch schemas (#55043)

Merged via squash.

Prepared head SHA: 979bb0e8b7
Co-authored-by: brunolorente <127802443+brunolorente@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
Bruno Lorente
2026-04-01 22:41:19 +02:00
committed by GitHub
parent 7027dda8cd
commit ca76e2fedc
10 changed files with 1260 additions and 74 deletions

View File

@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { validateCronAddParams, validateCronUpdateParams } from "../gateway/protocol/index.js";
import { normalizeCronJobCreate, normalizeCronJobPatch } from "./normalize.js";
import { DEFAULT_TOP_OF_HOUR_STAGGER_MS } from "./stagger.js";
@@ -343,6 +344,27 @@ describe("normalizeCronJobCreate", () => {
expect(delivery.to).toBe("https://example.invalid/cron");
});
it("does not default explicit mode-less delivery objects to announce", () => {
const normalized = normalizeCronJobCreate({
name: "implicit announce",
enabled: true,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "hello" },
delivery: {
channel: "telegram",
to: "123",
},
}) as unknown as Record<string, unknown>;
const delivery = normalized.delivery as Record<string, unknown>;
expect(delivery.mode).toBeUndefined();
expect(delivery.channel).toBe("telegram");
expect(delivery.to).toBe("123");
expect(validateCronAddParams(normalized)).toBe(false);
});
it("defaults isolated agentTurn delivery to announce", () => {
const normalized = normalizeIsolatedAgentTurnCreateJob({
name: "default-announce",
@@ -391,6 +413,7 @@ describe("normalizeCronJobCreate", () => {
model: " openrouter/deepseek/deepseek-r1 ",
thinking: " high ",
timeoutSeconds: 45,
toolsAllow: [" exec ", " read "],
allowUnsafeExternalContent: true,
}) as unknown as Record<string, unknown>;
@@ -398,7 +421,9 @@ describe("normalizeCronJobCreate", () => {
expect(payload.model).toBe("openrouter/deepseek/deepseek-r1");
expect(payload.thinking).toBe("high");
expect(payload.timeoutSeconds).toBe(45);
expect(payload.toolsAllow).toEqual(["exec", "read"]);
expect(payload.allowUnsafeExternalContent).toBe(true);
expect(validateCronAddParams(normalized)).toBe(true);
});
it("preserves timeoutSeconds=0 for no-timeout agentTurn payloads", () => {
@@ -413,6 +438,100 @@ describe("normalizeCronJobCreate", () => {
expect(payload.timeoutSeconds).toBe(0);
});
it("preserves empty toolsAllow lists for create jobs", () => {
const normalized = normalizeCronJobCreate({
name: "empty-tools",
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: {
kind: "agentTurn",
message: "hello",
toolsAllow: [],
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.toolsAllow).toEqual([]);
expect(validateCronAddParams(normalized)).toBe(true);
});
it("prunes agentTurn-only payload fields from systemEvent create jobs", () => {
const normalized = normalizeCronJobCreate({
name: "system-event-prune",
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "main",
wakeMode: "now",
payload: {
kind: "systemEvent",
text: "hello",
model: "openai/gpt-5",
fallbacks: ["openai/gpt-4.1-mini"],
thinking: "high",
timeoutSeconds: 45,
lightContext: true,
toolsAllow: ["exec"],
allowUnsafeExternalContent: true,
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload).toEqual({ kind: "systemEvent", text: "hello" });
expect(validateCronAddParams(normalized)).toBe(true);
});
it("prunes schedule fields that do not belong to at schedules for create jobs", () => {
const normalized = normalizeCronJobCreate({
name: "at-prune",
schedule: {
kind: "at",
at: "2026-01-12T18:00:00Z",
expr: "* * * * *",
everyMs: 60_000,
anchorMs: 123,
tz: "UTC",
staggerMs: 30_000,
},
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: {
kind: "systemEvent",
text: "hi",
},
}) as unknown as Record<string, unknown>;
const schedule = normalized.schedule as Record<string, unknown>;
expect(schedule).toEqual({
kind: "at",
at: new Date("2026-01-12T18:00:00Z").toISOString(),
});
expect(validateCronAddParams(normalized)).toBe(true);
});
it("prunes staggerMs from every schedules for create jobs", () => {
const normalized = normalizeCronJobCreate({
name: "every-prune",
schedule: {
kind: "every",
everyMs: 60_000,
staggerMs: 30_000,
},
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: {
kind: "systemEvent",
text: "hi",
},
}) as unknown as Record<string, unknown>;
const schedule = normalized.schedule as Record<string, unknown>;
expect(schedule).toEqual({
kind: "every",
everyMs: 60_000,
});
expect(validateCronAddParams(normalized)).toBe(true);
});
it("coerces sessionTarget and wakeMode casing", () => {
const normalized = normalizeCronJobCreate({
name: "casing",
@@ -477,6 +596,16 @@ describe("normalizeCronJobCreate", () => {
});
describe("normalizeCronJobPatch", () => {
it("infers agentTurn payloads from top-level model-only patch hints", () => {
const normalized = normalizeCronJobPatch({
model: "openrouter/deepseek/deepseek-r1",
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.model).toBe("openrouter/deepseek/deepseek-r1");
});
it("infers agentTurn kind for model-only payload patches", () => {
const normalized = normalizeCronJobPatch({
payload: {
@@ -489,6 +618,127 @@ describe("normalizeCronJobPatch", () => {
expect(payload.model).toBe("anthropic/claude-sonnet-4-5");
});
it("infers agentTurn kind for lightContext-only payload patches", () => {
const normalized = normalizeCronJobPatch({
payload: {
lightContext: true,
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.lightContext).toBe(true);
});
it("maps top-level fallback lists into agentTurn payload patches", () => {
const normalized = normalizeCronJobPatch({
fallbacks: [" openrouter/gpt-4.1-mini ", "anthropic/claude-haiku-3-5"],
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.fallbacks).toEqual(["openrouter/gpt-4.1-mini", "anthropic/claude-haiku-3-5"]);
});
it("maps top-level toolsAllow lists into agentTurn payload patches", () => {
const normalized = normalizeCronJobPatch({
toolsAllow: [" exec ", " read "],
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.toolsAllow).toEqual(["exec", "read"]);
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true);
});
it("preserves empty fallback lists so patches can disable fallbacks", () => {
const normalized = normalizeCronJobPatch({
payload: {
fallbacks: [],
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.fallbacks).toEqual([]);
});
it("preserves empty toolsAllow lists so patches can disable all tools", () => {
const normalized = normalizeCronJobPatch({
payload: {
toolsAllow: [],
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.toolsAllow).toEqual([]);
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true);
});
it("infers agentTurn kind for fallback-only payload patches", () => {
const normalized = normalizeCronJobPatch({
payload: {
fallbacks: [" openrouter/gpt-4.1-mini ", "anthropic/claude-haiku-3-5"],
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.fallbacks).toEqual(["openrouter/gpt-4.1-mini", "anthropic/claude-haiku-3-5"]);
});
it("does not infer agentTurn kind for malformed fallback-only payload patches", () => {
const normalized = normalizeCronJobPatch({
payload: {
fallbacks: [123],
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBeUndefined();
expect(payload.fallbacks).toBeUndefined();
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(false);
});
it("infers agentTurn kind for toolsAllow-only payload patches", () => {
const normalized = normalizeCronJobPatch({
payload: {
toolsAllow: [" exec ", " read "],
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.toolsAllow).toEqual(["exec", "read"]);
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true);
});
it("does not infer agentTurn kind for malformed toolsAllow-only payload patches", () => {
const normalized = normalizeCronJobPatch({
payload: {
toolsAllow: [123],
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBeUndefined();
expect(payload.toolsAllow).toBeUndefined();
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(false);
});
it("preserves null toolsAllow so patches can clear the allow-list", () => {
const normalized = normalizeCronJobPatch({
payload: {
toolsAllow: null,
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload.kind).toBe("agentTurn");
expect(payload.toolsAllow).toBeNull();
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true);
});
it("does not infer agentTurn kind for delivery-only legacy hints", () => {
const normalized = normalizeCronJobPatch({
payload: {
@@ -534,4 +784,62 @@ describe("normalizeCronJobPatch", () => {
expect(normalized.delivery).toBeUndefined();
expect((normalized.payload as Record<string, unknown>).threadId).toBeUndefined();
});
it("prunes agentTurn-only payload fields from systemEvent patch payloads", () => {
const normalized = normalizeCronJobPatch({
payload: {
kind: "systemEvent",
text: "hi",
model: "openai/gpt-5",
fallbacks: ["openai/gpt-4.1-mini"],
thinking: "high",
timeoutSeconds: 15,
lightContext: true,
toolsAllow: ["exec"],
allowUnsafeExternalContent: true,
},
}) as unknown as Record<string, unknown>;
const payload = normalized.payload as Record<string, unknown>;
expect(payload).toEqual({ kind: "systemEvent", text: "hi" });
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true);
});
it("prunes schedule fields that do not belong to at schedules for patches", () => {
const normalized = normalizeCronJobPatch({
schedule: {
kind: "at",
at: "2026-01-12T18:00:00Z",
expr: "* * * * *",
everyMs: 60_000,
anchorMs: 123,
tz: "UTC",
staggerMs: 30_000,
},
}) as unknown as Record<string, unknown>;
const schedule = normalized.schedule as Record<string, unknown>;
expect(schedule).toEqual({
kind: "at",
at: new Date("2026-01-12T18:00:00Z").toISOString(),
});
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true);
});
it("prunes staggerMs from every schedules for patches", () => {
const normalized = normalizeCronJobPatch({
schedule: {
kind: "every",
everyMs: 60_000,
staggerMs: 30_000,
},
}) as unknown as Record<string, unknown>;
const schedule = normalized.schedule as Record<string, unknown>;
expect(schedule).toEqual({
kind: "every",
everyMs: 60_000,
});
expect(validateCronUpdateParams({ id: "job-1", patch: normalized })).toBe(true);
});
});

View File

@@ -23,6 +23,41 @@ const DEFAULT_OPTIONS: NormalizeOptions = {
applyDefaults: false,
};
function hasTrimmedStringValue(value: unknown) {
return parseOptionalField(TrimmedNonEmptyStringFieldSchema, value) !== undefined;
}
function hasAgentTurnPayloadHint(payload: UnknownRecord) {
return (
hasTrimmedStringValue(payload.model) ||
normalizeTrimmedStringArray(payload.fallbacks) !== undefined ||
normalizeTrimmedStringArray(payload.toolsAllow, { allowNull: true }) !== undefined ||
hasTrimmedStringValue(payload.thinking) ||
typeof payload.timeoutSeconds === "number" ||
typeof payload.lightContext === "boolean" ||
typeof payload.allowUnsafeExternalContent === "boolean"
);
}
function normalizeTrimmedStringArray(
value: unknown,
options?: { allowNull?: boolean },
): string[] | null | undefined {
if (Array.isArray(value)) {
const normalized = value
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
.map((entry) => entry.trim());
if (normalized.length === 0 && value.length > 0) {
return undefined;
}
return normalized;
}
if (options?.allowNull && value === null) {
return null;
}
return undefined;
}
function coerceSchedule(schedule: UnknownRecord) {
const next: UnknownRecord = { ...schedule };
const rawKind = typeof schedule.kind === "string" ? schedule.kind.trim().toLowerCase() : "";
@@ -83,6 +118,23 @@ function coerceSchedule(schedule: UnknownRecord) {
delete next.staggerMs;
}
if (next.kind === "at") {
delete next.everyMs;
delete next.anchorMs;
delete next.expr;
delete next.tz;
delete next.staggerMs;
} else if (next.kind === "every") {
delete next.at;
delete next.expr;
delete next.tz;
delete next.staggerMs;
} else if (next.kind === "cron") {
delete next.at;
delete next.everyMs;
delete next.anchorMs;
}
return next;
}
@@ -99,16 +151,11 @@ function coercePayload(payload: UnknownRecord) {
if (!next.kind) {
const hasMessage = typeof next.message === "string" && next.message.trim().length > 0;
const hasText = typeof next.text === "string" && next.text.trim().length > 0;
const hasAgentTurnHint =
typeof next.model === "string" ||
typeof next.thinking === "string" ||
typeof next.timeoutSeconds === "number" ||
typeof next.allowUnsafeExternalContent === "boolean";
if (hasMessage) {
next.kind = "agentTurn";
} else if (hasText) {
next.kind = "systemEvent";
} else if (hasAgentTurnHint) {
} else if (hasAgentTurnPayloadHint(next)) {
// Accept partial agentTurn payload patches that only tweak agent-turn-only fields.
next.kind = "agentTurn";
}
@@ -149,26 +196,39 @@ function coercePayload(payload: UnknownRecord) {
delete next.timeoutSeconds;
}
}
if ("fallbacks" in next) {
const fallbacks = normalizeTrimmedStringArray(next.fallbacks);
if (fallbacks !== undefined) {
next.fallbacks = fallbacks;
} else {
delete next.fallbacks;
}
}
if ("toolsAllow" in next) {
const toolsAllow = normalizeTrimmedStringArray(next.toolsAllow, { allowNull: true });
if (toolsAllow !== undefined) {
next.toolsAllow = toolsAllow;
} else {
delete next.toolsAllow;
}
}
if (
"allowUnsafeExternalContent" in next &&
typeof next.allowUnsafeExternalContent !== "boolean"
) {
delete next.allowUnsafeExternalContent;
}
if ("toolsAllow" in next) {
if (Array.isArray(next.toolsAllow)) {
next.toolsAllow = (next.toolsAllow as unknown[])
.filter((t): t is string => typeof t === "string" && t.trim().length > 0)
.map((t) => t.trim());
if ((next.toolsAllow as string[]).length === 0) {
delete next.toolsAllow;
}
} else if (next.toolsAllow === null) {
// Explicit null means "clear the allow-list" (edit --clear-tools)
next.toolsAllow = null;
} else {
delete next.toolsAllow;
}
if (next.kind === "systemEvent") {
delete next.message;
delete next.model;
delete next.fallbacks;
delete next.thinking;
delete next.timeoutSeconds;
delete next.lightContext;
delete next.allowUnsafeExternalContent;
delete next.toolsAllow;
} else if (next.kind === "agentTurn") {
delete next.text;
}
if ("deliver" in next) {
delete next.deliver;
@@ -222,6 +282,24 @@ function coerceDelivery(delivery: UnknownRecord) {
return next;
}
function inferTopLevelPayload(next: UnknownRecord) {
const message = typeof next.message === "string" ? next.message.trim() : "";
if (message) {
return { kind: "agentTurn", message } satisfies UnknownRecord;
}
const text = typeof next.text === "string" ? next.text.trim() : "";
if (text) {
return { kind: "systemEvent", text } satisfies UnknownRecord;
}
if (hasAgentTurnPayloadHint(next)) {
return { kind: "agentTurn" } satisfies UnknownRecord;
}
return null;
}
function unwrapJob(raw: UnknownRecord) {
if (isRecord(raw.data)) {
return raw.data;
@@ -278,6 +356,21 @@ function copyTopLevelAgentTurnFields(next: UnknownRecord, payload: UnknownRecord
if (typeof payload.timeoutSeconds !== "number" && typeof next.timeoutSeconds === "number") {
payload.timeoutSeconds = next.timeoutSeconds;
}
if (!Array.isArray(payload.fallbacks) && Array.isArray(next.fallbacks)) {
const fallbacks = normalizeTrimmedStringArray(next.fallbacks);
if (fallbacks !== undefined) {
payload.fallbacks = fallbacks;
}
}
if (!("toolsAllow" in payload) || payload.toolsAllow === undefined) {
const toolsAllow = normalizeTrimmedStringArray(next.toolsAllow, { allowNull: true });
if (toolsAllow !== undefined) {
payload.toolsAllow = toolsAllow;
}
}
if (typeof payload.lightContext !== "boolean" && typeof next.lightContext === "boolean") {
payload.lightContext = next.lightContext;
}
if (
typeof payload.allowUnsafeExternalContent !== "boolean" &&
typeof next.allowUnsafeExternalContent === "boolean"
@@ -290,12 +383,16 @@ function stripLegacyTopLevelFields(next: UnknownRecord) {
delete next.model;
delete next.thinking;
delete next.timeoutSeconds;
delete next.fallbacks;
delete next.lightContext;
delete next.toolsAllow;
delete next.allowUnsafeExternalContent;
delete next.message;
delete next.text;
delete next.deliver;
delete next.channel;
delete next.to;
delete next.toolsAllow;
delete next.threadId;
delete next.bestEffortDeliver;
delete next.provider;
@@ -377,12 +474,9 @@ export function normalizeCronJobInput(
}
if (!("payload" in next) || !isRecord(next.payload)) {
const message = typeof next.message === "string" ? next.message.trim() : "";
const text = typeof next.text === "string" ? next.text.trim() : "";
if (message) {
next.payload = { kind: "agentTurn", message };
} else if (text) {
next.payload = { kind: "systemEvent", text };
const inferredPayload = inferTopLevelPayload(next);
if (inferredPayload) {
next.payload = inferredPayload;
}
}

View File

@@ -169,6 +169,81 @@ describe("applyJobPatch", () => {
}
});
it("persists agentTurn payload.fallbacks updates when editing existing jobs", () => {
const job = createIsolatedAgentTurnJob("job-fallbacks", {
mode: "announce",
channel: "telegram",
});
job.payload = {
kind: "agentTurn",
message: "do it",
fallbacks: ["openrouter/gpt-4.1-mini"],
};
applyJobPatch(job, {
payload: {
kind: "agentTurn",
message: "do it",
fallbacks: ["anthropic/claude-haiku-3-5", "openai/gpt-5"],
},
});
expect(job.payload.kind).toBe("agentTurn");
if (job.payload.kind === "agentTurn") {
expect(job.payload.fallbacks).toEqual(["anthropic/claude-haiku-3-5", "openai/gpt-5"]);
}
});
it("persists agentTurn payload.toolsAllow updates when editing existing jobs", () => {
const job = createIsolatedAgentTurnJob("job-tools", {
mode: "announce",
channel: "telegram",
});
job.payload = {
kind: "agentTurn",
message: "do it",
toolsAllow: ["exec"],
};
applyJobPatch(job, {
payload: {
kind: "agentTurn",
message: "do it",
toolsAllow: ["read", "write"],
},
});
expect(job.payload.kind).toBe("agentTurn");
if (job.payload.kind === "agentTurn") {
expect(job.payload.toolsAllow).toEqual(["read", "write"]);
}
});
it("clears agentTurn payload.toolsAllow when patch requests null", () => {
const job = createIsolatedAgentTurnJob("job-tools-clear", {
mode: "announce",
channel: "telegram",
});
job.payload = {
kind: "agentTurn",
message: "do it",
toolsAllow: ["exec", "read"],
};
applyJobPatch(job, {
payload: {
kind: "agentTurn",
message: "do it",
toolsAllow: null,
},
});
expect(job.payload.kind).toBe("agentTurn");
if (job.payload.kind === "agentTurn") {
expect(job.payload.toolsAllow).toBeUndefined();
}
});
it("applies payload.lightContext when replacing payload kind via patch", () => {
const job = createIsolatedAgentTurnJob("job-light-context-switch", {
mode: "announce",
@@ -191,6 +266,50 @@ describe("applyJobPatch", () => {
}
});
it("carries payload.fallbacks when replacing payload kind via patch", () => {
const job = createIsolatedAgentTurnJob("job-fallbacks-switch", {
mode: "announce",
channel: "telegram",
});
job.payload = { kind: "systemEvent", text: "ping" };
applyJobPatch(job, {
payload: {
kind: "agentTurn",
message: "do it",
fallbacks: ["anthropic/claude-haiku-3-5", "openai/gpt-5"],
},
});
const payload = job.payload as CronJob["payload"];
expect(payload.kind).toBe("agentTurn");
if (payload.kind === "agentTurn") {
expect(payload.fallbacks).toEqual(["anthropic/claude-haiku-3-5", "openai/gpt-5"]);
}
});
it("carries payload.toolsAllow when replacing payload kind via patch", () => {
const job = createIsolatedAgentTurnJob("job-tools-switch", {
mode: "announce",
channel: "telegram",
});
job.payload = { kind: "systemEvent", text: "ping" };
applyJobPatch(job, {
payload: {
kind: "agentTurn",
message: "do it",
toolsAllow: ["exec", "read"],
},
});
const payload = job.payload as CronJob["payload"];
expect(payload.kind).toBe("agentTurn");
if (payload.kind === "agentTurn") {
expect(payload.toolsAllow).toEqual(["exec", "read"]);
}
});
it.each([
{ name: "no delivery update", patch: { enabled: true } satisfies CronJobPatch },
{

View File

@@ -687,6 +687,14 @@ function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronP
if (typeof patch.model === "string") {
next.model = patch.model;
}
if (Array.isArray(patch.fallbacks)) {
next.fallbacks = patch.fallbacks;
}
if (Array.isArray(patch.toolsAllow)) {
next.toolsAllow = patch.toolsAllow;
} else if (patch.toolsAllow === null) {
delete next.toolsAllow;
}
if (typeof patch.thinking === "string") {
next.thinking = patch.thinking;
}
@@ -718,6 +726,8 @@ function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload {
kind: "agentTurn",
message: patch.message,
model: patch.model,
fallbacks: patch.fallbacks,
toolsAllow: Array.isArray(patch.toolsAllow) ? patch.toolsAllow : undefined,
thinking: patch.thinking,
timeoutSeconds: patch.timeoutSeconds,
lightContext: patch.lightContext,

View File

@@ -108,7 +108,9 @@ type CronAgentTurnPayload = {
type CronAgentTurnPayloadPatch = {
kind: "agentTurn";
} & Partial<CronAgentTurnPayloadFields>;
} & Partial<Omit<CronAgentTurnPayloadFields, "toolsAllow">> & {
toolsAllow?: string[] | null;
};
export type CronJobState = {
nextRunAtMs?: number;
runningAtMs?: number;