Earlier framing assumed each /schedule-fired tick was a cold start
needing on-disk reconciliation. The actual model is a long-lived
orchestrator session where /schedule fires ticks within the running
session — TaskList sees prior-tick tasks just fine. Reframed
accordingly, while leaving the door open to other trigger sources
(Discord channel, webhook, second user session) which DO need
cross-session reconciliation.
Reconciliation logic now has a fast path and a slow path:
- Fast path: task in our TaskList → it's ours → reconcile via TaskGet.
- Slow path: task NOT in our TaskList (another session, or our session
restarted) → check the worker's heartbeat file. Fresh → another
session owns it, leave alone. Stale or missing → consider abandoned,
retry; do not call TaskStop on a task we don't own.
Workers now write .symphony/heartbeats/<issue-id>.json on start,
refresh it at heartbeat_interval_ms cadence (default 60s), and delete
it on graceful exit. Stale heartbeats are how abandonment is detected
across sessions and after crashes.
Also added session_id to state.json running entries so cross-session
reconcilers know who spawned what.
WORKFLOW.md gains heartbeat_interval_ms and heartbeat_stale_ms knobs
under agent: with documented defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>