Skip to content

Harden event-driven owners: atomic dispatch guard + focus affordance + watcher bounds #78

Description

@anshulsao

Context

Event-driven owners shipped in #71 as a documented pattern + a manual-tick overlap guard: an owner is a cheap poller by default, and for a bounded reactive window it dispatches a watcher task (holding a Monitor) that, on the event, writes a journal focus note and fires flow owner tick <slug> --auto (a focused tick), then exits. The basic capability works. This issue tracks the residual hardening — the pattern is currently pure skill composition with best-effort guarantees.

1. Make the tick-dispatch overlap guard ATOMIC (most important)

Both the scheduler guard (ownerTickDue) and the manual --auto guard (ownerTickManual) are check-then-act with no transaction:

if o.TickPID.Valid && processAlive(...) && !ownerTickStale(o) { continue/refuse }
// ... later ...
launch + recordOwnerTick{Started,Dispatched}   // sets tick_pid

Two dispatchers racing within a few ms can both pass the check and both launch — and the skill (§4.17 "Event-driven owners") now documents "won't double-fire." Event triggers make near-simultaneous dispatch (a watcher's owner tick --auto racing a scheduled tick) realistic, so this gap matters more than before.

Fix: a compare-and-swap claim instead of check-then-act — e.g.
UPDATE owners SET tick_pid=?, tick_started=? WHERE slug=? AND (tick_pid IS NULL OR <stale>)
and only proceed to launch if RowsAffected==1. That makes the doc's claim actually true and removes the residual TOCTOU in both the scheduler and the manual path.

2. First-class focus passing — flow owner tick --with "<focus>"

Today a watcher must journal-note-then-trigger (two steps) to give the focused tick context. A --with injection that forwards the event context straight into the triggered tick's prompt would be cleaner and less racy (no "did the note land before the tick read step 3?" ordering concern).

3. Watcher scaffolding + enforced bounds

The watcher is hand-authored skill composition (a task brief that says "use the Monitor tool with a stop condition + timeout"). The skill warns a watcher must be bounded (it's a living, token-costing session) and never permanent — but nothing enforces it. Consider a helper that scaffolds the bounded watcher task and enforces a timeout, so a never-fired event can't strand a watcher running indefinitely.

4. Ties to #75 (tick inspectability)

Event-triggered focused ticks especially want a transcript / "why did this fire?" trail. #75 (capture a per-tick session id) would make event-driven owners debuggable — worth doing together.

Also-adjacent low-severity robustness nits (from the Fable 5 review of #71)

While in this code:

  • clearOwnerTickBookkeeping clears with WHERE slug=? only — could mirror the reconcile pattern (AND tick_pid=?) to avoid a theoretical ABA wipe of a freshly-dispatched pid.
  • ownerTickStale returns false on an unparseable tick_started (re-opens the trust-pid-forever hole); safer fallback is true. Both unreachable without manual DB edits today.

Relevant code

  • internal/app/owner_tick.goownerTickDue (scheduler guard), ownerTickManual (--auto guard), recordOwnerTickStarted/Dispatched, ownerTickStale, clearOwnerTickBookkeeping
  • internal/app/skill/SKILL.md §4.17 "Event-driven owners"

Context: event-driven owners landed in #71; surfaced reviewing that PR.

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