agent-orchestrator/docs/architecture/adr-001-dual-stack-binding.md
Hibryda 579157f6da feat: Phase 2 — store audit, migration clusters, ADR, settings domain migration
- MIGRATION_CLUSTERS.md: reactive dependency graph across 20 bridges/stores
- PHASE1_STORE_AUDIT.md: 11 stores audited (3 clean, 5 bridge-dependent, 3 platform-specific)
- ADR-001: dual-stack binding strategy (accepted, S-1+S-3 hybrid)
- Settings domain migration: all 6 settings components + App.svelte + FilesTab
  migrated from settings-bridge to getBackend() calls
2026-03-22 03:48:41 +01:00

7.3 KiB

ADR-001: Dual-Stack Frontend-Backend Binding Strategy

Status: Accepted Date: 2026-03-22 Deciders: Human + Claude (tribunal consensus: Claude 72%, Codex 78%)

Context

Agent Orchestrator runs on two backends: Tauri 2.x (Rust, production) and Electrobun (Bun/TypeScript, experimental). Both share the same Svelte 5 frontend but communicate through different IPC mechanisms:

  • Tauri: @tauri-apps/api/core invoke() + listen()
  • Electrobun: RPC request/response + message listeners

The initial implementation used 23 bridge adapter files that directly import @tauri-apps/api. This created three problems:

  1. Duplicate code — Each backend reimplements the same IPC surface. Electrobun adapters must manually replicate every bridge function.
  2. Drift risk — When a Tauri bridge gains a new function, the Electrobun equivalent silently falls behind. No compile-time enforcement.
  3. 58 Codex findings — Three independent Codex audits identified duplicate SQL schemas, inconsistent naming (snake_case vs camelCase across the wire), missing error handling in Electrobun paths, and untested adapter code.

The project needed a binding strategy that:

  • Eliminates code duplication for shared frontend logic
  • Provides compile-time guarantees that both backends implement the same surface
  • Does not require a premature monolithic rewrite
  • Supports incremental migration of existing bridge adapters

Decision

Adopt the S-1 + S-3 hybrid strategy: a shared BackendAdapter interface (S-1) combined with scoped, audit-gated extraction (S-3).

Core Mechanism

A single BackendAdapter TypeScript interface defines every operation the frontend can request from the backend. Two concrete implementations (TauriAdapter, ElectrobunAdapter) are compile-time selected via path aliases. A singleton getBackend() function provides the active adapter.

Frontend stores and components call getBackend().someMethod() instead of importing platform-specific bridge files. The BackendCapabilities flags allow UI to gracefully degrade features unavailable on a given backend.

Package Structure

  • @agor/types — Shared type definitions (agent, project, btmsg, bttask, health, settings, protocol, backend, ids). No runtime code.
  • src/lib/backend/backend.ts — Singleton accessor (getBackend(), setBackend(), setBackendForTesting()).
  • src/lib/backend/TauriAdapter.ts — Tauri 2.x implementation.
  • src/lib/backend/ElectrobunAdapter.ts — Electrobun implementation.

Canonical SQL

A single schema/canonical.sql (29 tables) is the source of truth for both backends. A tools/validate-schema.ts extracts DDL metadata for comparison. A tools/migrate-db.ts handles one-way Tauri-to-Electrobun data migration with version fencing.

Phase 1 Scope (DONE)

Implemented 2026-03-22:

  • @agor/types package: 10 type files covering all cross-backend contracts
  • BackendAdapter interface with 15 methods + 5 event subscriptions
  • BackendCapabilities with 8 boolean flags
  • TauriAdapter implementing all methods via invoke()/listen()
  • ElectrobunAdapter implementing all methods via RPC
  • backend.ts singleton with test helpers
  • Canonical SQL DDL (29 tables, CHECK constraints, FK CASCADE, 13 indexes)
  • Schema validator and migration tool
  • pnpm-workspace.yaml workspace setup
  • docs/SWITCHING.md migration guide

Phase 2 Scope (In Progress)

Store audit and selective migration to @agor/stores:

  • Reactive dependency graph analysis across 13 stores and 23 bridges
  • Categorization: CLEAN (4 stores) / BRIDGE-DEPENDENT (9 stores) / PLATFORM-SPECIFIC (0 stores)
  • Settings domain migration: first cluster, replacing settings-bridge.ts with getBackend()
  • 8 migration clusters identified, ordered by dependency depth

Phase 2 Trigger Checklist (Frozen)

All items verified as implementable via BackendAdapter:

  • PTY session create/attach/detach/destroy
  • Agent dispatch with status tracking
  • Settings read/write persistence
  • Search index query
  • Provider credential management
  • Notification dispatch
  • Workspace CRUD

Phase 3 Direction (Documented, Not Committed)

agor-daemon: A standalone background process that both Tauri and Electrobun connect to, eliminating per-backend reimplementation entirely. This is documented as the long-term direction but explicitly NOT committed to. The hybrid adapter approach is sufficient for the current two-backend scope.

Turborepo threshold: Adopt Turborepo when package count reaches 3. Currently at 2 (@agor/types, main app). The third package would likely be @agor/stores (migrated pure-state stores).

Consequences

Enables

  • Compile-time guarantee that both backends implement the same API surface
  • Incremental migration — bridge files can be replaced one at a time
  • Capability-gated UI degradation (features disabled on backends that lack support)
  • Testing with mock backends (setBackendForTesting())
  • Future backend additions (GPUI, Dioxus) only need a new adapter class
  • Canonical SQL prevents schema drift between backends

Costs

  • Two adapter implementations must be maintained in parallel
  • BackendAdapter interface grows as new IPC commands are added
  • Bridge files coexist with BackendAdapter during transition (dual access paths)
  • One-way migration (Tauri to Electrobun) requires version fencing to prevent data loss

Risks

Identified by tribunal (Claude + Codex consensus) and three Codex audits:

Semantic Drift

Risk: Shared types enforce shape consistency but not behavioral consistency. Two adapters may handle edge cases differently (null vs undefined, error message format, timing). Mitigation: Integration tests per adapter. Type-narrowing at adapter boundary.

Svelte Rune Coupling

Risk: Stores using $state/$derived are tightly coupled to Svelte 5's rune execution model. Moving to @agor/stores package requires the consumer to also use Svelte 5. Mitigation: Only move stores that are pure state (no $derived chains that reference DOM). Keep reactive-heavy stores in app layer.

SQLite Pragma Differences

Risk: Tauri uses rusqlite (bundled SQLite, WAL mode, 5s busy_timeout). Electrobun uses better-sqlite3 (system SQLite, different default pragmas). Mitigation: Canonical SQL includes pragma requirements. Migration tool validates pragma state before copying data.

Build Toolchain Cache Invalidation

Risk: pnpm workspace changes can invalidate build caches unpredictably. @agor/types changes rebuild all consumers. Mitigation: Types package is stable (changes are additive). Turborepo deferred until package count warrants it.

Version Fencing

Risk: One-way migration (Tauri to Electrobun) means Electrobun DB is a snapshot. User switching back to Tauri loses Electrobun-only changes. Mitigation: tools/migrate-db.ts writes a version fence marker. Tauri startup checks for fence and warns if Electrobun has newer data.

Plugin Bridge Orphaning

Risk: Not all bridges map cleanly to BackendAdapter (btmsg, bttask, audit, plugins, anchors, remote). These "orphan bridges" may never migrate. Mitigation: Documented as acceptable. BackendAdapter covers the critical path (settings, groups, agents, PTY, files). Domain-specific bridges can remain as adapters that internally use BackendAdapter or direct IPC.