Description
The schedule_create tool exposed to Hands (see tool_runner.rs:2096) appears to register a recurring task, but the registered schedule never fires. This silently breaks every Hand that relies on the Phase 1 pattern documented in bundled HAND.toml files — e.g., timesheet-sync's HAND.toml: "Check schedule_list for an existing schedule. If missing, create one."
Root cause: there are two unrelated "scheduler" subsystems that share no wiring.
-
Kernel cron scheduler (openfang-kernel/src/cron.rs) — persists to ~/.openfang/cron_jobs.json; the kernel tick loop at kernel.rs:4354 calls kernel.cron_scheduler.due_jobs() every 15s and dispatches via cron_run_job. This is the path that actually fires jobs.
-
Tool-level schedules (tool_runner.rs + routes.rs) — schedule_create, schedule_list, schedule_delete, and the matching /api/schedules* HTTP routes read/write a shared-memory key __openfang_schedules. Nothing in the kernel ever reads this key. Grepping the whole tree confirms SCHEDULES_KEY / __openfang_schedules appears only in tool_runner.rs and routes.rs — CRUD only, no executor.
The tool returns "Schedule created: ID: …", schedule_list happily lists the entry, /api/schedules returns it as enabled: true — but last_run stays null and run_count stays 0 forever.
A secondary effect compounds this: Hands with max_iterations set are forced into ScheduleMode::Continuous { check_interval_secs: 3600 } (kernel.rs:3558, the #848 fix). So a scheduled Hand is woken up — but on a fixed hourly cadence entirely divorced from its schedule, and the Hand's autonomous-tick responses end up introspecting their own registered schedule and reporting "next sync at 14:00 UTC" without anything actually firing at 14:00 UTC. This is related to the open concerns in #756 (no explicit autonomous = true opt-in, no decoupling of max_iterations from schedule mode).
Expected Behavior
One of the following:
- Option A (preferred):
schedule_create registers a real cron job in cron_scheduler so the existing due_jobs() tick path picks it up. The __openfang_schedules memory key becomes the durable record, and kernel startup reconciles it into cron_scheduler. A Hand that calls schedule_create("daily at 9am", …) then actually gets invoked at 9am.
- Option B: add a new kernel background task that reads
__openfang_schedules on each tick, compares cron against now, and dispatches via cron_run_job or the agent message queue. Update last_run and run_count on fire.
Either way:
Steps to Reproduce
- Activate a bundled Hand that calls
schedule_create on first run — e.g., timesheet-sync (see crates/openfang-hands/bundled/timesheet-sync/HAND.toml Phase 1).
- Let the Hand boot and execute its first autonomous tick. It will call
schedule_create with something like "daily at 9am".
- Verify the schedule is registered:
curl -s http://127.0.0.1:4200/api/schedules
Returns the entry with cron: "0 14 * * *" (or similar), enabled: true, last_run: null, run_count: 0.
- Verify the kernel-cron file is untouched:
cat ~/.openfang/cron_jobs.json
# []
- Wait for the scheduled time. The Hand will not fire.
- Observe that the Hand still wakes up hourly with
[AUTONOMOUS TICK] messages (from the forced Continuous { check_interval_secs: 3600 } in kernel.rs:3558). These ticks consume tokens but do no useful work — the Hand introspects its own schedule and logs "waiting for next sync window."
- After the scheduled time has passed,
/api/schedules still shows last_run: null, run_count: 0.
OpenFang Version
0.5.9
Operating System
Linux (x86_64) — WSL2 Ubuntu on Windows 11
Logs / Screenshots
/api/schedules output — registered but never fired (~34 hours after creation):
{
"schedules": [{
"id": "daabf2e6-d9b3-44cd-86ef-efad8541ba8f",
"agent_id": "d92363e5-b9fe-5908-aa57-67b9f0383e6f",
"name": "timesheet-sync",
"cron": "0 14 * * *",
"created_at": "2026-04-16T05:32:55Z",
"enabled": true,
"last_run": null,
"run_count": 0,
"message": ""
}],
"total": 1
}
~/.openfang/cron_jobs.json:
Hand's own autonomous-tick log confirming it knew it should have fired at 14:00 UTC / 9 AM CDT, but nothing did:
## 11:55:04
**Status: ✅ Idle — approaching sync window**
| Next sync | 14:00 UTC (9 AM CDT) — ~1h away |
## 14:48:54
**No — the sync did not run.**
The scheduled window passed (~14:00 UTC / 9 AM CDT) without a sync firing.
This likely means the cron/schedule trigger isn't wired up yet to auto-invoke this agent.
Grep confirming no executor exists for __openfang_schedules:
$ rg -n "SCHEDULES_KEY|__openfang_schedules" crates/
crates/openfang-runtime/src/tool_runner.rs:2094 (CRUD only)
crates/openfang-api/src/routes.rs:8346 (CRUD only)
# No hits in openfang-kernel/ — no tick loop reads this key.
Related Issues
Description
The
schedule_createtool exposed to Hands (seetool_runner.rs:2096) appears to register a recurring task, but the registered schedule never fires. This silently breaks every Hand that relies on the Phase 1 pattern documented in bundled HAND.toml files — e.g., timesheet-sync's HAND.toml: "Checkschedule_listfor an existing schedule. If missing, create one."Root cause: there are two unrelated "scheduler" subsystems that share no wiring.
Kernel cron scheduler (
openfang-kernel/src/cron.rs) — persists to~/.openfang/cron_jobs.json; the kernel tick loop atkernel.rs:4354callskernel.cron_scheduler.due_jobs()every 15s and dispatches viacron_run_job. This is the path that actually fires jobs.Tool-level schedules (
tool_runner.rs+routes.rs) —schedule_create,schedule_list,schedule_delete, and the matching/api/schedules*HTTP routes read/write a shared-memory key__openfang_schedules. Nothing in the kernel ever reads this key. Grepping the whole tree confirmsSCHEDULES_KEY/__openfang_schedulesappears only intool_runner.rsandroutes.rs— CRUD only, no executor.The tool returns
"Schedule created: ID: …",schedule_listhappily lists the entry,/api/schedulesreturns it asenabled: true— butlast_runstaysnullandrun_countstays0forever.A secondary effect compounds this: Hands with
max_iterationsset are forced intoScheduleMode::Continuous { check_interval_secs: 3600 }(kernel.rs:3558, the #848 fix). So a scheduled Hand is woken up — but on a fixed hourly cadence entirely divorced from its schedule, and the Hand's autonomous-tick responses end up introspecting their own registered schedule and reporting "next sync at 14:00 UTC" without anything actually firing at 14:00 UTC. This is related to the open concerns in #756 (no explicitautonomous = trueopt-in, no decoupling ofmax_iterationsfrom schedule mode).Expected Behavior
One of the following:
schedule_createregisters a real cron job incron_schedulerso the existingdue_jobs()tick path picks it up. The__openfang_schedulesmemory key becomes the durable record, and kernel startup reconciles it intocron_scheduler. A Hand that callsschedule_create("daily at 9am", …)then actually gets invoked at 9am.__openfang_scheduleson each tick, comparescronagainstnow, and dispatches viacron_run_jobor the agent message queue. Updatelast_runandrun_counton fire.Either way:
POST /api/schedules/{id}/run(which exists and works) should share the dispatch path solast_run/run_countupdates are consistent whether the fire is scheduled or manual.__openfang_schedulesentry, the kernel should not also forceScheduleMode::Continuouson it — the schedule should be the trigger. (This is the decoupling from Hands with max_iterations silently become Continuous-mode agents, burning tokens on idle ticks #756.)Steps to Reproduce
schedule_createon first run — e.g.,timesheet-sync(seecrates/openfang-hands/bundled/timesheet-sync/HAND.tomlPhase 1).schedule_createwith something like"daily at 9am".cron: "0 14 * * *"(or similar),enabled: true,last_run: null,run_count: 0.[AUTONOMOUS TICK]messages (from the forcedContinuous { check_interval_secs: 3600 }inkernel.rs:3558). These ticks consume tokens but do no useful work — the Hand introspects its own schedule and logs "waiting for next sync window."/api/schedulesstill showslast_run: null,run_count: 0.OpenFang Version
0.5.9
Operating System
Linux (x86_64) — WSL2 Ubuntu on Windows 11
Logs / Screenshots
/api/schedulesoutput — registered but never fired (~34 hours after creation):{ "schedules": [{ "id": "daabf2e6-d9b3-44cd-86ef-efad8541ba8f", "agent_id": "d92363e5-b9fe-5908-aa57-67b9f0383e6f", "name": "timesheet-sync", "cron": "0 14 * * *", "created_at": "2026-04-16T05:32:55Z", "enabled": true, "last_run": null, "run_count": 0, "message": "" }], "total": 1 }~/.openfang/cron_jobs.json:Hand's own autonomous-tick log confirming it knew it should have fired at 14:00 UTC / 9 AM CDT, but nothing did:
Grep confirming no executor exists for
__openfang_schedules:Related Issues
/api/schedules/{id}/runhandler; a proper fix here should probably share that dispatch path).max_iterationssilently forces Continuous mode; three open architectural concerns remain, one of which (decouplingmax_iterationsfrom schedule mode) is directly load-bearing for a proper fix here.