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.go — ownerTickDue (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.
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--autoguard (ownerTickManual) are check-then-act with no transaction: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 --autoracing 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
--withinjection 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:
clearOwnerTickBookkeepingclears withWHERE slug=?only — could mirror the reconcile pattern (AND tick_pid=?) to avoid a theoretical ABA wipe of a freshly-dispatched pid.ownerTickStalereturnsfalseon an unparseabletick_started(re-opens the trust-pid-forever hole); safer fallback istrue. Both unreachable without manual DB edits today.Relevant code
internal/app/owner_tick.go—ownerTickDue(scheduler guard),ownerTickManual(--autoguard),recordOwnerTickStarted/Dispatched,ownerTickStale,clearOwnerTickBookkeepinginternal/app/skill/SKILL.md§4.17 "Event-driven owners"Context: event-driven owners landed in #71; surfaced reviewing that PR.