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).
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
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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
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.
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
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.
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
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.
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.
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.
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.
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.