Files
didactyl/plans/fix_cron_triggers_not_firing.md

7.0 KiB

Fix: Cron Triggers Not Firing

Root Cause Analysis

After tracing the full cron trigger lifecycle in src/trigger_manager.c, I identified three bugs and one usability gap that together explain why cron triggers never fire.

Bug 1: last_poll_at initialized to time(NULL) — first poll always skipped

In trigger_manager_init():

mgr->last_poll_at = time(NULL);

Then in trigger_manager_poll():

if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 30) {
    return 0;  // skip
}

This means the first 30 seconds after init, all cron polls are silently skipped. This is a minor delay, not the primary cause, but it compounds with Bug 2.

Fix: Initialize last_poll_at = 0 so the first poll runs immediately.

Bug 2: Cron triggers loaded AFTER init — poll window already consumed

The startup sequence in src/main.c is:

  1. trigger_manager_init() — sets last_poll_at = time(NULL)
  2. trigger_manager_load_from_startup_events() — loads cron triggers
  3. trigger_manager_load_from_skills() — loads more cron triggers (after EOSE, async)
  4. Main loop starts calling trigger_manager_poll()

Because last_poll_at is set at init time, and loading happens after init, the first poll after loading may still be within the 30-second window. Combined with Bug 1, this means the first cron evaluation is delayed.

This is not the primary cause either — after 30 seconds, polls should start working. But it contributes to the perception that cron is broken.

Bug 3 (PRIMARY): Invalid cron expressions — * instead of * * * * *

The agent's diagnosis shows:

  • infrastructure-monitor has cron filter * — a single asterisk
  • c-relay-memory-watch has cron filter 0 21,22,23,0,1,2 * * * — valid 5-field

The cron_matches_now() function strictly requires exactly 5 whitespace-separated fields:

if (nf != 5 || tok != NULL) {
    return 0;  // silently rejects
}

A single * produces nf == 1, which fails the nf != 5 check. This is the primary reason infrastructure-monitor never fires.

For c-relay-memory-watch with 0 21,22,23,0,1,2 * * * — this is a valid 5-field expression. The hour field 21,22,23,0,1,2 means hours 21-23 and 0-2 UTC. If the agent was tested outside those hours, it would correctly not fire. However, the agent reports last_fired: 0 which means it has never fired, suggesting either:

  • The agent hasn't been running during those hours, OR
  • There's a secondary issue with how the expression was stored

Usability Gap: No cron shorthand support

Standard cron implementations support shorthands like @hourly, @daily, @every_5m. Didactyl only supports raw 5-field expressions. The LLM creating skills may generate * thinking it means "every minute" when it should be * * * * *.

Usability Gap: No validation or error logging on invalid cron expressions

When cron_matches_now() rejects an expression, it returns 0 silently. There is no log message indicating the expression was invalid. This makes debugging impossible without reading the source code.

Similarly, trigger_manager_add() copies the filter to cron_expr without validating it:

if (t->trigger_type == TRIGGER_TYPE_CRON) {
    snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", filter_json);
}

No validation that filter_json is a valid 5-field cron expression.


Fix Plan

Fix 1: Initialize last_poll_at to 0

File: src/trigger_manager.c

Change:

mgr->last_poll_at = time(NULL);

To:

mgr->last_poll_at = 0;

This allows the first poll to run immediately after triggers are loaded.

Fix 2: Add cron expression validation in trigger_manager_add and trigger_manager_update

File: src/trigger_manager.c

Add a static validation function:

static int cron_expr_valid(const char* expr) {
    if (!expr || expr[0] == '\0') return 0;
    char buf[128];
    snprintf(buf, sizeof(buf), "%s", expr);
    int nf = 0;
    char* saveptr = NULL;
    char* tok = strtok_r(buf, " \t", &saveptr);
    while (tok && nf < 6) { nf++; tok = strtok_r(NULL, " \t", &saveptr); }
    return (nf == 5 && tok == NULL) ? 1 : 0;
}

Call it in trigger_manager_add() and trigger_manager_update() when trigger_type == TRIGGER_TYPE_CRON, logging a warning if invalid.

Fix 3: Add cron shorthand expansion

File: src/trigger_manager.c

Add a helper that expands common shorthands before parsing:

Shorthand Expansion
* * * * * *
@yearly / @annually 0 0 1 1 *
@monthly 0 0 1 * *
@weekly 0 0 * * 0
@daily / @midnight 0 0 * * *
@hourly 0 * * * *

This directly fixes the infrastructure-monitor case where * should mean "every minute".

Fix 4: Add debug logging for cron evaluation

File: src/trigger_manager.c

Add DEBUG_INFO or DEBUG_WARN logging in trigger_manager_poll() when:

  • A cron expression fails to parse — log the expression and skill d_tag
  • A cron expression matches — log the match before firing
  • The poll is skipped due to the 30-second throttle — log at DEBUG level

Fix 5: Add last_cron_fire and cron_expr to status JSON

File: src/trigger_manager.c

In trigger_manager_status_json(), add the cron-specific fields so the agent can self-diagnose:

if (t->trigger_type == TRIGGER_TYPE_CRON) {
    cJSON_AddStringToObject(item, "cron_expr", t->cron_expr);
    cJSON_AddNumberToObject(item, "last_cron_fire", (double)t->last_cron_fire);
}

Flow After Fix

flowchart TD
    INIT[trigger_manager_init - last_poll_at=0] --> LOAD[Load triggers from startup/skills]
    LOAD --> POLL[trigger_manager_poll called from main loop]
    POLL --> CHECK{now - last_poll_at >= 30?}
    CHECK -->|No| SKIP[Return 0 - throttled]
    CHECK -->|Yes| ITER[Iterate triggers]
    ITER --> CRON{trigger_type == CRON?}
    CRON -->|No| NEXT[Next trigger]
    CRON -->|Yes| EXPAND[Expand shorthand if needed]
    EXPAND --> VALID{Valid 5-field expr?}
    VALID -->|No| WARN[Log warning + skip]
    VALID -->|Yes| MATCH{cron_matches_now?}
    MATCH -->|No| NEXT
    MATCH -->|Yes| DEDUP{last_cron_fire < 50s ago?}
    DEDUP -->|Yes| NEXT
    DEDUP -->|No| FIRE[execute_llm_action]
    FIRE --> NEXT
    WARN --> NEXT

Files Changed

File Change
src/trigger_manager.c Fix last_poll_at init to 0
src/trigger_manager.c Add cron_expand_shorthand() helper
src/trigger_manager.c Add cron_expr_valid() validation + warning log
src/trigger_manager.c Add debug logging in poll loop
src/trigger_manager.c Add cron fields to status JSON