Files
claude-plugins/symphony/templates/state.schema.json
movq ee4e0b236e 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>
2026-04-29 21:51:23 -05:00

46 lines
1.6 KiB
JSON

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Symphony orchestrator state",
"type": "object",
"required": ["running", "retry_attempts", "claimed", "completed"],
"properties": {
"running": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["task_id", "worktree", "started_at"],
"properties": {
"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" }
}
}
},
"retry_attempts": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["attempt", "due_at_ms"],
"properties": {
"attempt": { "type": "integer", "minimum": 1 },
"due_at_ms": { "type": "integer" },
"last_error": { "type": ["string", "null"] }
}
}
},
"claimed": { "type": "array", "items": { "type": "string" } },
"completed": { "type": "array", "items": { "type": "string" } },
"totals": {
"type": "object",
"properties": {
"ticks": { "type": "integer" },
"dispatched": { "type": "integer" },
"completed": { "type": "integer" },
"failed": { "type": "integer" }
}
}
}
}