# Architecture
## 1. Objective
Implement the Symphony orchestration model in Rust while using OpenHands as the
execution substrate and FrankenTUI as an optional operator client.
The system must preserve these boundaries:
- the orchestrator is the source of truth for scheduling state
- the tracker is polled and reconciled by the orchestrator
- each issue executes in its own workspace
- `WORKFLOW.md` remains the repo-owned policy and prompt contract
- UI is optional and must not affect correctness
## 2. Layered design
OpenSymphony is split into five layers:
1. Policy layer
- `WORKFLOW.md`
- target-repo `AGENTS.md`
- target-repo `.agents/skills/`
2. Configuration layer
- typed workflow/config loader
- env and path resolution
- OpenHands extension config
3. Coordination layer
- orchestrator actor
- retry queue
- reconciliation
- runtime snapshot store
4. Execution layer
- workspace manager
- OpenHands REST client
- OpenHands WebSocket runtime stream
- local Codex app-server stdio adapter
- issue session runner
5. Observability layer
- structured logs
- control-plane API
- FrankenTUI
Packaging distinction:
- modularity is preserved through explicit internal subsystem boundaries
- packaging is intentionally flat: crates.io publishes only `opensymphony`
- the `crates/opensymphony-*` directories are internal module trees compiled
into that one package
## 3. Main decisions
### 3.1 Rust owns orchestration
Rust owns:
- poll cadence
- issue eligibility
- bounded concurrency
- retry scheduling
- stall detection
- startup cleanup
- restart recovery
- operator snapshots
OpenHands conversation state is informative, not authoritative.
### 3.2 OpenHands is the execution adapter
OpenHands provides:
- per-conversation workspace configuration
- persistent conversations
- background run triggering
- searchable event history
- real-time updates over WebSocket
- provider/model flexibility
OpenSymphony does not reimplement an agent loop.
### 3.3 WebSocket-first, not WebSocket-only
REST is still required for:
- conversation creation
- sending messages
- triggering runs
- initial sync
- reconnect reconciliation
- restart recovery
### 3.4 One local server, many workspaces
The local supervised topology runs one OpenHands server for the daemon while
passing a distinct `working_dir` per issue.
### 3.5 One conversation per issue by default
OpenSymphony persists a stable `conversation_id` per issue inside the issue
workspace and reuses it across retries and daemon restarts unless the workflow
reuse policy says otherwise.
### 3.6 GraphQL-only Linear writes
OpenSymphony 1.0.0 removed the old bridge layer for agent-side Linear writes.
The supported model is now:
- orchestrator reads Linear through the internal `opensymphony_linear` module
- initialized target repos read and write Linear through the checked-in
GraphQL helper assets under `.agents/skills/linear/`
This keeps one canonical Linear API surface for the agent path.
## 4. Component model
### Internal subsystem modules
- `opensymphony_domain`
- domain models and scheduler transitions
- `opensymphony_workflow`
- workflow loading, config resolution, prompt rendering, and alpha harness/model
selection
- `opensymphony_workspace`
- workspace management and manifests
- `opensymphony_linear`
- Linear GraphQL read adapter and guarded archive mutation
- `opensymphony_memory`
- issue capsules, DuckDB memory index, docs sync, and archive eligibility
- `opensymphony_code_intel`
- built-in Tree-sitter parser provider skeletons, starting with Rust source
summaries, one-based spans, symbols, and recoverable AST diagnostics
- `opensymphony_openhands`
- OpenHands transport and session runner
- `opensymphony_codex`
- local Codex app-server stdio adapter, JSON-RPC lifecycle requests, event
normalization, installed-schema validation, credential reuse, and benchmark
helpers for experimental transports
- `opensymphony_orchestrator`
- scheduler loop, route decisions, and reconciliation
- `opensymphony_control`
- control-plane snapshot store and compatibility API
- `opensymphony_gateway`
- operator gateway API, dashboard snapshots, Linear-backed task graph reads,
run detail/file/diff endpoints, event journal, and web assets
- `opensymphony_cli`
- user-facing entrypoints
- `opensymphony_tui`
- terminal operator UI
- `opensymphony_testkit`
- fakes and contract fixtures
### Target-repo Linear assets
Initialized repositories receive a checked-in Linear skill tree:
- `SKILL.md`
- `scripts/linear_graphql.py`
- `queries/*.graphql`
- `references/*.md`
Those assets are part of the supported public interface of `opensymphony init`.
They include canonical query files for issue create/update flows, comments,
relations, attachments, project content/status updates, and introspection.
## 5. Process model
Local MVP process graph:
```text
opensymphony run
├─ orchestrator
├─ workspace manager
├─ linear adapter
├─ openhands REST client
├─ openhands WebSocket client
├─ optional Codex app-server stdio worker
├─ gateway API
├─ control-plane compatibility API
└─ local server supervisor
└─ python -m openhands.agent_server
```
The scheduler attaches a `HarnessRouteDecision` to each worker start request.
The default route remains `openhands_agent_server`. Workflow `routing.harness`
or the `OPENSYMPHONY_HARNESS` environment override can select the local
`codex_app_server` route when that harness is available and can start runs.
Route decisions are emitted as `routing.decision` runtime audit events so dry-run
previews and real dispatches show the selected harness, model, and model
profile.
Other processes:
- `opensymphony debug <issue-id>`
- `opensymphony tui`
- target-repo hooks started by the workspace manager
- OpenHands-managed tool execution inside the agent runtime
There is no separate agent-side Linear bridge process in 1.0.0.
## 5.1 Gateway and rich clients
The web and desktop clients consume the gateway contract rather than reaching
into orchestrator internals. Dashboard and run state come from the
control-plane snapshot, while the task graph read endpoint asks the
orchestrator-side Linear adapter for tracker hierarchy and dependency
relationships, then overlays live runtime details from the latest snapshot.
Runtime token usage in that snapshot carries input, output, cache-read, and
provider-reported total counters when the selected harness reports them; legacy
metadata without an explicit total falls back to input plus output.
Run Detail metadata also carries tracker-backed branch and PR fields when
Linear provides them: `branchName` becomes `branch_name`, and explicit GitHub PR
attachments become `pr_url`.
The gateway emits `root_ids` from the returned Linear parent/child graph so
clients can render the same forest without inventing hierarchy locally.
If the optional task graph reader cannot be built, `opensymphony run` still
starts the gateway and the task graph endpoint returns `503`; this does not
weaken the scheduler's separate Linear tracker requirement.
Native desktop builds may call the same operations through Tauri IPC instead
of loopback HTTP, but the data contract is identical. Tauri command arguments
use the Rust command parameter names exactly, including snake_case keys such as
`run_id`, `project_id`, `page_token`, `page_size`, and `file_path`. If a native
desktop read command fails, the desktop adapter may retry through the loopback
HTTP transport for the same gateway operation.
Run-event `page_token` values are gateway-generated sequence tokens encoded as
strings; malformed tokens are rejected with `400 Bad Request` instead of being
silently treated as the first page.
## 6. Failure boundaries
- scheduler correctness must not depend on tracker comments or transitions
- GraphQL write failures in the target repo do not corrupt orchestrator state
- a missing `LINEAR_API_KEY` blocks Linear operations but should fail clearly
- UI failures must not affect daemon execution
## 7. Interrupt diagnostics
Interrupt requests are recorded in orchestrator-owned issue execution state
before any harness-specific protocol call is attempted. The shared command
captures the run id, Linear issue id, harness kind, conversation or thread id,
optional turn id, reason, and expected next state. Current reasons are
`operator_cancel` and `tracker_merging_supersedes_human_review`.
The command is idempotent for the active run: repeated operator clicks or
tracker observations return the existing command instead of enqueueing another
harness interrupt. Harness adapters later translate that command to their
native protocol, but scheduler state does not depend on desktop-local state or
adapter-private DTOs.
`opensymphony run` consumes accepted gateway `cancel` actions from the gateway
event journal and forwards them into the scheduler-owned `operator_cancel`
interrupt path. The gateway validates and records the operator intent, but it
does not mutate scheduling state directly.
Run Detail diagnostics surface the orchestrator-owned cancel state as
requested, acknowledged, failed, timed out, and reason fields. Terminal cancel
states are sticky: late acknowledgements, failures, or timeouts do not overwrite
an already terminal interrupt status. Non-cancel worker outcomes do not infer a
harness interrupt acknowledgement or timeout; adapters must still report the
actual acknowledgement, failure, or timeout path.
## 8. Migration boundary
OpenSymphony 1.0.0 is the compatibility boundary for the GraphQL-only Linear
rewrite and the provider-agnostic AI review configuration changes.
Notable removals:
- workflow-owned `openhands.mcp`
- the old bridge CLI command
- provider-specific AI review secret naming
## Current model
- COE-530 contributed: PR #193: docs(installer): document desktop installer validation (merge `7588cc7`)
## Important invariants
- Preserve the behavior described in the recent captured changes unless current code and tests show it has changed.
- Use capsule source refs to inspect the original PR or Linear issue when context is ambiguous.
## Operational flow
- No generated diagram requested for this sync.
## Known gotchas
- No area-specific gotchas were inferred from the selected memory.
## Recent changes
- COE-530: Installer Docs And End-To-End Validation
## Source refs
- COE-530