You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Use Effect where it earns its place, and say plainly where it does not. An audit after the POC
phase found the dependency is carried in full (effect@4.0.0-rc.115) but used in three files'
worth of runtime combinators — Schema is everywhere and genuinely load-bearing, Cron is used
properly, and beyond that it is Stream in one file, Fiber in another, and one Effect.repeat in the scheduler. There are zero uses of Layer, Context, or any platform
module, on a version that ships effect/unstable/cli, http, sql and process in-package.
Two gaps cost us something concrete today:
The CLI.src/cli.ts hand-rolls four argument parsers that disagree with each other — a
stride-2 flag walker that structurally cannot represent a boolean flag, a separate single-step
loop for factory start because it needed one, an inline loop for init, and a serve parser
that passes an unvalidated Number(portRaw) straight to Bun.serve. usageError calls process.exit inside a parser, so the whole argv layer is untestable and untested: every CLI
test bypasses argv entirely.
Dependency injection. We already built a hand-rolled DI container out of optional fields —
six Injectable for tests comments, a DispatchEnv bag threaded recursively through the
dispatch chain, a dedupeRegistry? option that exists only because the registry is a
process-wide singleton, a now?: () => number clock seam, a beforeStart? hook whose only job
is holding a window open in one test. Four module-level mutable maps mean two daemons cannot
coexist in one process. Every feature since #13 has added another ?: seam field, and that drip
continues until the pattern is replaced by the thing it is imitating.
The agent runtime is where both gaps meet, and it is the worst case of each: the adapter is
threaded by hand through eight files and ~20 call sites, defaulted to opencodeAdapter in two
separate places, and not selectable from config — while the interpretation of its chunk stream
sits on the Factory side of the seam, so a second adapter would have to emit opencode's exact
CUSTOM event names to surface a session id.
Decisions
Recorded as ADRs, written before the code:
ADR 0012 (new) — the agent-runtime seam: chunks stay opaque and AG-UI-shaped, the adapter
interprets its own stream and prepares its own workspace, the agent runtime becomes a
context-resolved service, the adapter contract stays plain async, and the sandbox provider is
explicitly not abstracted yet.
ADR 0009 §5 (amendment) — Effect's boundary moves in one direction: it gains the CLI and a
DI composition root inside the daemon. The authoring surface, the event store (D21) and the
HTTP route handlers (D22) are reaffirmed as plain, not reopened.
ADR 0004 (status note) — D22 reaffirmed with a clarification, so nobody reads "plain Bun.serve" as "no Effect anywhere in the server" once the composition root lands.
The failure mode this epic is steering away from is not "too little Effect" — it is shallow
Effect spread evenly everywhere, which costs the ergonomics of both idioms and buys neither.
Both surfaced in the same audit, both are worth fixing whether or not this epic proceeds, and
neither depends on any Effect work.
Out of scope
effect/unstable/http / HttpApi — deferred, not rejected. D22's reasoning still mostly
holds; the trigger is the API surface growing past what web/api.ts's hand-written client can
mirror without drift. Doing it before DI means doing it twice.
effect/unstable/sql for the event store — ADR 0010/D21 stands. bun:sqlite is
synchronous in-process I/O and the store is 181 lines. Only Migrator is worth revisiting, and
only once the schema outgrows one table.
effect/unstable/process for hostExec — 20 lines of Bun.spawn + AbortSignal, and the ExecFn seam already exists.
What to build
Use Effect where it earns its place, and say plainly where it does not. An audit after the POC
phase found the dependency is carried in full (
effect@4.0.0-rc.115) but used in three files'worth of runtime combinators —
Schemais everywhere and genuinely load-bearing,Cronis usedproperly, and beyond that it is
Streamin one file,Fiberin another, and oneEffect.repeatin the scheduler. There are zero uses ofLayer,Context, or any platformmodule, on a version that ships
effect/unstable/cli,http,sqlandprocessin-package.Two gaps cost us something concrete today:
The CLI.
src/cli.tshand-rolls four argument parsers that disagree with each other — astride-2 flag walker that structurally cannot represent a boolean flag, a separate single-step
loop for
factory startbecause it needed one, an inline loop forinit, and aserveparserthat passes an unvalidated
Number(portRaw)straight toBun.serve.usageErrorcallsprocess.exitinside a parser, so the whole argv layer is untestable and untested: every CLItest bypasses argv entirely.
Dependency injection. We already built a hand-rolled DI container out of optional fields —
six
Injectable for testscomments, aDispatchEnvbag threaded recursively through thedispatch chain, a
dedupeRegistry?option that exists only because the registry is aprocess-wide singleton, a
now?: () => numberclock seam, abeforeStart?hook whose only jobis holding a window open in one test. Four module-level mutable maps mean two daemons cannot
coexist in one process. Every feature since #13 has added another
?:seam field, and that dripcontinues until the pattern is replaced by the thing it is imitating.
The agent runtime is where both gaps meet, and it is the worst case of each: the adapter is
threaded by hand through eight files and ~20 call sites, defaulted to
opencodeAdapterin twoseparate places, and not selectable from config — while the interpretation of its chunk stream
sits on the Factory side of the seam, so a second adapter would have to emit opencode's exact
CUSTOM event names to surface a session id.
Decisions
Recorded as ADRs, written before the code:
interprets its own stream and prepares its own workspace, the agent runtime becomes a
context-resolved service, the adapter contract stays plain async, and the sandbox provider is
explicitly not abstracted yet.
DI composition root inside the daemon. The authoring surface, the event store (D21) and the
HTTP route handlers (D22) are reaffirmed as plain, not reopened.
Bun.serve" as "no Effect anywhere in the server" once the composition root lands.The failure mode this epic is steering away from is not "too little Effect" — it is shallow
Effect spread evenly everywhere, which costs the ergonomics of both idioms and buys neither.
Subtasks
effect/unstable/cliSchema.TaggedError#33, #34 and #35 have no blockers and can start in parallel.
Related, deliberately outside this epic
permissionModeis declared on the authoring surface and read by nobodyBoth surfaced in the same audit, both are worth fixing whether or not this epic proceeds, and
neither depends on any Effect work.
Out of scope
effect/unstable/http/HttpApi— deferred, not rejected. D22's reasoning still mostlyholds; the trigger is the API surface growing past what
web/api.ts's hand-written client canmirror without drift. Doing it before DI means doing it twice.
effect/unstable/sqlfor the event store — ADR 0010/D21 stands.bun:sqliteissynchronous in-process I/O and the store is 181 lines. Only
Migratoris worth revisiting, andonly once the schema outgrows one table.
effect/unstable/processforhostExec— 20 lines ofBun.spawn+AbortSignal, and theExecFnseam already exists.Schedule.cronin the scheduler — the cron string is stored deliberately so next-fire iscomputable for the UI (Schedules page with next-fire times and manual trigger #17). The 30s poll granularity is the accepted cost.
including
ctx.*.non-host sandbox, not a second model provider.