commit 98843d2184110b08bc0ab8759ba7aef81f32b600 from: Isaac Meerleo date: Tue Jul 14 20:18:05 2026 UTC record canonical architecture decisions commit - a74847ebc13a55884bc2f3a8b311c83548d55908 commit + 98843d2184110b08bc0ab8759ba7aef81f32b600 blob - ba088d2ecc6c709a83f441056a1a7bfab618a34f blob + 526917744516c4cc3b90e94fbe98a5f519ed063a --- CONTEXT.md +++ CONTEXT.md @@ -14,10 +14,15 @@ The conversation the user drives (as opposed to a suba _Avoid_: main chat, primary conversation **Turn**: -One user submission through to the assistant's completed response, -including all tool calls in between. +One prompt submission through to a terminal outcome, including every +Generation and Tool call in between. _Avoid_: round, exchange +**Generation**: +One Provider request and its streamed outcome within a Turn; Tool results +may extend the Projection and cause another Generation before the Turn ends. +_Avoid_: model turn, provider turn + **Projection**: The conversation state as shaped for and sent to a provider's API. _Avoid_: history, context (alone) @@ -63,9 +68,10 @@ The single role holding a given credential after start fugu-api for model keys, fugu-web for the search token. **Coordinator**: -`bin/fugu` — spawns workers, owns conversation state, turn loop, -journal, and the authority map; parses no raw hostile bytes. The -only setgid `_fugu` binary; sheds the group after reading config. +`bin/fugu` — spawns workers, owns the Lead's Projection and Turn +orchestration, the Session Journal, and the authority map; parses no raw +hostile bytes. The only setgid `_fugu` binary; sheds the group after +reading config. **Control fd**: The fixed descriptor (`FUGU_CTL_FD`, 3) each worker's coordinator @@ -114,6 +120,11 @@ into every projection; `#` lines append captures to th A wire protocol (anthropic or openai dialect) plus endpoint and key; named provider blocks coexist with the implicit default. +**Search adapter**: +The provider-specific request and response translation used by brokered +web search; it owns no transport, credential, SSRF guard, or access policy. +_Avoid_: search provider (when naming the adapter seam) + **Effective window**: The context-size denominator for `/context` and the status gauge: `context_limit`, else provider-reported, else compiled table, else @@ -123,9 +134,10 @@ unknown. - The **Coordinator** alone talks to every worker; **Workers** never talk to each other (authority map, I10). -- A **Turn** belongs to a **Session**; only successful (or - iteration-capped) turns survive in the **Journal**'s replay. -- A **Subagent** runs its turns through the same **fugu-api** +- A **Turn** driven by the **Lead** belongs to a **Session**; only successful + (or iteration-capped) Lead Turns survive in the **Journal**'s replay. A + Turn run by a **Subagent** is ephemeral and never enters the Journal. +- A **Subagent** runs each Turn through the same **fugu-api** custodian but executes tools only in the read-only **fugu-tool** role. Correlated model connections make progress concurrently inside that one custodian; the Coordinator queues brokered web calls and blob - 364d411841b75e2dd4c89f93378708b6c5973d99 blob + c43906d6ddbf0e54a107b625cd3db44f786c9a02 --- docs/adr/0001-process-topology.md +++ docs/adr/0001-process-topology.md @@ -4,10 +4,11 @@ The security invariants (handoff/invariants.md I1–I1 separations but leave process count and topology free. We chose a vmd-style hub: `bin/fugu` is the coordinator — it parses the configuration (shedding the `_fugu` group immediately after), spawns -every worker by fork+exec, owns conversation state, the turn loop, the -session journal, and the authority map — and never parses raw hostile -byte formats. Workers under `libexec/fugu/`: **fugu-tty** (curses and -line front end; inherits the terminal fds; no secrets), **fugu-api** +every worker by fork+exec, owns the Lead's Projection and Turn +orchestration, the Session Journal, and the authority map — and never +parses raw hostile byte formats. Workers under `libexec/fugu/`: +**fugu-tty** (curses and line front end; inherits the terminal fds; no +secrets), **fugu-api** (model-credential custodian; TLS, SSE/JSON codecs; no filesystem or exec after the CA bundle loads), **fugu-web** (search-credential custodian; fetch, HTML-to-text, SSRF guard), **fugu-tool** (tool @@ -32,3 +33,6 @@ in credential-custodian or blast-room workers that can The coordinator retains `proc exec` at steady state to spawn subagent executors and the editor room; this is conforming because it parses no raw untrusted input (vmd-parent precedent). +Sharing the canonical Turn Implementation with the read-only Subagent Role +does not move either runtime or add a channel; ADR-0005 records that +refinement. blob - /dev/null blob + ff4e8bf42c4b85e205bc6cdbf7705a740e073a87 (mode 644) --- /dev/null +++ docs/adr/0004-search-adapter-boundary.md @@ -0,0 +1,9 @@ +# Keep web search behind a transport-free adapter + +Although Kagi is the only implementation today, brokered web search will keep +provider-specific request and response translation behind a **Search adapter** +boundary. The adapter builds the search request shape and interprets success +and error responses; `fugu-web` retains TLS, deadlines, credential custody and +attachment, SSRF and access policy, and output caps. This accepts the small +cost of a single-implementation seam so that adding another search service does +not require reopening the security-sensitive transport path. blob - /dev/null blob + 50238fa013fd60d244a8ed388c77c94e926af4df (mode 644) --- /dev/null +++ docs/adr/0005-canonical-turn-mechanics.md @@ -0,0 +1,71 @@ +# Share canonical Turn mechanics without centralizing a Role + +The Lead and Subagent have separate provider-and-tool loops with duplicated +request construction, Generation collection, message pairing, bounds, and +stop handling. They have already drifted on malformed tool arguments and +empty Generations. We will replace those loops with one provider-neutral +Turn Implementation linked into both the Coordinator and the read-only +`fugu-tool -r` Subagent Role. This shares mechanics, not runtime authority: +the Coordinator retains the Lead and its Journal, while `fugu-tool -r` +retains each ephemeral, kernel-confined Subagent Turn. No Role, credential, +channel, or worker-to-worker communication is added; this refines rather than +changes ADR-0001. + +## Interface and ownership + +The canonical Turn Module owns Generation assembly, provider-neutral request +and message construction, complete-object validation for tool arguments, +tool-use/tool-result pairing, shared byte and call limits, empty-Generation +validity, and the request-tool-result iteration rules. Provider codec Modules +retain wire translation. The tool Interface is batch-shaped so the Lead can +preserve concurrent Subagent waves and stable result ordering. An absent +no-argument tool input becomes `{}`; a non-object input or an empty normal +Generation fails before a canonical assistant message is accepted. Provider +length stops and iteration caps follow their explicit policy rather than being +confused with those invalid Generations. + +Two real Adapters supply what legitimately varies. The Lead Adapter supplies +persistence, cancellation checkpoints, steering, mutable and brokered tool +execution, and user-facing observation. The Subagent Adapter supplies its +ephemeral Projection, process cancellation, read-only tool surface, brokered +web access, and bounded final report. Their deliberately different +50-versus-25 iteration behavior, Subagent conclusion Generation, tool +authority, and reporting remain explicit policy; they are not duplicated +mechanics disguised as policy. + +The Lead Adapter uses a deep Turn transaction Interface as the only owner of +pending Projection messages, matching Journal records, background-job +`BEGIN`/`COMMIT`/`ABORT` participation, and the prior-request cache cursor. +Every accepted message crosses that Interface once, active Generations see +committed plus pending messages, and commit or abandon are the only terminal +operations. The Coordinator still chooses cancellation under its atomic +signal gate; the transaction performs the chosen terminal operation exactly +once. A cache cursor advances when a Projection is sent, never merely because +the resulting Turn commits. + +Commit durably records the Journal Turn end before committing background jobs, +then makes the pending Projection state final. Abandon first stops a live +synchronous executor when necessary, aborts pending jobs, records the abandoned +Turn, discards every pending message, and restarts provider caching cold. +`/clear`, `/compact`, `/model`, and resume reset Projection and cache state only +through the same owner. Background-job participation is a private +remote-but-owned seam: production uses the existing imsg Adapter and +transaction tests use an in-memory Adapter without exposing that port through +the Turn transaction Interface. + +## Consequences + +The existing behavior matrix is characterized before either loop is deleted, +so accidental drift is separated from intentional policy. Extraction starts +with a provider-neutral Generation collector over the existing length-carrying +events, then lifts the complete Turn engine after the protocol work in ADR-0006 +and the Turn transaction are stable. Tests exercise both Adapters through the +same Interface, while adapter-specific tests pin the intentional policy +differences and the Lead's commit/cancellation ordering. + +Presentation remains Lead Adapter policy during this migration. A standalone +presentation Module would currently only move branches already divided among +the Coordinator, `output.c`, and `fugu-tty`. TTY, plain, JSON, and NDJSON may +become Adapters only after the canonical Turn Module exposes a stable tagged +Turn-event Interface that lets them own filtering, ordering, streaming, and +final aggregation. blob - /dev/null blob + 1d4cf3462691dd6aa23db4d88835ac20c31b1846 (mode 644) --- /dev/null +++ docs/adr/0006-typed-imsg-exchanges.md @@ -0,0 +1,45 @@ +# Deepen imsgev and keep logical exchanges typed + +The existing `imsgev` Module is the transport seam, but normal callers can +ignore composition failures and many Roles duplicate payload extraction, +chunking, terminal handling, and receive-state validation. A failed middle +chunk can therefore be accepted as truncated complete data, while a failed +terminal frame can leave a receiver waiting indefinitely. We will deepen +`imsgev` rather than layer a second universal protocol Module beside it. + +`imsgev` owns checked normal sends, explicitly best-effort teardown sends, +length-carrying payload views that distinguish an empty payload from extraction +failure, and bounded checked chunk emission. A normal send failure cannot be +silently discarded or followed by a successful terminal frame for the same +logical exchange. Variable-length logical payloads are either bounded to one +checked frame or sent incrementally. + +Correlation, ordering, accumulation caps, and terminal validation remain in +narrow typed exchange Modules for the protocol families that actually share +those rules. Provider requests, tools, web requests, editor composition, and +UI streams do not acquire one parameterized state machine merely because they +all use imsg. `proto.h` remains the single auditable message taxonomy and +authority map, and receivers retain fail-closed, length-carrying Role +validation under I10 and I13. + +## Considered options + +- *Transplant fugu1's arbitrary-length logical-message reassembly.* Rejected: + fugu2 deliberately processes several bounded payloads incrementally and has + a different hub topology. The useful lesson is centralized checked framing + and sequence ownership, not the old wire format. +- *One Module aware of every `FUGU_IMSG` type.* Rejected: its Interface would + mirror all exchange policy, obscure the authority map, and become shallower + as protocols diverged. +- *Fix ignored return values call by call.* Rejected: framing, send failure, + and payload-view rules would remain non-local and drift again. + +## Consequences + +Protocol hardening precedes the Turn refactor in ADR-0005 because it is bounded +and stabilizes the events that the canonical Generation collector consumes. +The protocol test surface injects failure at every chunk and terminal position +and traces wrong sizes, out-of-order data, duplicate or missing terminals, +wrong correlation, overruns, and data after termination. This decision does +not change ADR-0001's process topology, ADR-0002's libevent dispatch, or +ADR-0003's bespoke hub-and-spoke privsep Module.