feat(cron): event triggers — polled condition-watcher scripts via code mode#101195
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b1c33a6c3
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (patch.trigger === null || patch.trigger === undefined) { | ||
| delete job.trigger; | ||
| } else { | ||
| job.trigger = structuredClone(patch.trigger); |
There was a problem hiding this comment.
Reset stale trigger state when replacing scripts
When an existing trigger is edited (or cleared and later re-added), this only swaps job.trigger and leaves job.state.triggerState/last trigger metadata intact. In the common watcher pattern documented for comparing current status to trigger.state, changing a job from watching one resource to another can compare the new script against the old script's state and either suppress the first real event or fire for the wrong previous value. Clear the stored trigger state/counters when the trigger script changes or is removed, while preserving state for once-only edits.
Useful? React with 👍 / 👎.
|
Codex review: found issues before merge. Reviewed July 7, 2026, 2:06 PM ET / 18:06 UTC. Summary PR surface: Source +1367, Tests +1318, Docs +40, Generated +4, Other +12. Total +2741 across 53 files. Reproducibility: not applicable. as a user bug repro; this is a feature PR. The remaining review blockers are source-reproducible on the current PR head from the trigger patch, timer, and config schema paths. Review metrics: 3 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Land only after quiet polls create no run/task artifacts, trigger state resets on script replacement or removal, config metadata warns operators, and a maintainer accepts the default-off security boundary. Do we have a high-confidence way to reproduce the issue? Not applicable as a user bug repro; this is a feature PR. The remaining review blockers are source-reproducible on the current PR head from the trigger patch, timer, and config schema paths. Is this the best way to solve the issue? Not yet; the cron/code-mode owner boundary is plausible, but this head is not the best mergeable solution until quiet ticks are artifact-free, replacement scripts clear stale state, and config metadata carries the security warning. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 21f251220fc0. Label changesLabel justifications:
Evidence reviewedPR surface: Source +1367, Tests +1318, Docs +40, Generated +4, Other +12. Total +2741 across 53 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (4 earlier review cycles)
|
Real behavior proof — live end-to-end run (2026-07-06, isolated gateway on macOS)Setup: fresh 1. Agent authored the watcher itself via the 2. Quiet polling (the whole point): gateway evaluated the script headlessly every 5s — observed 3. Trip → fire: changed the signal file { "action": "finished", "status": "ok", "summary": "DONE", "triggerFired": true }4. Hosted gates: full Security note for reviewers: the run exercised the exact boundary flagged — agent-authored script executing unattended with the agent's tool policy — behind the default-off |
…e cycle for trigger types CI fixes for #101195: trigger evaluator result types move to the cron types leaf (madge cycle), cron tool schema inventory gains trigger, Swift protocol bindings + docs map + Linux prompt snapshots regenerated.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3359f18461
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (result.status === "ok" && result.triggerEval && !result.triggerEval.fired) { | ||
| // Quiet trigger ticks intentionally emit no finished event: run history, | ||
| // plugin hooks, and completion notifications represent payload runs only. | ||
| applyTriggerNoFireResult(state, job, { |
There was a problem hiding this comment.
Avoid run artifacts for quiet trigger polls
When a trigger returns fire: false, this branch runs only after runDueJob has already emitted the started cron event and created a task-ledger run; returning here suppresses the matching finished/run-log entry, so a quiet 30s watcher can produce endless started-only broadcasts/cron_changed hooks and completed task rows even though no payload ran. Evaluate the trigger before emitting/creating run artifacts, or suppress those artifacts for no-fire ticks.
Useful? React with 👍 / 👎.
…e cycle for trigger types CI fixes for #101195: trigger evaluator result types move to the cron types leaf (madge cycle), cron tool schema inventory gains trigger, Swift protocol bindings + docs map + Linux prompt snapshots regenerated.
3359f18 to
c07162e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c07162eaf5
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| triggers: z | ||
| .object({ | ||
| enabled: z.boolean().optional(), | ||
| minIntervalMs: z.number().int().positive().optional(), | ||
| }) |
There was a problem hiding this comment.
Document trigger config in schema metadata
This adds new public config keys (cron.triggers.enabled / minIntervalMs) but only the docs page was updated; src/config/schema-base.ts and src/config/schema.hints.ts populate public JSON schema/UI hints from FIELD_HELP and FIELD_LABELS, and those maps still have no cron.triggers* entries. In the config schema/Control UI, the security-sensitive opt-in for unattended headless tool execution is therefore exposed without the warning/default/min-interval guidance, making it easy for operators to enable or tune incorrectly; add labels/help alongside the schema keys.
Useful? React with 👍 / 👎.
Part A of cron event triggers: runCodeModeScriptHeadless runs a script to completion (exec/settle/resume, no snapshots, no session), plus the cron-facing evaluator with per-job tool catalog cache, semaphore, 16KB state cap, and closed failure taxonomy.
Part B: trigger field on cron jobs gated by cron.triggers.enabled; timer evaluates the script each due tick, quiet ticks leave no run history, fired runs append the script message to the payload; once semantics, min-interval floor, SQLite columns, RPC/CLI/agent-tool surfaces, and docs.
…e cycle for trigger types CI fixes for #101195: trigger evaluator result types move to the cron types leaf (madge cycle), cron tool schema inventory gains trigger, Swift protocol bindings + docs map + Linux prompt snapshots regenerated.
…symbol localization Upstream #101831 made registerToolSearchCatalog module-private; fold the headless ref-only catalog registration into one public seam.
7249f38 to
1481b60
Compare
|
Merged via squash.
|
…e mode (openclaw#101195) * feat(cron): add headless code-mode driver and trigger-script evaluator Part A of cron event triggers: runCodeModeScriptHeadless runs a script to completion (exec/settle/resume, no snapshots, no session), plus the cron-facing evaluator with per-job tool catalog cache, semaphore, 16KB state cap, and closed failure taxonomy. * fix(cron): correct trigger-script bootstrap flags and cache narrowing * feat(cron): add event triggers (polled condition-watcher scripts) Part B: trigger field on cron jobs gated by cron.triggers.enabled; timer evaluates the script each due tick, quiet ticks leave no run history, fired runs append the script message to the payload; once semantics, min-interval floor, SQLite columns, RPC/CLI/agent-tool surfaces, and docs. * fix(cron): propagate triggerEval through startup catch-up outcomes * fix(cron): honor cron staggering on quiet trigger ticks; fix trigger test types * fix(cron): reject with Error reason in trigger-script abort test * fix(cron): regenerate protocol/docs/snapshot artifacts and break madge cycle for trigger types CI fixes for openclaw#101195: trigger evaluator result types move to the cron types leaf (madge cycle), cron tool schema inventory gains trigger, Swift protocol bindings + docs map + Linux prompt snapshots regenerated. * fix(cron): drop underscore-dangle names in trigger code sync assert * fix(cron): adopt registerHeadlessToolSearchCatalog after tool-search symbol localization Upstream openclaw#101831 made registerToolSearchCatalog module-private; fold the headless ref-only catalog registration into one public seam.
…e mode (openclaw#101195) * feat(cron): add headless code-mode driver and trigger-script evaluator Part A of cron event triggers: runCodeModeScriptHeadless runs a script to completion (exec/settle/resume, no snapshots, no session), plus the cron-facing evaluator with per-job tool catalog cache, semaphore, 16KB state cap, and closed failure taxonomy. * fix(cron): correct trigger-script bootstrap flags and cache narrowing * feat(cron): add event triggers (polled condition-watcher scripts) Part B: trigger field on cron jobs gated by cron.triggers.enabled; timer evaluates the script each due tick, quiet ticks leave no run history, fired runs append the script message to the payload; once semantics, min-interval floor, SQLite columns, RPC/CLI/agent-tool surfaces, and docs. * fix(cron): propagate triggerEval through startup catch-up outcomes * fix(cron): honor cron staggering on quiet trigger ticks; fix trigger test types * fix(cron): reject with Error reason in trigger-script abort test * fix(cron): regenerate protocol/docs/snapshot artifacts and break madge cycle for trigger types CI fixes for openclaw#101195: trigger evaluator result types move to the cron types leaf (madge cycle), cron tool schema inventory gains trigger, Swift protocol bindings + docs map + Linux prompt snapshots regenerated. * fix(cron): drop underscore-dangle names in trigger code sync assert * fix(cron): adopt registerHeadlessToolSearchCatalog after tool-search symbol localization Upstream openclaw#101831 made registerToolSearchCatalog module-private; fold the headless ref-only catalog registration into one public seam.
Closes #101194
What Problem This Solves
Watching external state ("wake me when CI on PR 123 changes status") currently requires heartbeat- or cron-waking the full agent so the model can poll — every check burns tokens and adds latency even when nothing changed. Cron had no general event-shaped trigger beyond the
on-exitprocess watcher.Why This Change Was Made
Cron jobs gain an optional
trigger: { script, once? }onevery/cronschedules: a JavaScript condition script evaluated headlessly in the existing code-mode QuickJS sandbox on each due tick, with the owning agent's tool policy (tools.call) and persistedtrigger.statefor change detection. The script returns{ fire, message?, state? }; onlyfire: trueruns the payload through the normal cron pipeline (wake modes, delivery, run logs, retry/backoff, failure alerts). Poll-over-push keeps per-source core wiring at zero — anything a tool can reach becomes a trigger source — and the contract leaves room for push sources (webhooks/hook bus) to feed the same scripts later.Key decisions:
runCodeModeScriptHeadless): loops exec → settle bridge tool calls → resume to completion in one call; never stores snapshots, never creates a session, bypasses the agentexec/waitmachinery. Zero changes to the QuickJS worker;trigger.stateinjects via the existing namespace-descriptor path (deep-frozen; scripts return new state).cron_run_logsrows.cron.triggers.enabled(default off; enabling means agent-authored scripts run unattended with that agent's full tool policy, includingexec— docs carry a loud warning). Per-eval budgets: 30s wall clock, 5 tool calls, 16KB state, 30s min poll interval (configurable floor), max 3 concurrent evals, before-tool-call hooks still wrap script-initiated calls.cron_jobs(additiveensureColumnmigration, no schema version bump); eval counters/state ride the existingstate_json.cron.add/update+ protocol schemas), agentcrontool, CLI (--trigger-script <path|->,--trigger-once,--clear-trigger, list/get display),triggerFiredon run-log entries. Force-runs bypass evaluation;oncedisarms after the first successful fired payload.User Impact
Agents and operators can set up zero-token watchers: "check CI every 30s, wake me only on change." Existing cron jobs and configs are unaffected; the feature is off by default and trigger-bearing jobs validate against the enabled gate at create/patch time. Docs:
docs/automation/cron-jobs.mdgains an "Event triggers (condition watchers)" section.Evidence
src/agents/code-mode-headless.test.ts(10 — multi-round tool calls with no snapshot leakage, frozen state injection, budget/timeout/abort/failure taxonomy, override clamps),src/cron/trigger-script.test.ts(11 — result parsing, state cap, semaphore busy, single-flight cache, abort cancellation),src/cron/service.trigger-eval.test.ts(10 — quiet ticks leave no run history + counters persist, fired runs append message + marktriggerFired, evaluator errors ride backoff, busy skip,oncedisarm vs errored-fire retry, missing-evaluator error, force-run bypass, stagger preservation, state kept on failed fired payload), plus store codec round-trip, persisted-shape, normalize, protocol validator, zod config, CLI flag, and RPC gate coverage.pnpm check:changedgreen on Blacksmith Testbox (all lanes incl. typechecks, lint, kysely drift): https://github.com/openclaw/openclaw/actions/runs/28817511044triggerEval; quiet ticks bypassing cron staggering).pnpm db:kysely:checkclean after regen; branch rebased onto latestorigin/mainwith focused tests re-run post-rebase.Release-note context: feat(cron): event triggers — polled condition-watcher scripts run headlessly via code mode; payload fires only when the watched condition changes. Requested by @steipete.