fix(symphony): correct runtime model — long-lived session, with multi-session support

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>
This commit is contained in:
movq
2026-04-29 21:51:23 -05:00
parent 92f005895c
commit ee4e0b236e
5 changed files with 112 additions and 27 deletions

View File

@@ -34,16 +34,31 @@ in your summary; the orchestrator will not retry until the project documents it.
Bash with `timeout_ms`. Non-zero exit → abort the attempt with a `before_run_failed`
error. Do NOT proceed to the agent work.
2. **Do the work.** Follow the rendered prompt. The workflow author is responsible for
2. **Start heartbeat.** Write `<repo-root>/.symphony/heartbeats/<issue-id>.json` with:
```
{ "task_id": "...", "session_id": "...", "started_at": "...", "status": "running" }
```
Update the file's `timestamp` periodically — at least once per minute (the orchestrator's
`agent.heartbeat_interval_ms`, default 60000). The simplest implementation is a
`touch`-like rewrite at natural pause points (after a hook completes, after a test pass,
after a tracker write). The heartbeat is what other Symphony sessions use to know
you're alive when they can't see your task in their own `TaskList`.
3. **Do the work.** Follow the rendered prompt. The workflow author is responsible for
telling you to do things like create a branch, run tests, push, open a PR, comment on
the ticket, transition state. Don't infer those steps; if the prompt doesn't say to,
don't.
3. **Bounded turns.** Track your own progress. If `agent.max_turns` was provided in the
4. **Bounded turns.** Track your own progress. If `agent.max_turns` was provided in the
issue payload, treat that as a soft budget — at the limit, stop and hand off with a
summary even if work is incomplete. The orchestrator will retry or release.
4. **`after_run` hook.** Run `hooks.after_run` if present. Failure is logged, not fatal.
5. **`after_run` hook.** Run `hooks.after_run` if present. Failure is logged, not fatal.
6. **Stop heartbeat.** On graceful exit (regardless of status), delete the heartbeat
file. Stale heartbeats are how the orchestrator detects abandonment, so leave a clean
trail behind you. If you crash hard, the file stays and the next reconciliation will
correctly reclaim the issue.
## Output contract