One host, many agents, one journal.
LFG runs beside your AI coding agents, discovers their processes, normalizes their transcripts, and serves a single cursor-resumable event stream to native Apple clients. The browser product is gone. What remains is a smaller system with two files still carrying far more than their share.
Use the Current / Both / Target control in the header to switch every comparison on this page between what ships today and the proposed boundary refactor. It is a refactor, not a rewrite: route behavior, journal semantics, transcript parsers and Swift reducers all stay characterized while composition moves into smaller units.
What the deprecation already bought
Measured from the working tree against its parent commit. These are deletions that have landed, not projections.
Still carrying too much
| File | Lines | Owns |
|---|---|---|
| ios/LFG/SessionStore.swift | 2,615 | Network, hosts, GRDB, outbox, projection, commands |
| src/sessions.ts | 1,721 | Discovery, identity, transcripts, pagination, projection |
| desktop/LFGSessions.swift | 1,766 | API, host state, iTerm automation, all SwiftUI |
| src/commands/serve.ts | 1,408 | Startup, routing, validation, orchestration |
| src/tmux.ts | 997 | Process inspection, pane I/O, prompt parsing |
| ios/LFG/SessionListView.swift | 1,054 | Grouping, search, rows, headers, toolbars |
The one-line thesis
Removing the browser PTY path eliminated the strongest argument for a big-bang backend migration. The remaining problem is not the runtime — it is that six files hold most of the system’s decisions.
Who talks to what
The control API is unauthenticated and can launch shell-capable agents. It binds to loopback and is reachable only over Tailscale. Every box below sits inside that high-trust boundary.
Current — native clients, direct runtimes
Target — same boundary, explicit seams
/api/file is read-only and
traversal-hardened by realpath containment against a fixed root set
(repos root, self repo, home, $TMPDIR/lfg-uploads), but it lives inside
the same high-trust zone. No component may assume this API is safe on a public network.
Four surviving surfaces
lfg serve
The control API plus background pumps: journal, send queue, APNs watcher. One Bun process, one event loop.
LFG for iOS / iPadOS
Monitoring and control. Offline-durable GRDB cache, cursored SSE, durable outbox, push and Live Activity.
LFG desktop for macOS
Lightweight cross-host launcher into iTerm2. No local database, no stream, no push, no editing.
CLI integrations
Markdown-defined insight agents and a WhatsApp sidecar. Both candidates for a Phase 0 product decision.
The Bun service, by responsibility
Colocated *.test.ts files characterize state projection, transcript normalization, session binding, leases, event pumping, send delivery, tmux parsing, APNs and push transitions.
| Area | Modules | Responsibility | Status |
|---|---|---|---|
| Entry & config | cli.ts · config.ts · commands/setup.ts | Command dispatch, environment paths and defaults, setup wrapper | Stable |
| HTTP composition | commands/serve.ts 1,408 | Starts pumps and the APNs watcher lazily; validates and routes every REST and SSE request; maps API operations to domain functions | Split |
| Session projection | sessions.ts 1,721 · session-state.ts · turn-state.ts · hook-state.ts · activity.ts | Discovers direct Claude and Codex processes and transcripts; normalizes message formats; resolves live/resumable state, prompt/activity/status, fork lineage, titles, pagination | Split |
| Process adapter | tmux.ts 997 · procinfo.ts · closing.ts | Cross-platform process inspection; tmux spawn/attach/input/interrupt/close; prompt and busy-chrome parsing; close tombstones | Async |
| Ownership | managed.ts · leases.ts | Records LFG-managed panes and lineage; prevents two synced hosts owning one transcript | Stable |
| Durable delivery | sendq.ts 831 · sendq-store.ts | Idempotent client-id sends, SQLite state, background insertion and confirmation, queue actions, retry and reconciliation | Stable |
| Durable events | journal.ts · journal-pump.ts · transcript.ts | SQLite event log, one global transcript/pane/queue pump, cursor replay and retention, bounded file scanning | Stable |
| User & directory metadata | users.ts · dirs.ts · hostinfo.ts | Owner tags, repository and inbox creation and trust, stable host identity | Unify I/O |
| Push | push/apns.ts · store.ts · watcher.ts 609 · liveactivity*.ts · fleet-active-store.ts | APNs credentials, JWT and transport; device and token stores; transition detection; notification payloads; fleet Live Activity lifecycle | Stable |
| Insight agents | commands/agents.ts · agents/registry.ts · runner.ts · collectors/* | Loads Markdown agent definitions, collects git/GitHub/security/files/model data, runs reports through claude -p, writes report and action continuity files |
Optional |
| Optional messaging | commands/whatsapp.ts 543 | WhatsApp authentication, group routing, session listing and control | Optional |
claude and codex now share the same managed-tmux lifecycle.
The alternate harness registry, command files, provider/MCP packages, OpenCode binary,
and native model-picker branches are gone. WhatsApp remains but routes into these same
direct sessions; insight reports always use the installed Claude CLI.
iOS, iPadOS and macOS layers
iOS / iPadOS app target
- App & lifecycle — LFGApp · RootView · SettingsView. Scene entry, host setup, adaptive navigation, foreground/background and deep-link lifecycle.
- Application state — SessionStore (2,615) · HostLink · BackgroundSender. Multi-host orchestration, REST snapshots plus journal reduction, commands, optimistic state and outbox, stream health, background-safe sends.
- Screens — SessionListView (1,054) · SessionDetailView · NewSessionView · NewSession/*. Group, filter, search; transcript and control surface; create, resume and fork flows.
- Presentation — Components · MessageComposer · RichContent · Theme · UnreadBadges. Reusable UI, attachments, Markdown/tool/media rendering, read affordances.
- Notifications — PushManager · LiveActivityManager · FleetActivityController · RetiredSessionActivity. APNs lifecycle and navigation, push-to-start tokens, fleet activity projection.
LFGCore — the shared, simulator-free core
- Contract & transport — Models · LFGClient · SSEParser · HostEvents. Codable contract, REST/SSE client, lenient decoding.
- Identity & health — MultiHost · HostConfig · HostState · HostHealth. Multi-host identity and one host-health state machine.
- Persistence — LFGStore · LFGStoreRecords. GRDB store and records; the durable local authority.
- Reducers — OptimisticSendReconciliation · ReadState · RecentDirs. Transcript and display reduction, optimistic-send reconciliation, read/unread.
- Activity — Push · FleetActivitySnapshot · LFGFleetAttributes. Push and ActivityKit data shared with the widget target.
LFGCore/Tests covers these seams without a simulator. ios/project.yml is the XcodeGen source of truth; ios/LFGWidgets supplies the ActivityKit widget.
macOS launcher & operations
Storage and ownership
Agent CLIs own transcript truth. LFG projects it. SQLite owns durable server state and in-memory caches are only accelerators.
| Store | Owner | Contents & behavior |
|---|---|---|
| ~/.claude/projects | Claude Code | JSONL conversation history, session ids, cwd and tool records; syncable across hosts |
| ~/.codex/sessions | Codex | Rollout JSONL and thread metadata |
| Agent hook state | Agent hooks | Strong running/idle/ended signal when present |
| ~/.lfg/journal.db | Bun service | Journal sequence and events plus send-queue rows; survives process restart |
| ~/.lfg/managed.json | Bun service | tmux name, cwd, agent kind, creation time, parent/fork lineage |
| ~/.lfg/leases + transcript-adjacent | Bun service | Host/process/start identity and expiry; a synced lease prevents two machines claiming one conversation |
| ~/.lfg/session-users.json, titles, host id | Bun service | Owner tags, title overrides, stable host identity |
| ~/.lfg/push-* | Bun service | APNs devices, Live Activity tokens, active fleet activity snapshot |
| ~/.lfg/reports, ~/.lfg/agents | Insight-agent CLI | Generated reports, action sidecars and runlogs, local agent definitions |
| iOS application database | LFGStore / GRDB | Host, session, message and queue snapshots; cursors; read state; durable outbound work |
| iOS preferences | AppSettings | Configured hosts, owner/filter/group and presentation preferences |
| ~/.config/lfg-desktop/hosts.json | macOS app | Host URL list, initially localhost |
- Agent CLIs own transcript truth; LFG only projects it into a normalized session API.
- SQLite owns durable server event and delivery state; in-memory caches are accelerators.
- The iOS database owns locally renderable state and cursors; network state is reconciled into it, never treated as durable by itself.
- A fresh lease identifies the one host allowed to present or control a synced transcript as live. Resume and fork return conflicts when another host owns it.
needsInput, blocked,
working, idle — is a projection of hook, transcript, pane
and process facts. It is never an independently mutable flag.
“Closed / resumable” means a transcript exists without a locally owned
live process; a fresh foreign lease can redirect or reject work.
The native client HTTP/SSE API
One Bun server, JSON unless noted. This is the API the iOS and macOS clients consume — it is not the removed browser product, and none of its routes served a web bundle.
Host, events and environment
| GET /api/info | Stable hostId and display hostname |
| GET /api/ping | Liveness, journal head, timestamp; also keeps carrier-NAT mappings warm |
| GET /api/events?since= | Host-wide cursor-resumable SSE |
| GET /api/events/page | Bounded journal page for background wake |
| GET /api/users | Configured owner roster and avatar URLs |
| GET /api/repos · /api/dirs | Available working directories |
| POST /api/dirs/new · /inbox | Create a trusted directory · set inbox |
| GET /api/file?path= | Scoped agent-produced file read with byte ranges |
| GET /api/claude/usage | Cached Claude OAuth usage, when credentials exist |
Sessions and lifecycle
| GET /api/sessions | Current normalized live session snapshot |
| GET /api/sessions/resumable | Paginated closed transcript catalog |
| POST /api/sessions/new | Start a runtime and bind its session id |
| POST /api/sessions/resume | Resume a closed transcript |
| POST /api/sessions/fork | Fork a Claude transcript |
| GET /api/sessions/:id/messages | Bounded, backward or full transcript page |
| POST /api/sessions/:id/send | Idempotent queued, immediate or wake-up send |
| POST /api/sessions/:id/upload | Store attachment, return host path |
| POST /api/sessions/:id/model | Change or recover Claude model |
| PUT /api/sessions/:id/title | Persist title override |
| POST /api/sessions/:id/user | Assign or clear configured owner |
Control and queue
| POST /api/sessions/:id/answer | Resolve a pending pane prompt by index |
| POST /api/sessions/:id/dismiss | Send Escape to the pending prompt |
| POST /api/sessions/:id/interrupt | Stop the current turn, preserve queued steering |
| POST /api/sessions/:id/close | End the live process, release the lease |
| GET|DELETE /api/sessions/:id/queue | Reconcile and list · clear resolved rows |
| POST …/queue/:mid/retry | Retry a failed row |
| DELETE …/queue/:mid | Remove an undelivered row |
| POST …/queue/:mid/send-now | Interrupt and prioritize a row |
Push
| POST /api/push/register | Register an APNs device token |
| POST /api/push/unregister | Remove a device token |
| GET /api/push/health | Push configuration and device count |
| POST …/live-activity/start-token | Register a push-to-start token |
| POST …/live-activity/update-token | Register or update an activity token |
21 static path literals plus 16 session-scoped UUID-matched routes, all matched by
hand-rolled string and regex comparison inside a single fetch handler.
Removed browser surface — not current architecture, not supported contract
The deprecated browser product added a React/Vite static bundle, browser-only live streams, a terminal PTY WebSocket, speech routes, a runtime proxy, an Auto-agent editor/scheduler/controllers and report/action HTTP controllers. None of it was used by the native clients. Historical engineering notes may still describe these as failure analysis — they are history, not contract.
Also deleted: src/links.ts (77), docs/mockups/auto-agents.html, the .claude/web-screen-captures/ set, and the web feature doc. The PTY removal is what eliminated the strongest reason to consider a big-bang backend migration.
Features and platform requirements
| Feature | Status | Requirements & behavior |
|---|---|---|
| Multi-host ownership | Core | Synced transcripts plus fresh leases; the native client aggregates hosts and dedupes by hostId |
| Cursored event stream | Core | One global journal pump; 14-day retention; SSE heartbeats carry the current head |
| Durable sends | Core | Client-id idempotency, SQLite queue, composer-safe insertion, transcript-confirmed delivery |
| Direct agent runtimes | Core | Managed tmux sessions for Claude and Codex only; omitted agent defaults to Claude and unsupported kinds return 400 |
| Insight agents | Optional | Markdown/YAML definitions; collectors read repo files, git, GitHub, security and OpenRouter; report generation uses claude -p |
| WhatsApp sidecar | Optional | Baileys authentication and a configured group allowlist; routes to direct Claude or Codex managed sessions |
| APNs alerts | Optional | Apple team, key and bundle configuration; sandbox vs production token awareness |
| Fleet Live Activity | Optional | ActivityKit tokens and the APNs live-activity topic; bounded to three active fleet rows |
| Setup & release automation | Implemented | Ubuntu/Debian or macOS, systemd or launchd, Bun bundle, optional Tailscale Serve |
iOS / iPadOS requirements
- iOS / iPadOS 17.2+, Swift 6 strict concurrency
- GRDB for the durable local store, MarkdownUI for GFM rendering
- Network reachability to every configured host, typically over Tailscale
- Optional APNs credentials on those hosts for push and Live Activity
- XcodeGen (ios/project.yml) is the project source of truth
Host and macOS requirements
- Bun ≥ 1.3.14, single long-lived
serveprocess - tmux for CLI runtimes; Claude Code and/or Codex CLI installed
- Linux for full process enumeration — /proc is used for cwd resolution
- macOS launcher targets macOS 26, needs iTerm2 automation permission
- Tailscale or an equivalently private network — mandatory, not optional
Quirks, invariants and failure behavior
The invariants are what the refactor must not break. The quirks are what previous sessions burned real time rediscovering.
Core invariants
- One global producer writes live deltas. Clients never cause the server to create per-session transcript-tail pumps.
- Journal sequence is monotonic within a database lifetime. A cursor gap triggers resync rather than guessing.
- A client-id send is idempotent, and a durable send is never owned by a SwiftUI view lifecycle.
- Normalized transcript identity is the client-facing key — not the tmux name, which is only used for process ownership and owner tags.
- Foreign fresh leases are respected before resume or control. Close releases the local lease but never deletes transcript history.
- UI status is a projection of hook, transcript, pane and process facts.
- Clients decode leniently, servers validate strictly — native clients tolerate missing or older fields; server inputs are validated at each command boundary.
- Nothing assumes the API is safe on a public network.
Architecture hazards
- Single-process Bun server. One event loop serves all HTTP and both pumps. Synchronous subprocess fan-out on a hot path stalls everything.
- No bare
setIntervalfan-out over a session collection — a growing collection turns the tick into a spawn storm. Batch or stagger. - No /proc on macOS.
listSessionsenriches via/proc/<pid>/cwd. On macOS, CLI and tmux sessions will not enumerate — expected, not a bug. - Bun auto-loads
.env.ps ewwdoes not show a Bun process’s effective environment. - Restart by port, not by pattern. Kill via
lsof -ti :8766; stale processes silently keep the port. Then probe a changed endpoint before calling the deploy done.
The tmux send path — two traps
- Newlines are Enter.
send-keys -ltransmits byte-for-byte, so an embedded\nsubmits early and fragments a multi-line message. Insert via bracketed paste (load-buffer+paste-buffer -p) so Claude collapses it to a single pasted chip and submits it whole. - Do not gate Enter on re-finding your text. A busy Claude swallows input into its own queue and clears the composer, so a composer scrape misfires. Confirm via transcript growth when idle, or composer-cleared →
queuedwhen busy. - codex inside a claude pane breaks that session’s send path. Two session rows claim one pane; the collision guard nulls
tmuxTargetand sends 409. Fingerprint:tmuxNameset buttmuxTarget: null.
Failure behavior the clients guarantee
- Offline — durable cached state stays renderable; the list and transcripts still work with no host reachable.
- Stream loss — reconnect with the persisted per-host cursor; a watchdog catches a silent stream that never errors.
- Unserviceable cursor — the server emits
event: resyncwith its head; the client full-refreshes over REST and resets. - Send failure — surfaced as a failed queue row with retry, delete and send-now, never silently dropped.
- Host unreachable — reported explicitly rather than presented as an empty session list.
- Lease conflict — resume and fork return a conflict instead of two hosts driving one transcript.
serve process is long-lived, so the running
code routinely lags source. A “previously-fixed” bug that recurs is usually
a deploy gap. Compare the process start time against the fixed file’s mtime before
re-debugging correct, unit-tested code.
Major user flows
Six flows carry the product. Each one crosses the client, the API, the domain and a real agent process — which is exactly why the seams matter.
Monitor the fleet
/api/sessions and /api/info per host; two URLs reporting one hostId collapse into a single host.HostLink · MultiHostevent: resync and a REST refresh.HostEvents → LFGStoreCreate a session
Send a message — durably
Answer an agent prompt
Resume, fork and cross-host transfer
Push, background catch-up and deep link
Current architecture vs simplified target
Same product boundary, same runtime, same routes. What changes is where decisions live. Switch the header control to isolate either side.
Current — composition inside two hot files
Target — one decision per seam
Current — iOS state
A high-conflict file across concurrent agent sessions, and expensive to reason about for actor isolation and source of truth.
Target — iOS state
Proposed module boundaries
Concrete file layout, so the refactor is reviewable as a diff rather than argued as a principle.
Backend — from one fetch handler to typed controllers
| src/server/app.ts | Bun startup, error policy, dependency construction |
| src/server/router.ts | Method and path matching, 404 and 405 behavior |
| src/server/schema.ts | Request and response validators |
| src/server/controllers/host.ts | info, ping, users |
| src/server/controllers/events.ts | SSE stream and bounded pages |
| src/server/controllers/directories.ts | repos, dirs, new, inbox |
| src/server/controllers/files.ts | Scoped reads with byte ranges |
| src/server/controllers/sessions.ts | Snapshot, resumable, new, resume, fork, messages, control |
| src/server/controllers/queue.ts | Queue listing and row actions |
| src/server/controllers/push.ts | Device and activity tokens, health |
| src/server/controllers/usage.ts | Cached Claude OAuth usage |
| src/application/session-service.ts | The single orchestration seam |
Session domain — from one file to five seams
| runtime-catalog.ts | Processes, panes, registry and managed binding |
| transcripts/claude.ts | Resolve, decode, extract metadata |
| transcripts/codex.ts | Resolve, decode, extract metadata |
| session-projection.ts | Merge facts into the external Session DTO |
| resumable-repository.ts | Indexed, paginated closed history |
| message-repository.ts | Recent and backward pages, normalization |
RuntimeDriver — the capability interface that replaces agent-kind branching
Avoid a universal base class. A small capability record plus functions keeps the distinct Claude and Codex semantics visible instead of flattening them.
Changes to make
Make the backend/Swift contract executable
TypeScript handlers and lenient Swift Codable models currently evolve independently; route strings are duplicated and only selected builders and decoders have tests.
- Define compact JSON Schema / OpenAPI for external request, response and event envelopes.
- Generate only DTOs and route constants if generation is stable; otherwise validate bodies with Zod and run fixture contract tests against the real Swift decoders.
- Keep lenient response decoding for rolling upgrades; make required command inputs strict.
- Adopt a compatibility rule: servers may add fields; removals and renames require a migration window.
Extract HTTP controllers
- First extract pure validators and controller functions, snapshot-testing status, body and headers for every current route.
- Inject Journal, queue, session catalog, runtime and push dependencies rather than reaching for module singletons.
- Avoid adopting a framework unless the small router itself becomes a burden — the win is separation and testability, not routing features.
Split the store behind one facade
- Keep
SessionStoreas the only environment-facing@Observablesurface. - Move mechanics into HostCoordinator, SessionRepository, Outbox, SessionProjection, SessionCommands.
- Views read published query projections and invoke commands only.
- Characterize cold launch, offline list, optimistic send and create, id remap, resync, deep link and transfer before moving each seam.
Capabilities, persistence and the hot path
- Replace agent-kind branching with the RuntimeDriver capability interface.
- Add a tiny atomic JSON record helper — schema validation, temp-file plus rename, version and corruption reporting — for managed, titles, users, host identity, registry and push tokens. Keep high-churn queue and journal state in SQLite.
- Do not migrate every small file into one database. Inspectability is genuinely valuable for a self-hosted tool.
- Cache one discovery snapshot per pump tick, use async subprocesses for request work, cap collector and runtime concurrency.
Fill the verification gaps
- Start the Bun app with fake dependencies and contract-test every route — including method mismatch, malformed JSON, ranges, stale cursors and conflicts.
- Add one end-to-end test for create → send → queue event → transcript reconciliation against a fake runtime.
- Add macOS model and API tests independent of iTerm automation.
- Add shell syntax and release-bundle assertions to CI so deprecated artifacts cannot re-enter packaging.
- Track p50/p95 session enumeration, event-pump duration, cursor resync count and send confirmation latency in structured logs.
Adopt an active-document policy
The repository holds far more historical .claude, .codex, delegation, verification and improvement-log material than current product documentation.
- Keep one current system map, current feature docs and decision records.
- Archive or remove superseded plans and evidence on a cadence.
- Mark historical documents with status and replacement links so search results stop masquerading as the current API.
Phased roadmap
The phases are ordered because each one removes ambiguity the next one would otherwise have to preserve.
serve.ts into typed controllers. Split
SessionStore.swift behind one observable facade. Establish the
executable backend/Swift contract. These three are where the leverage is.
Guarded by golden route tests and iOS characterization tests
sessions.ts into catalog, transcript and projection modules. Unify small
server records behind one atomic persistence helper. Move blocking process work off
the request and event hot path.
Recorded transcript and process fixtures are the safety net
SessionListView.swift and
desktop/LFGSessions.swift; on macOS, separate API and host state, iTerm
automation and SwiftUI presentation even if the build stays swiftc-based.
Fill the verification gaps. Only then make the runtime-language choice evidence-driven.
Do this after SessionStore query boundaries settle, or view extraction preserves accidental coupling
Priority, risk and guardrails
Ordered by execution sequence, not by size. Each row names the specific way it can go wrong and the specific thing that catches it.
| # | Change | Value | Main risk | Guardrail |
|---|---|---|---|---|
| 1 | Optional-family product decision | Very high | Removing latent use | Usage inventory and one explicit decision |
| 2 | Route/contract tests and schemas | High | Fixture drift | Run against real Swift decoders |
| 3 | Extract HTTP controllers | High | Route behavior changes | Golden status, body and header tests |
| 4 | Split iOS store services | Very high | Offline and outbox regressions | Characterization tests plus real lifecycle verification |
| 5 | Runtime driver capabilities | High | Claude/Codex semantic flattening | Explicit capability matrix and per-driver fixtures |
| 6 | Split session catalog and transcripts | High | Identity binding regressions | Recorded transcript and process fixtures |
| 7 | Persistence and process cleanup | Medium | Migration and corruption | Versioned atomic reads and fallback backups |
| 8 | View and desktop decomposition | Medium | UI regressions | Screenshot or UI evidence after each screen |
| 9 | Re-evaluate backend runtime | Conditional | Big-bang rewrite | Metrics and an API-preserving strangler migration |
Expected complexity reduction
Measured values are counted from the working tree. Targets are proposed budgets, not predictions — they are the thresholds that make the refactor reviewable.
Measured — already landed
| Metric | Before | After | Δ |
|---|---|---|---|
| Production TS lines | 14,212 | 10,271 | −28% |
| serve.ts | 2,396 | 1,408 | −41% |
| Browser-only routes | 13 | 0 | −100% |
| Deleted production modules | 11 | 0 | −100% |
| Frontend build pipelines | 2 | 1 | −50% |
| Live-delivery pipelines | 2 | 1 | −50% |
Total working-tree diff: 396 insertions, 5,901 deletions across 76 files.
Target — proposed budgets
| Metric | Today | Budget |
|---|---|---|
| Largest backend file | 1,721 | ≤ 400 |
| Largest Swift state file | 2,615 | ≤ 400 |
| Files touched to add a runtime | many | 2 |
| Files touched to add a route | 1 hot file | 1 controller + schema |
| Sources of truth in a view | 3 | 1 |
| Routes under contract test | partial | all |
| Sync subprocesses on hot path | several | 0 |
| Runtime variants | 5 | 2–3 |
The runtime-variant budget is contingent on the Phase 0 usage inventory, not assumed.
Definition of simpler
- A route handler contains validation and one application-service call — not process, transcript and persistence mechanics.
- Adding a runtime implements one driver and one transcript adapter, without editing unrelated controllers.
- SwiftUI views have one query source and one command surface.
- Restart, offline, resync and duplicate-send behavior remain deterministic.
- Current architecture and API can be found in one document and verified by an executable contract suite.
- Optional features incur dependencies and branches only when they are an intentional, supported product line.