Skip to content

schedule_create tool registers schedules that never fire — two disconnected scheduler systems #1069

@drzow

Description

@drzow

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.

  1. 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.

  2. 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

  1. 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).
  2. Let the Hand boot and execute its first autonomous tick. It will call schedule_create with something like "daily at 9am".
  3. 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.
  4. Verify the kernel-cron file is untouched:
    cat ~/.openfang/cron_jobs.json
    # []
    
  5. Wait for the scheduled time. The Hand will not fire.
  6. 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."
  7. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions