Architecture review — conduit

2026-10-19 · ~/src/personal/conduit @ main · hot spots from 1859 commits / 6 weeks · 12 candidates · weighed against 7 competitors

module seam leakage deep module
t3code ab6be13d43bb 9937c46f5cpaseo be0378ce63zeron c526d6f7cf diri bfa680e69amonocode e53e09aad2capy docs 2026-10-19T02:39Zcapy rows = vendor claims

Survey matrix

● ships the deeper shape · ◐ partial · ✕ has conduit's friction · ○ not attempted / not documented

candidatet3codebbpaseozerondirimonocodecapystrength
1 RPC scope beside contract◐◐✕✕✕◐◐Worth exploring
2 Daemon settings module●◐●●●✕○Strong
3 Commit seam owns sidebar change●◐✕●✕✕◐Strong
4 One turn-end notifier✕●●●●◐◐Strong
5 One composition root◐✕●●◐◐○Worth exploring
6 One activity derivation✕●●●●✕●Strong
7 File-derived migrations + rebuild◐●○◐○✕○Worth exploring
8 One provider turn launcher●●●●◐◐◐Strong · top
9 Operations on the adapter●●✕◐◐◐○Worth exploring
10 One session-rows module◐●●✕●✕○Worth exploring
11 One follow-a-subscription module●●●◐●✕◐Strong
12 Composer-draft module◐●●◐○◐◐Worth exploring

Convergence

One owner with one update op (settings, 5 of 6); one launch path shared with recovery (turn launch, 6 of 6); one pure status function (activity, 5 of 7); one subscription primitive (4 of 6).

Outliers worth stealing

t3code's satisfies Record<Method, Scope>; paseo's presence-based notification plan; bb's narrow-only capability handshake; diri's server-minted attention ids.

Gaps — conduit can lead

Nobody resolves RPC context centrally; nobody syncs composer drafts across devices; only bb and t3code run a provider contract suite.

1 · Declare each RPC's scope beside its contract

Worth exploringin-process

server/ws-rpc.ts:225-251 · :313-340 · :405 · :513 · server/ws-rpc/daemon.ts:8-45 · daemon-ws-rpc-layer.ts:724-737 · unknown-session built at ws-rpc.ts:326, ws-rpc/sessions.ts:226, ws-routing-layer.ts:332, daemon-ws-rpc-layer.ts:795

Before — five layers guess scope

DaemonRpcName union (hand-kept)
daemonOnly stubs + stream stubs
payload sniff: sessionId? projectSlug?
special cases: CreateSession, SubscribeShell
re-wrap wsRpcHandlers.X in run

unknown-session error built 4×

After — scope declared once

contract: Rpc + scope (daemon | project | session)

Scope resolver

session → project lookup · one unknown-session · stream scoping

handlers receive their context

Problem. The contract never says whether an RPC is daemon-, project- or session-scoped; parallel lists and payload field names decide it.

Solution. Each RPC declares its scope next to its contract; one Scope resolver turns scope into context.

  • locality: a new RPC touches one place
  • leverage: one lookup, one error
  • compiler rejects an unscoped RPC
  • delete DaemonRpcName and the stubs
ADR-0012 retires relays but leaves dispatch scope undecided — this distinction outlives relays.

Competitors

t3code ◐Exhaustive RPC_REQUIRED_SCOPES satisfies Record<WsRpcMethod,…> — a new method without a scope fails to compile. Auth only: handlers still read threadId.apps/server/src/auth/RpcAuthorization.ts:40
bb ◐Scope is the REST path in defineRoute; context lookup repeated ~30×.packages/server-contract/src/public-api.ts:435
paseo ✕Payload-inferred dispatch in an 8686-line Session; decomposition plan in flight.packages/server/src/server/session.ts:2152
zeron ✕Scope in doc comments plus a hand-kept forwardable() list; routing-drift fixes ba280074, 45beb3da.crates/engine/src/rpc.rs:1487
diri ✕TERMINAL_ONLY_METHODS list plus a raw-JSON "sessionID" sniff; control.rs has 148 commits.diri-engine/src/control.rs:1064
monocode ◐Per-case ownership checks; identity bound to a turn-scoped token.host/server.ts:254
capy ◐Vendor claim: scope in REST path; "not yours" returns 404.docs · API reference › Errors

