Commit Diff


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.