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

@@ -37,6 +37,11 @@ agent:
max_retry_backoff_ms: 600000
max_concurrent_agents_by_state:
"in progress": 2
# Worker heartbeat cadence — affects how quickly other Symphony sessions
# (Discord triggers, webhooks, separate user sessions) detect abandoned
# workers. Defaults are safe; tighten if you have many concurrent sessions.
heartbeat_interval_ms: 60000 # workers refresh the heartbeat at most this often
heartbeat_stale_ms: 300000 # reconciler treats heartbeats older than this as abandoned
---
You are working on issue **{{ issue.identifier }}** — {{ issue.title }}.

View File

@@ -10,9 +10,10 @@
"type": "object",
"required": ["task_id", "worktree", "started_at"],
"properties": {
"task_id": { "type": "string" },
"worktree": { "type": "string" },
"started_at": { "type": "string", "format": "date-time" },
"task_id": { "type": "string" },
"session_id": { "type": ["string", "null"], "description": "Identifier for the orchestrator session that spawned this task. Other sessions reconcile via the heartbeat file when this session's TaskList does not include the task_id." },
"worktree": { "type": "string" },
"started_at": { "type": "string", "format": "date-time" },
"issue_identifier": { "type": "string" }
}
}