Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,9 @@ Retry entry creation:

- Cancel any existing retry timer for the same issue.
- Store `attempt`, `identifier`, `error`, `due_at_ms`, and new timer handle.
- Keep the issue claimed while retrying so a queued handoff cannot be dispatched twice.
- If a claim lease exists, update it to `retrying` with retry due/backoff metadata and the last
seen worker/workspace details.

Backoff formula:

Expand All @@ -776,6 +779,23 @@ Note:
- Retry handling mainly operates on active candidates and releases claims when the issue is absent,
rather than performing terminal cleanup itself.

Claim lease behavior:

- When an issue is claimed for a worker, create a visible tracker comment marker headed
`## Symphony Claim Lease`.
- Persist these lease fields in runtime state and tracker-visible marker text:
`worker_id`, `worker_host`, `workspace_path`, `attempt`, `last_seen_at`, and
`lease_expires_at`.
- The default lease TTL is derived from polling cadence: at least 60 seconds and at least three poll
intervals.
- Active workers refresh the lease during poll reconciliation and when runtime/Codex activity is
observed.
- Retry and blocked transitions update the lease marker with `retrying` or `blocked` state and
relevant error/backoff details.
- Expired leases are recovered only when no live running or blocked worker exists for the issue.
Recovery logs the expiration, records an `expired` claim entry for observability, and requeues the
issue for retry handoff.

### 8.5 Active Run Reconciliation

Reconciliation runs every tick and has two parts.
Expand Down Expand Up @@ -1394,7 +1414,9 @@ Minimum endpoints:
"generated_at": "2026-02-24T20:15:30Z",
"counts": {
"running": 2,
"retrying": 1
"retrying": 1,
"blocked": 0,
"expired": 0
},
"running": [
{
Expand Down Expand Up @@ -1423,6 +1445,19 @@ Minimum endpoints:
"error": "no available orchestrator slots"
}
],
"claim_leases": [
{
"issue_id": "abc123",
"issue_identifier": "MT-649",
"state": "active",
"worker_id": "local:#PID<0.123.0>",
"workspace_path": "/tmp/symphony_workspaces/MT-649",
"attempt": 1,
"last_seen_at": "2026-02-24T20:14:59Z",
"lease_expires_at": "2026-02-24T20:16:30Z"
}
],
"expired": [],
"codex_totals": {
"input_tokens": 5000,
"output_tokens": 2400,
Expand Down
7 changes: 7 additions & 0 deletions elixir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ issue claimed and exposes it as blocked in the runtime state, JSON API, and dash
entries are in memory only; restarting the orchestrator clears that blocked map, so any still-active
Linear issue can become a dispatch candidate again after restart.

Claimed issues also get a Symphony claim lease marker through the tracker comment API. The lease
records the last-seen worker id, workspace path, attempt number, last heartbeat time, and expiry.
Active workers refresh the lease during poll and Codex activity; retry and blocked transitions
update the same lease state. If a non-live claim lease expires, Symphony logs the recovery and
requeues the issue without starting a duplicate worker for a still-running claim.

## How to use it

1. Make sure your codebase is set up to work well with agents: see
Expand Down Expand Up @@ -162,6 +168,7 @@ The observability UI now runs on a minimal Phoenix stack:

- LiveView for the dashboard at `/`
- JSON API for operational debugging under `/api/v1/*`
- Active, retrying, blocked, and expired claim lease visibility
- Bandit as the HTTP server
- Phoenix dependency static assets for the LiveView client bootstrap

Expand Down
Loading
Loading