Convergence: t3code, bb and capy declare scope in the contract; nobody resolves context centrally. The payload-sniffers (paseo, zeron, diri) are where routing fixes land. A gap conduit can lead — held at Worth exploring because no competitor has shipped the full shape.

2 · One daemon settings module owns update-and-persist

Stronglocal-substitutable

domain/daemon/Services/daemon-config-ref.ts:22-77 · daemon-state.ts:27-47 · daemon-foreground.ts:389-410 · config-persistence-service.ts:14-21 · daemon-ws-rpc-layer.ts:428-463 · ws-rpc/settings.ts:52-58 · daemon-config-persistence.ts (dead)

Before — mass: interface ≈ implementation

interface
impl
  • 1 commit config
  • 2 maybe DaemonState
  • 3 requestSave or flush
  • 4 maybe publish
  • every caller picks the order
  • 3 copies of pinHash/keepAwake

After — deep

update()
impl
  • apply → on disk → notify
  • replies after daemon.json is written
  • sole owner of the fields

Problem. Every caller sequences commit, DaemonState, requestSave-or-flush and publish; fix 2d641d49 (nysa, ixhb) was a wrong choice plus a second writer erasing fields.

Solution. One Daemon settings module with one update that applies, persists before returning, and notifies followers.

  • locality: the on-disk invariant lives once
  • tests assert disk, not call order
  • delete daemon-config-persistence.ts
  • three copies become one

Competitors

