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>
46 lines
1.6 KiB
JSON
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" }
|
|
}
|
|
}
|
|
}
|
|
}
|