fix(cron): recover flat schedule params and coerce non-object job values#57640
fix(cron): recover flat schedule params and coerce non-object job values#57640tyxben wants to merge 2 commits into
Conversation
Greptile SummaryThis PR fixes two related bugs in Key findings:
No test covers Confidence Score: 5/5Safe to merge; the root-cause fix is correct and all remaining findings are P2 edge-case gaps that do not affect the primary bug being addressed. Both open findings are P2: the staggerMs data-loss only occurs on a combination (cron shorthand + staggerMs flat param) that no current LLM is known to produce, and the misleading comment does not affect runtime behaviour. The core fix — schema relaxation, null/string coercion, and every/cron/at reconstruction — is correct and well-tested. src/agents/tools/cron-tool.ts — specifically the two duplicate schedule-reconstruction blocks (add and update paths) where staggerMs should be spread into the cron schedule object.
|
| Filename | Overview |
|---|---|
| src/agents/tools/cron-tool.ts | Schema relaxation and flat-params recovery extended; staggerMs is captured in JOB_KEYS but not propagated into the reconstructed cron schedule, causing silent data loss if an LLM sends it as a flat param |
| src/agents/tools/cron-tool.flat-params.test.ts | Six well-targeted test cases added for the new shorthand paths; no coverage for staggerMs as a flat param alongside cron |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/tools/cron-tool.ts
Line: 363-386
Comment:
**`staggerMs` silently dropped after cron-shorthand reconstruction**
`staggerMs` is now captured in `JOB_KEYS` (signalling that LLMs may send it as a flat param), but the `cron` shorthand reconstruction only spreads `tz` into the synthetic schedule — `staggerMs` is never added. The cleanup loop then deletes it, so a flat call like `{ cron: "0 0 * * *", staggerMs: 5000, message: "ping" }` produces `{ kind: "cron", expr: "0 0 * * *" }` with `staggerMs` silently discarded.
The same gap exists in the `update` code path (the duplicate reconstruction block around lines 556-600).
Consider spreading `staggerMs` into the cron schedule when present:
```typescript
} else if (typeof synthetic.cron === "string" && synthetic.cron.trim()) {
synthetic.schedule = {
kind: "cron",
expr: synthetic.cron.trim(),
...(typeof synthetic.tz === "string" && synthetic.tz.trim()
? { tz: synthetic.tz.trim() }
: {}),
...(typeof synthetic.staggerMs === "number" ? { staggerMs: synthetic.staggerMs } : {}),
};
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/agents/tools/cron-tool.ts
Line: 308-312
Comment:
**Misleading comment — `null` is not coerced here**
The comment says "Coerce non-object job values **(null**, string) to undefined", but because `typeof null === "object"` in JavaScript, the condition `params.job != null && typeof params.job !== "object"` evaluates as:
- `null`: `null != null` → `false` → the branch is **never entered**; `params.job` stays `null`
- `"some string"`: both checks pass → correctly coerced to `undefined` ✓
`null` still works because the immediately-following `!params.job` check is truthy for `null`, so flat-params recovery does trigger. But the comment and the test name (`"coerces null job to undefined"`) misrepresent what the code actually does, which could mislead future readers.
```suggestion
// Coerce non-null non-object job values (e.g. a bare string) to undefined so
// flat-params recovery triggers instead of failing schema validation (#56996).
// Note: null is handled implicitly below by `!params.job` being truthy.
if (params.job != null && typeof params.job !== "object") {
params.job = undefined;
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(cron): recover flat schedule params ..." | Re-trigger Greptile
| // Reconstruct schedule from shorthand keys when not already present. | ||
| if (!synthetic.schedule) { | ||
| const everyMs = | ||
| typeof synthetic.everyMs === "number" | ||
| ? synthetic.everyMs | ||
| : typeof synthetic.every === "number" | ||
| ? synthetic.every | ||
| : undefined; | ||
| if (typeof everyMs === "number" && everyMs > 0) { | ||
| synthetic.schedule = { kind: "every", everyMs }; | ||
| } else if (typeof synthetic.cron === "string" && synthetic.cron.trim()) { | ||
| synthetic.schedule = { | ||
| kind: "cron", | ||
| expr: synthetic.cron.trim(), | ||
| ...(typeof synthetic.tz === "string" && synthetic.tz.trim() | ||
| ? { tz: synthetic.tz.trim() } | ||
| : {}), | ||
| }; | ||
| } else if (synthetic.at !== undefined) { | ||
| const atValue = | ||
| typeof synthetic.at === "string" ? synthetic.at : JSON.stringify(synthetic.at); | ||
| synthetic.schedule = { kind: "at", at: atValue }; | ||
| } | ||
| } |
There was a problem hiding this comment.
staggerMs silently dropped after cron-shorthand reconstruction
staggerMs is now captured in JOB_KEYS (signalling that LLMs may send it as a flat param), but the cron shorthand reconstruction only spreads tz into the synthetic schedule — staggerMs is never added. The cleanup loop then deletes it, so a flat call like { cron: "0 0 * * *", staggerMs: 5000, message: "ping" } produces { kind: "cron", expr: "0 0 * * *" } with staggerMs silently discarded.
The same gap exists in the update code path (the duplicate reconstruction block around lines 556-600).
Consider spreading staggerMs into the cron schedule when present:
} else if (typeof synthetic.cron === "string" && synthetic.cron.trim()) {
synthetic.schedule = {
kind: "cron",
expr: synthetic.cron.trim(),
...(typeof synthetic.tz === "string" && synthetic.tz.trim()
? { tz: synthetic.tz.trim() }
: {}),
...(typeof synthetic.staggerMs === "number" ? { staggerMs: synthetic.staggerMs } : {}),
};Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/cron-tool.ts
Line: 363-386
Comment:
**`staggerMs` silently dropped after cron-shorthand reconstruction**
`staggerMs` is now captured in `JOB_KEYS` (signalling that LLMs may send it as a flat param), but the `cron` shorthand reconstruction only spreads `tz` into the synthetic schedule — `staggerMs` is never added. The cleanup loop then deletes it, so a flat call like `{ cron: "0 0 * * *", staggerMs: 5000, message: "ping" }` produces `{ kind: "cron", expr: "0 0 * * *" }` with `staggerMs` silently discarded.
The same gap exists in the `update` code path (the duplicate reconstruction block around lines 556-600).
Consider spreading `staggerMs` into the cron schedule when present:
```typescript
} else if (typeof synthetic.cron === "string" && synthetic.cron.trim()) {
synthetic.schedule = {
kind: "cron",
expr: synthetic.cron.trim(),
...(typeof synthetic.tz === "string" && synthetic.tz.trim()
? { tz: synthetic.tz.trim() }
: {}),
...(typeof synthetic.staggerMs === "number" ? { staggerMs: synthetic.staggerMs } : {}),
};
```
How can I resolve this? If you propose a fix, please make it concise.| // Coerce non-object job values (null, string) to undefined so flat-params | ||
| // recovery triggers instead of failing schema validation (#56996). | ||
| if (params.job != null && typeof params.job !== "object") { | ||
| params.job = undefined; | ||
| } |
There was a problem hiding this comment.
Misleading comment —
null is not coerced here
The comment says "Coerce non-object job values (null, string) to undefined", but because typeof null === "object" in JavaScript, the condition params.job != null && typeof params.job !== "object" evaluates as:
null:null != null→false→ the branch is never entered;params.jobstaysnull"some string": both checks pass → correctly coerced toundefined✓
null still works because the immediately-following !params.job check is truthy for null, so flat-params recovery does trigger. But the comment and the test name ("coerces null job to undefined") misrepresent what the code actually does, which could mislead future readers.
| // Coerce non-object job values (null, string) to undefined so flat-params | |
| // recovery triggers instead of failing schema validation (#56996). | |
| if (params.job != null && typeof params.job !== "object") { | |
| params.job = undefined; | |
| } | |
| // Coerce non-null non-object job values (e.g. a bare string) to undefined so | |
| // flat-params recovery triggers instead of failing schema validation (#56996). | |
| // Note: null is handled implicitly below by `!params.job` being truthy. | |
| if (params.job != null && typeof params.job !== "object") { | |
| params.job = undefined; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/cron-tool.ts
Line: 308-312
Comment:
**Misleading comment — `null` is not coerced here**
The comment says "Coerce non-object job values **(null**, string) to undefined", but because `typeof null === "object"` in JavaScript, the condition `params.job != null && typeof params.job !== "object"` evaluates as:
- `null`: `null != null` → `false` → the branch is **never entered**; `params.job` stays `null`
- `"some string"`: both checks pass → correctly coerced to `undefined` ✓
`null` still works because the immediately-following `!params.job` check is truthy for `null`, so flat-params recovery does trigger. But the comment and the test name (`"coerces null job to undefined"`) misrepresent what the code actually does, which could mislead future readers.
```suggestion
// Coerce non-null non-object job values (e.g. a bare string) to undefined so
// flat-params recovery triggers instead of failing schema validation (#56996).
// Note: null is handled implicitly below by `!params.job` being truthy.
if (params.job != null && typeof params.job !== "object") {
params.job = undefined;
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a10e57810
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (params.job != null && typeof params.job !== "object") { | ||
| params.job = undefined; | ||
| } |
There was a problem hiding this comment.
Coerce array-valued job inputs before recovery
Because job now uses a fully permissive schema, arrays are accepted at the tool boundary, but this guard only rewrites non-object primitives. In the add flow, a non-empty array like job: [{...}] skips flat-params recovery, normalizeCronJobCreate returns null, and the ?? params.job fallback forwards the array to cron.add, causing downstream validation failure instead of recovering or failing fast with a clear error. The same pattern exists for patch in update, so array inputs should be treated as invalid wrappers (like strings/null) before normalization.
Useful? React with 👍 / 👎.
6a10e57 to
aaebd7f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aaebd7f19a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ...(typeof synthetic.staggerMs === "number" | ||
| ? { staggerMs: synthetic.staggerMs } | ||
| : {}), |
There was a problem hiding this comment.
Preserve exact/stagger shorthand when rebuilding cron schedules
In flat-params recovery, cron schedules only carry staggerMs when it is already a number, and then the code deletes exact/stagger/staggerMs from the synthetic payload. That means inputs like cron: "0 * * * *", exact: true or staggerMs: "0" (which normalizeCronStaggerMs otherwise accepts) lose the explicit stagger override and fall back to default top-of-hour staggering, changing when jobs run. Please translate exact/string staggerMs/stagger into schedule.staggerMs before the cleanup step (the same issue is duplicated in the update recovery block).
Useful? React with 👍 / 👎.
|
This is related to the legacy cron shape cleanup, but I’m leaving it open as a separate track. #71534 fixed persisted legacy Next useful step here is resolving the existing review comments, especially preserving flat |
|
Thanks for the useful flat-schedule recovery work here. I reworked the safe subset into #71547 and merged it to main as 9242713. That version preserves flat |
Summary
Fixes #56996 —
cron addfails with "job: must be object" when LLMs send flat parameters.Root cause: Two issues in
cron-tool.ts:jobfield usedType.Objectin the tool schema, which rejectsnull/non-object values at provider-level validation before the tool's flat-params recovery can run.name,message,schedule, etc.) but missed schedule shorthand keys (every,everyMs,cron,at,tz), so LLM-generated flat params like{ every: 300000, message: "ping" }lost the schedule.Changes:
job/patchschema fromType.ObjecttoType.Unsafe({})so provider-level validation accepts any valuejob/patchvalues (null, string) toundefinedbefore flat-params recoveryJOB_KEYS/PATCH_KEYS(every,everyMs,cron,at,tz,stagger,staggerMs,exact)scheduleobject from shorthand keys when not already presentscheduleto avoid downstream schema rejectionaddandupdatecode pathsTest plan
cron-tool.flat-params.test.ts:everyshorthand → reconstructs{ kind: "every", everyMs }schedulecron+tzshorthand → reconstructs{ kind: "cron", expr, tz }scheduleatshorthand → reconstructs{ kind: "at", at }scheduleeveryMsshorthand → reconstructs schedule correctlyjob: null→ coerced to undefined, triggers flat-params recoveryupdateaction witheveryshorthand → reconstructs schedule in patch