t3code ●ServerSettingsService.update: semaphore → atomic write → cache → PubSub, plus a debounced fs.watch. Caveat: one stream reads before subscribing.apps/server/src/serverSettings.ts:998
paseo ●DaemonConfigStore.patch: Zod, merge, atomic write, field handlers with reverse-order rollback, then broadcast. 19 commits — settled.packages/server/src/server/daemon-config-store.ts:349
zeron ●SettingsStore single writer; SavePolicy immediate or debounced 400 ms. Retrofitted in 9b811c32.crates/ui/src/settings.rs:438
diri ●update_preferences: save to disk, then swap memory, then emit effects.diri-app/src/store/mod.rs:1482
bb ◐Revisioned CAS single writer for UI prefs (#3303) beside route-sequenced app settings.apps/server/src/services/system/ui-preferences.ts:83
monocode ✕42 hand-written load/save/subscribe triples.src/features/settings/model/settings.ts:783
capy ○Vendor claim: the UI states when a change applies (now, next boot). Persistence not documented.docs · Secrets

Convergence: 5 of 6 code competitors own settings in one module with one update op; t3code and paseo are near-exact templates (paseo adds rollback). Raises strength.

3 · The commit seam owns sidebar change; callers stop announcing

Strongin-process

handlers/session.ts:439,464,501 · auto-settle-layer.ts:94 · worktree-cleanup-layer.ts:119 · session-git-layer.ts:304-323 · session-manager-service.ts:1231 · persistence/effect/commit-and-signal.ts:83-97 · session-workspace-hint.ts:45-52 · daemon-ws-rpc-layer.ts:157-199,297-322

Before

flowchart LR
  H[session handlers] -->|publish 3 of 6| B((event bus))
  AS[auto-settle] --> B
  WC[worktree cleanup] --> B
  SG[session git] -->|raw json_set| SB[(session_sidebar)]
  SM[session manager] --> B
  WH[workspace hint] -->|force marker| SB
  B --> R[RPC layer: subscriber set + sessionsRevision]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class SG,WH,H leak
      

After

flowchart LR
  M[mutation commands] --> C[commit seam: stamp content or overlay]
  C --> F[Daemon feeds module]
  F --> R[RPC: followList]
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class C,F deep
      

Problem. Whether a change reaches the sidebar depends on which caller made it: pin, snooze and rename don't publish, and overlay changes must force the sidebar marker by hand.

Solution. The commit seam owns the stamp (content or overlay) and the announcement; one Daemon feeds module turns committed changes into list wake-ups.

  • locality: one sidebar-visibility rule
  • leverage: 7+ callers stop publishing
  • lost wake-ups tested once
  • grep guard tests can retire
Follows ADR-0004 (mutations are canonical events) and ADR-0009 (one shell feed). Check whether sessionsRevision still earns its place under ADR-0009.

Competitors

t3code ●Publishes after commit in commit order; the shell feed subscribes, snapshots, then filters by sequence. Mutation code never publishes.apps/server/src/orchestration-v2/EventSink.ts:232 · LiveStreamBudget.ts:279
zeron ●WorkspaceHost's private mutate() bumps a counter; watch channels send the current value first, so no lost wake-ups.crates/engine/src/workspace_host.rs:177
bb ◐DB mutators notify after commit via a buffer — yet 59 direct notifyThread calls and some mutators that never notify.packages/db/src/notifier.ts
paseo ✕Handlers publish beside AgentManager; renaming a stored agent never dispatches — conduit's exact bug.packages/server/src/server/agent/agent-manager.ts:2374
diri ✕28 publish_updated call sites; a bounded replay plus events.dropped resync catches misses.diri-engine/src/control.rs:3412
monocode ✕Caller-sequenced upsert, pin, setHistory; works only with one webview.src/app/App.tsx:5919
capy ◐Vendor claim: every mutation returns the updated row; "read" is an event cursor.docs · List threads

Convergence: where the owner publishes (t3code, zeron) the bug class is absent; caller-publish (paseo, diri, half of bb) reproduces it. t3code's shape is ADR-0004's.

4 · One turn-end notifier fed by canonical events

Strongin-process

relay/sse-wiring.ts:597-672 · poller-wiring.ts:88-230 · monitoring-wiring.ts:175-300 · project-relay-publisher.ts:11-44 · turn-ended-alert.ts:14-23 · notification-policy.ts · relay/event-translator.ts (803 lines) · event-pipeline.ts:143 · opencode-runtime-ingress-service.ts:567

Before — five sites, two translators

flowchart LR
  OC[OpenCode SSE] --> T1[legacy event-translator]
  T1 --> S1[sse-wiring]
  P[message poller] --> S2[poller-wiring x2]
  SP[status poller] --> S3[monitoring-wiring]
  CE[canonical events] --> S4[relay publisher]
  S1 & S2 & S3 & S4 -.viewer? subagent? alert id.-> N[push + in-app]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class S1,S2,S3,S4,T1 leak
      

After

flowchart LR
  CL[Claude] --> CE[canonical events]
  OC[OpenCode] --> CE
  CU[Cursor] --> CE
  CE --> A[Turn-end notifier: announce]
  A --> N[push + in-app, deduped]
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class A deep
      

Problem. Five sites each work out viewer, subagent and alert id; OpenCode turn ends still come from a second translator whose output has no browser consumer.

Solution. Every provider's turn ends come from canonical events; one announce(sessionId, outcome) owns viewer and subagent checks, alert identity and dedup.

  • delete ~800-line translator, ~1950 test lines
  • alert ids defined once
  • leverage: one policy, every provider
  • tests cross one interface
Overlaps epic 82tn (.12 moves pollers to the daemon, .24 turn-terminal rule) — fold into it rather than run beside it.

Competitors

paseo ●Edge-only checkAndSetAttention feeds a pure computeNotificationPlan: heartbeat presence picks none, in-app or push. Subagents skipped.packages/server/src/server/agent-attention-policy.ts:42
diri ●Reducer emits turn_completed once; server-minted attention ids; one client NotificationFeed owner. Refactor tell: 7d79aa00.diri-engine/src/attention.rs:1 · diri-app/src/notification_feed.rs:47
bb ●One choke point applyLoggedThreadLifecycleEvent → plugin; re-reads at flush and suppresses if already read.apps/server/src/services/threads/lifecycle-outcome.ts:66
zeron ●Completion stamp set in the same step as status; one client edge detector drives every channel.crates/engine/src/sessions.rs:1498
t3code ✕Two unrelated notifier paths (web coordinator, mobile relay); both can alert for one turn.apps/web/src/components/ThreadNotificationCoordinator.tsx
monocode ◐One announceSession policy; triggers scattered through App.tsx.src/features/notifications/model/notifications.ts:244
capy ◐Vendor claim: run vs turn split; turn outcome settled | aborted | dropped.docs · Streaming › Events

Convergence: 4 competitors route turn ends through one choke point; t3code's split is the cautionary tale. paseo's presence plan maps onto conduit's viewer check.

5 · Tests build the daemon's own composition root

Worth exploringports & adapters

relay/project-relay-layers.ts:298-395,452-543,706-792 · relay-stack.ts:417-442,644-1005 · test/integration/helpers/relay-harness.ts · test/e2e/helpers/e2e-harness.ts · claude-trace-replayer.ts

Before — two roots

flowchart TB
  D[daemon root] --> L[project-relay-layers]
  RS[createRelayStack: own HTTP, PIN copy] --> L
  L -->|10x borrow or build| SVC[services]
  ENV[CONDUIT_TEST_CLAUDE_QUERY_MODULE] -.-> RS
  T[test harnesses] --> RS
  classDef leak stroke:#dc2626,stroke-width:2px;
  class RS,ENV leak
      

After — one root, two adapters

flowchart TB
  P[production inputs: real SDK] --> R[Daemon composition layer]
  T[test inputs: fake SDK, intervals] --> R
  R --> SVC[services]
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class R deep
      

Problem. About ten borrow-or-build forks and a second root exist only for the test harnesses, and production reads an env var to load a test SDK.

Solution. One daemon composition layer with explicit test inputs (Claude SDK, title query, intervals), used by production and harnesses alike.

  • tests cover production wiring
  • two adapters make the SDK seam real
  • env-var test seam deleted
  • duplicate PIN check deleted
Epic 82tn.16 deletes the per-project relay but not where its harnesses go — this card is that decision.

Competitors

paseo ●createTestPaseoDaemon wraps the production createPaseoDaemon; 89 test files. No clock injection.packages/server/src/server/test-utils/paseo-daemon.ts:96
zeron ●EngineCore::assemble shared by 38 of 46 tests; production adds auth and updater outside it, which tests skip.crates/engine/src/lib.rs:133
t3code ◐runtimeLayer exports layer and layerProduction; but ProviderReplayHarness rebuilds the graph by hand and can drift.apps/server/src/orchestration-v2/runtimeLayer.ts:347
bb ✕Three hand-kept wirings; one new dependency touches all three (#5441).apps/server/test/helpers/test-app.ts:156
diri ◐Same ControlServer type with fewer builders; background loops wired per test or not at all.diri-engine/tests/control_socket.rs:237
monocode ◐Only the remote host injects fake providers; App.tsx has no integration coverage.host/server.test.ts:46
capy ○Not documented.—

Convergence: paseo, zeron and t3code share one root; every second wiring (t3code's harness, bb's three) drifts. Strength held at Worth exploring because 82tn.16 is about to move the ground.

6 · One session-activity derivation for sidebar, header and composer

Strongin-process

persistence/effect/read-query-effect.ts:137-390,1374,2038 · sidebar-projection.ts:133,184-230 · session-family-query.ts:3 · frontend/stores/session.svelte.ts:116,243 · chat.svelte.ts:299-410 · turn.svelte.ts:75 · SubagentBackBar.svelte:19-26 · SessionBar.svelte:79-82 · InputArea.svelte:159-162

Before — five answers, four roots

flowchart LR
  Q1[caller 1374] & Q2[caller 2038] & Q3[sidebar-projection] -->|rebuild 6 maps| F[roll-up fn: 10 optional inputs]
  F --> R[row status]
  R --> A[isBusy] & B[isSessionBusy] & C[chat phase] & D[turn.working] & E[attention]
  A & B & C & D & E --> UI[SubagentBackBar / SessionBar / InputArea re-derive]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class Q1,Q2,Q3,UI leak
      

After

flowchart LR
  FR[family rows + overlays] --> S[Family summary module]
  S --> I[SessionInfo incl. activity]
  I --> AC[activity id: working, monitoring, idle, needs-you]
  AC --> UI[every component]
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class S,AC deep
      

Problem. The roll-up takes ten optional inputs that three callers rebuild, "root" is defined four ways, and the client answers "what is this session doing" five ways.

Solution. One Family summary module computes activity on the server; the client reads activity(id) from one module, and the legacy chat phase layer goes.

  • locality: six fix commits concentrate
  • interface shrinks to one input type
  • leverage: six+ UI readers, one answer
  • delete dead chat streaming fields

Competitors

paseo ●Pure deriveAgentStateBucket in the shared protocol package; the server rolls descendants up to workspace status.packages/protocol/src/agent-state-bucket.ts:22
bb ●Server facts plus one shared resolveThreadListIndicator with child roll-up, used by web, desktop and mobile.packages/client-core/src/thread/thread-activity.ts
zeron ●Pure effective_indicator / display_status in proto::view, shared by desktop and iOS.crates/proto/src/view.rs:44
diri ●StatusReducer is "the single canonical answer"; attention() on the wire type. One UI mapping copy already drifted.diri-engine/src/status/mod.rs:1
capy ●Vendor claim: one server status including subagents; needs-you orthogonal; four legacy fields collapsed into one.docs · Threads › Read a thread's status
t3code ✕Four client derivations that can disagree.apps/web/src/components/Sidebar.logic.ts:922
monocode ✕Separate busy / approval / unseen sets, duplicated in Sidebar.src/app/App.tsx:2192

Convergence: 5 of 7 ship one pure status function; capy collapsed four fields into one. Both outliers show conduit's drift.

7 · Migrations derived from files; derived tables rebuilt after

Worth exploringlocal-substitutable

persistence/effect/migrations.ts:133,461,972-1250,1115,1219-1237 · persistence/schema.ts · read-model-types.ts:6 · migrations/fork-lineage-import.ts:240-248 · sidebar-projection.ts:73,105,225 · sidebar-row.ts:9

Before

flowchart TB
  SQL[0062_sessions_archived.sql] -.id 0063.-> REG[hand-kept registry]
  COL[one column] --> REG & EXP[expectedTableColumns] & APP[appendedSessionColumns]
  REG --> MIG[migrations] -->|calls live code| PROJ[refreshSidebar / rerootSession]
  PROJ -.must handle every schema.-> ROW[SessionRow cast, optional fields]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class SQL,PROJ,ROW leak
      

After

flowchart TB
  DIR[migrations dir: filename = id] --> M[Migration registry]
  M --> MARK[mark families dirty]
  MARK --> RB[Rebuild on final schema + shape version]
  M --> DEC[SessionRow decoded by Schema]
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class M,RB deep
      

Problem. Registry ids drift from filenames, a column is listed three times, rows are cast rather than decoded, and migrations call live projection code that must cope with every historical schema.

Solution. Filename is the id, one add-column form feeds the schema check, rows decode, and migrations mark families dirty for one rebuild on the final schema.

  • one column = one file + one field
  • merge clashes become filename conflicts
  • projection sees one schema
  • stale sidebar rows heal on shape bump

Competitors

bb ●drizzle-kit journal derived from 145 files; rows from $inferSelect; FTS rebuilt in SQL. Cost: a 1472-line migrate wrapper (42 commits).packages/db/src/migrate.ts:1412
t3code ◐Hand-kept list with a fail-fast id-clash guard; rows decoded with Schema; replay rebuild only via t3 doctor.apps/server/src/persistence/Migrations.ts:157
monocode ✕Hand-numbered chain needed an idempotent repair pass because builds reused numbers.src-tauri/src/session_store.rs:968
zeron ◐Versions are array positions; cache DBs are dropped and rebuilt, not migrated.crates/engine/src/change_request_store.rs:252
diri ○No framework; CREATE IF NOT EXISTS and versioned table names.diri-engine/src/attention.rs:154
paseo ○No server DB; one COMPAT migration with a delete date; client cache dropped on version mismatch.packages/app/src/runtime/replica-cache/row-store-sqlite.ts:60
capy ○Not documented.—

Convergence: rebuild derived data rather than migrate it (zeron, t3code, bb, paseo). Hand-kept ids hurt monocode the way they hurt conduit; bb shows how far a wrapper can grow.

8 · One provider turn launcher, shared by first delivery and recovery

Strongports & adapters

provider/orchestration-side-effect-reactor.ts:442-599 · claude/claude-runner-recovery.ts:46-163 · claude-runner-driver.ts:54-80 · prepare-turn.ts:96,340 · config dir resolved ~15× · provider-turn-dispatch.ts:550-565 · runner/runner-recovery.ts:233-245 · cursor-runner-payload.ts:1039

Before

flowchart TB
  OB[outbox reactor] -->|if supportsNativeResume| CL[Claude launch planning]
  RC[claude recovery] -->|copy| CL2[same planning again]
  DR[runner driver] -->|3rd folder resolve| CFG[(config dir x15)]
  OB --> S1[settle: native-resume only]
  PD[turn dispatch] --> S2[settle: every provider]
  RR[runner recovery] --> S3[settle: every provider]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class CL,CL2,CFG,S1 leak
      

After

flowchart TB
  OB[outbox reactor] --> L
  RC[recovery] --> L
  PW[prewarm / continuation] --> L
  L[Turn launcher: send-turn row in, dispatch-ready turn + fallback + settlement out]
  L -.seam.-> C[Claude] & U[Cursor] & O[OpenCode]
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class L deep
      

Problem. Claude launch planning sits in the generic outbox reactor and is copied into recovery; three owners settle turn results with different gating; the config dir is resolved ~15 times (fixes 0e673ac2, 85363dec, 52af44ca).

Solution. Deepen prepareTurnEffect into one Turn launcher that the reactor and recovery both call; it also owns settlement.

  • locality: one owner of folder and cursor
  • leverage: reactor, recovery, prewarm share it
  • reactor's Claude branch deleted
  • settlement tested per TurnResult

Competitors

t3code ●ProviderTurnStartServiceV2.start serves start and restart; resume, else fresh thread plus a context-handoff event; never replays a process-bound effect.apps/server/src/orchestration-v2/ProviderTurnStartService.ts:516
zeron ●Engine owns resume (resume_for); callers always pass None; recovery auto-resumes through the same dispatch.crates/engine/src/sessions.rs:1168
bb ●prepareTurnSubmitCommandPayload for live, queued and retry; each command carries a self-contained resumeContext.apps/server/src/services/threads/thread-commands.ts:270
paseo ●sendPromptToAgent — "every surface MUST go through it"; pending/completed receipts report unknown outcomes instead of resending.packages/server/src/server/agent/agent-prompt.ts:95
monocode ◐Recovery resubmits Continue through the normal send; five manual bind sites.src/features/sessions/model/inFlight.ts:8
diri ◐conversation_plan is "every launch path", yet branching stays inline in control.rs.diri-engine/src/agent.rs:294
capy ◐Vendor claim: each turn resolves a connection with an out-of-credit fallback policy.docs · Models & pricing › Connections

Convergence: every code competitor routes first delivery and recovery through one launch path. t3code's Effect service is a near drop-in template. Raises strength.

9 · Optional provider operations live on the adapter

Worth exploringports & adapters

contracts/provider-instance.ts:22-189 · instance-manager-service.ts (31 trait reads) · runner-provider-instance.ts:96,500 · runner-payload.ts:46 · handlers/background-tasks.ts:29 · session-command.ts:743-900 · test/helpers/provider-runtime-stream-invariants.ts

Before — task stop declared five ways

sessionCapabilities.supportsTaskStop
RunnerDriverPieces.stopTask
RunnerPayload.stopTask?
runner hello: stopsTasks
ProviderInstance.stopTaskEffect?

~25 traits read in ~55 files · invariants checked on 1 of 3 adapters

After

Provider adapter

fork? · stopTask? · nativeSession? — present = capable

Claude
Cursor
OpenCode
one contract suite over every adapter's committed traces

Problem. Single-provider traits act as identity checks in ~55 files, task stop is declared five ways, and the stream-invariant contract runs on one adapter of three.

Solution. Behaviour-selecting traits become adapter operations whose presence is the capability; static facts stay for the UI; one contract suite runs every adapter's traces.

  • locality: a new provider touches one module
  • four task-stop declarations deleted
  • the seam becomes real for three adapters
  • ADR-0002 traces gain a contract

Competitors

t3code ●Optional runtime ops plus typed capabilities; "orchestration reads capabilities, never the driver kind"; replay-fixture contract test.packages/provider-core/src/server/ProviderAdapter.ts:541
bb ●Closed AdapterCommand union; handshake facts may only narrow the declaration; runBridgeConformance on every bridge.packages/provider-bridge-protocol/src/conformance/index.ts:41
monocode ◐Presence-based optional ops behind canX(); per-session op queue; no contract suite across 11 adapters.src/integrations/harness/core/registry.ts:53
paseo ✕Flags and optional methods side by side, checked twice — drift.packages/server/src/server/agent/rewind/rewind.ts:18
zeron ◐Boolean flags on the Harness trait; no conformance suite; turn-end drift fixed in 91fd851c.crates/harness/src/lib.rs:163
diri ◐Data manifests; one function turns provider facts into a capability struct.diri-engine/src/agent.rs:224
capy ○Own harness over model APIs; no comparable layer.docs · Models & pricing

Convergence: split on flags vs operations — those carrying both (t3code, paseo) name the disagreement a bug. Only bb and t3code run a contract suite, and both lean on it. Steal t3code's "never the driver kind" rule.

10 · One session-rows module for every client feed

Worth exploringin-process

frontend/transport/session-subscription.svelte.ts:134-207 · stores/session.svelte.ts:59,80,510-578,725,743

Before

flowchart LR
  SH[shell feed] --> A1[transport applier]
  FA[family feed] --> A2[applyFamilyChange]
  A1 -->|busy, turn end, delete...| X[reactions]
  A2 -->|same, re-written| X
  A1 -.imports 7 stores.-> ST[stores]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class A1,A2 leak
      

After

flowchart LR
  SH[shell] & FA[family] & SE[search] & SV[saved] --> R[Session rows module: version wins]
  R -->|row changed prev to next| X[reactions, subscribed once]
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class R deep
      

Problem. Shell and family appliers each re-write the same reactions, split rows by an informal rule, and drifted (b196938b: finished session stuck on Working).

Solution. One Session rows module takes every feed, picks the winner by version, and emits one row-changed notice that reactions subscribe to once.

  • ~70-line near-copy deleted
  • transitions written once
  • transport only delivers rows
  • tests: envelopes in, notices out
Builds on ADR-0009 / ADR-0010 feeds; does not reopen them.

Competitors

paseo ●DirectoryTransactionOwner replays deltas over an in-flight snapshot; newer row wins by updatedAt.packages/app/src/runtime/directory-sync/transaction.ts
bb ●Cache-owner modules patch every list copy of a row; updatedAt guard; held optimistic patches. Cost: ~45 files.apps/app/src/hooks/cache-owners/query-cache.ts:720
diri ●One record map; the previous/current diff in upsert is the only place side effects run.diri-app/src/store/mod.rs:2055
t3code ◐Sequence-gated apply, but shell and detail stores disagree (documented in connection-runtime.md).packages/client-runtime/src/state/shellReducer.ts:130
monocode ✕Two caches reconciled by hand.src/features/sessions/data/sessionHistory.ts:29
zeron ✕Each list replaced wholesale; optimistic patches flicker away.crates/ui/src/state.rs:1158
capy ○Not documented.—

Convergence: one row owner with a version guard (paseo, bb, diri); split stores (t3code, monocode, zeron) show conduit's drift. Prefer server sequence over wall-clock updatedAt.

11 · One follow-a-subscription module

Strongin-process

frontend/stores/todo.svelte.ts:25-66 · input-draft.ts:24-58 · session-family-feed.ts:26-82 · approvals.ts:30-68 · session-list.svelte.ts:132-202 · transcript.svelte.ts:868 · terminal.svelte.ts:340 · project.svelte.ts:134 · alerts.ts:72 · project-settings.ts:79 · discovery.svelte.ts:470-545 · selection.svelte.ts:357-377 · ChatLayout.svelte:360-410

Before — 11 copies of one lifecycle

todo
no resub
draft
no resub
approvals
no resub
transcript
no resub
family
session-list
terminal
project
alerts
proj-settings
discovery

each: generation counter · fiber swap · getRuntime · forProject · supervise · stale guard

selection + ChatLayout open/close four feeds and run an eight-call reset

After — call graph collapsed

follow(key, open, apply, {resubscribeOn})

generation
fiber swap
account resub
stale guard
supervise
status
Viewed session — transcript · todo · draft · family open/close as one unit

Problem. About eleven hand-rolled copies of the subscribe lifecycle; four skip resubscribe on account switch, and selection and ChatLayout orchestrate store internals.

Solution. A transport follow module returns a handle with status; a Viewed session module opens the per-session feeds as one unit.

  • leverage: 11 call sites, one race rule
  • account-switch bug class gone
  • one fake subscription tests every feed
  • eight-call reset deleted

Competitors

paseo ●OwnedSubscription: ready snapshot, subscribe, release, generation counter; the push router reconciles desired vs active subscriptions (#4596, #4737).packages/client/src/connection/owned.ts:12
t3code ●subscribeDynamicMapped switchMaps over the supervisor session; transport vs domain failure split; idle-TTL ref-count.packages/client-runtime/src/rpc/client.ts:249
bb ●WebSocketManager: ref-counted keyed subscriptions, resubscribe on open, disconnect watermark.apps/app/src/lib/ws.ts:124
diri ●One global subscription; engine-instance id resets the replay cursor.diri-client/src/client.rs:280
zeron ◐Generic spawn_watch, yet five copies repeat their own stale guards.crates/ui/src/state.rs:3173
monocode ✕~74 hand-rolled listen() sites.src/app/App.tsx:13007
capy ◐Vendor claim: SSE resume via Last-Event-ID; until=run.docs · Streaming › Resuming

Convergence: paseo, t3code and bb each built one primitive after churn. zeron's half-done primitive shows copies return unless key-scoped stale guards live inside it.

12 · A composer-draft module per session

Worth exploringin-process

components/input/InputArea.svelte:320,393-413,607,870-885 (1679 lines, ~150 commits/6 wk) · stores/chat.svelte.ts:436-472 · server-status.svelte.ts:222-243 · selection.svelte.ts:344 · turn.svelte.ts:238-295 · transcript.svelte.ts:690

Before

flowchart TB
  IA[InputArea.svelte: draft map, grace window, debounce, flush, restore]
  CS[chat store: global inputSyncState] -.callback hook.-> IA
  SS[server-status] --> CS
  SEL[selection] --> CS
  IA & TU[turn store] & TR[transcript] --> DS[optimistic draftSend]
  classDef leak stroke:#dc2626,stroke-width:2px;
  class IA,CS leak
      

After

flowchart TB
  D[composerDraft sessionId: text, local-vs-remote, debounce, persist, clear on send, restore on failure]
  IA[InputArea] -->|binds| D
  R[remote draft feed] --> D
  classDef deep fill:#0f172a,color:#fff,stroke:#0f172a,stroke-width:3px;
  class D deep
      

Problem. Draft rules live in the busiest component behind a global sync state and an inverted callback, so they are only testable by mounting it.

Solution. A Composer draft module per session owns every draft rule; InputArea only binds to it.

  • ~150 lines leave the component
  • callback hook deleted
  • draft rules testable as functions
  • restore-if-empty stops clobbering

Competitors

bb ●Module store, 250 ms debounce, storage-event sync that ignores keys with a pending local write; restoreIfEmpty. Reverted server autosave (#4455).apps/app/src/hooks/usePromptDraftStorage.ts:123
paseo ●Persisted draft store with lifecycle active | abandoned | sent and a TTL; injectable scheduler; clear-then-restore submit.packages/app/src/stores/draft-store/index.ts:253
t3code ◐Draft store (v9 migrations), restore only if still empty with a generation guard — but send logic sits in an 8706-line ChatView.apps/web/src/composerDraftStore.ts
monocode ◐draftCache module plus a revision-guarded restore; not persisted.src/features/sessions/model/draftCache.ts:1
zeron ◐Per-key map in the composer; restore overwrites text typed during send.crates/ui/src/composer.rs:9550
diri ○In-memory map; cleared only on confirmed delivery. The agent TUI holds the real composer.diri-app/src/launcher.rs:1742
capy ◐Vendor claim: a queued message can return to the composer to edit.docs · Threads › Send and steer

Convergence: bb, paseo, t3code and monocode own drafts outside the view; restore-if-empty with a generation guard recurs. Nobody syncs drafts across devices — conduit leads there, and bb's revert warns against server autosave.

Top recommendation

8 · One provider turn launcher

Densest fix history (claude runtime 23, translator 13, runner 12 fixes in 6 weeks), outside epic 82tn so nothing collides, and the strongest convergence in the survey: every code competitor routes first delivery and recovery through one launch path — t3code's ProviderTurnStartServiceV2 is an Effect-native template, zeron and bb show the engine owning resume.

Quick wins beside it: 2 · settings module (5 of 6 converge, deletes dead code) · 3 · commit seam owns sidebar change (paseo ships conduit's exact bug).