commit 4a93e0de0e87b979672e0c5ca321485d1ee4a16f from: Isaac Meerleo date: Fri Jul 17 21:51:03 2026 UTC Document Compaction epoch policy commit - 9c156e43a6c19018ba9fa33b3031ab6bf197b0ba commit + 4a93e0de0e87b979672e0c5ca321485d1ee4a16f blob - f4993a3a63c5be2e2470480bcf0bfabba6d5dfb9 blob + bdbbd99776166f2f853207fbc6392a1648aeeede --- docs/compaction.md +++ docs/compaction.md @@ -1,11 +1,55 @@ # Compaction Compaction replaces the Lead's active Projection with model-written -continuation notes. It is currently initiated explicitly with `/compact`. -The completed notes are journaled as the Compaction record, injected into the -rebuilt system block, and recovered on resume. The transcript and the command's -visible behavior do not change. +continuation notes. `/compact` initiates it explicitly. With `auto_compact yes` +(the default), the canonical Lead Turn can also Compact before a prospective +Generation crosses its configured reserve, or once after an exactly classified +context-overflow rejection. A completed cut, its immutable System epoch, its +structured file details, and any retained Projection suffix are one Journal +record and are recovered together on resume. +## Threshold and estimate + +Automatic threshold Compaction runs only when the Effective window is known. +The existing window priority remains authoritative: `context_limit`, then +Provider-reported model metadata, then the compiled model table. Before every +Lead Generation, including continuations after Tool results, fugu conservatively +estimates the actual prospective request from the selected System-baseline +bytes, canonical Projection blocks, offered Tool definitions, and fixed +Provider-wire structural allowances. It converts the resulting bytes with +`ceil(bytes / 4)`; usage reported for an earlier request is never substituted +for this prospective estimate. + +The strict trigger is: + +```text +estimated tokens > Effective window - compact_reserve_tokens +``` + +The default reserve is 16,384 tokens. An unknown window skips this threshold +check. `auto_compact no` disables both threshold cuts and overflow recovery but +does not disable `/compact`. + +## Cut selection + +Automatic Compaction normally keeps complete recent Turns up to +`compact_keep_tokens` (20,000 by default) and summarizes the older prefix. The +retained Projection never starts with a Tool-result message and never separates +an Assistant Tool call from its exact result set. Prompt-cache receipts become +cold after every successful cut. + +When the current Turn alone exceeds the keep target, fugu may split its early +prefix at a valid Assistant boundary. A Tool-using Assistant is a valid retained +boundary only when the immediately following User message contains exactly one +result for each call. The early portion is separately delimited as +`` and merged into the replacement notes; the valid recent suffix +stays canonical. If no such boundary exists, the complete Turn is retained. + +An automatic cut during an active Turn is provisional. Cancellation, failure, +or a crash discards both the cut and that Turn's Pending Projection. The epoch +and retained snapshot become replayable only with the matching successful Turn +terminal, so already executed Tools are neither lost nor replayed. + ## Isolated request The Compaction Generation does not replay the active Projection in its live @@ -71,6 +115,17 @@ The conversation section has this byte-exact outer sha ``` +For an oversized active Turn, the summarized early prefix follows as a second +quoted section: + +```text + +[User]: current Turn input + +[Assistant]: early work within this Turn + +``` + Records are separated by one blank line and retain canonical message and block order. Adjacent User text blocks concatenate. Adjacent Assistant text blocks join with a newline. Adjacent Tool calls join with `; `. Empty text and Tool @@ -105,16 +160,63 @@ Provider JSON boundary, the existing central codec esc and replaces malformed UTF-8 with U+FFFD. The complete User document, including any previous-summary section and both -wrappers, is capped at `FUGU_REQUEST_MAX`. Provider serialization is checked -against the same cap afterward because JSON escaping can expand controls and -malformed bytes. Exceeding either bound fails Compaction conspicuously without -changing the Projection, Compaction notes, Journal, or Projection cursor. +wrappers (and an optional turn-context wrapper), is capped at +`FUGU_REQUEST_MAX`. Provider serialization is checked against the same cap +afterward because JSON escaping can expand controls and malformed bytes. +Exceeding either bound fails Compaction conspicuously without changing the +Projection, System epoch, structured file state, Journal, or Projection cursor. +## Structured file details + +After accepting model prose, fugu derives cumulative file details from +structured `read`, `write`, `edit`, and `multi_edit` Tool-call arguments in the +summarized prefix. It never guesses paths from shell text. Accepted calls are +tracked even when their result reports failure: these lists describe +model-visible structured work, not an audited filesystem log. + +The new details are merged with the prior epoch's structured sets. Modified +paths win over read-only paths; both sets are bytewise sorted and deduplicated. +Fugu, not the model, then appends exactly one pair of blocks after the prose: + +```text + +src/read-only.c + + + +src/changed.c + +``` + +Paths must be bounded valid UTF-8 without controls or line separators, and +`&`, `<`, and `>` are escaped in the rendered blocks. The structured arrays are +stored separately in the System-epoch Journal record, so repeated Compaction +and resume merge trusted details rather than parsing model prose. `/clear` +empties them. + +## One-shot overflow recovery + +When automatic Compaction is enabled, a rejected Lead Generation may be +recovered exactly once per Turn only if no Provider content was accepted and +the structured error is unambiguous. Today that means an OpenAI-compatible HTTP +400 response or stream error whose exact `error.code` is +`context_length_exceeded`. English text, HTTP status alone, Anthropic's broad +`invalid_request_error`, Claude Subscription errors, and HTTP 413 byte limits +never qualify. + +Recovery Compacts the current Projection and retries only the rejected +Generation. It does not rerun a Tool batch that has already produced results. +A second overflow, any post-content failure, cancellation, failed Compaction, +or overflow from the summarization request terminates normally through the +existing error path without another retry. Usage from the summarization and +retried request remains additive. + ## Completion and failure A Compaction result is accepted only when the Provider completes one normal, non-empty Assistant text response without Tool calls. Cancellation, a Provider -error, a length stop, malformed output, an attempted Tool call, or either size -bound leaves the existing state intact. On success, the Turn transaction -journals the notes and resets the Projection through its existing Compaction -operation; `/clear` later voids those notes as before. +error, a length stop, malformed output, an attempted Tool call, or any size +bound leaves the existing epoch, Projection, structured files, and cursor +intact. On success, one complete replacement System epoch is appended and +synchronized before the live prefix is removed. `/clear` later installs a new +no-summary epoch and voids the notes and file details. blob - 558868ffb6292c86f70e4ad0fa0c6184be41d09c blob + e560d830dc67656f1004feabeb8ea26b488f9cdb --- handoff/behavior.md +++ handoff/behavior.md @@ -272,8 +272,10 @@ A line starting with `/` is a command, not a message: - **/clear** — reset the active context: the next message starts a fresh conversation. The on-screen transcript stays as scrollback; - the journal is preserved and records the reset so a resume also - starts fresh (§7). + the journal is preserved and synchronizes one replacement System epoch so a + resume also starts fresh (§7). It adopts the invocation's current base, + reuses the Session-cached personal bytes, re-reads project context under + current policy, and drops Compaction notes and file details. - **/compact** — replace the active context with a model-written summary of the conversation (one extra API call, no tools offered to it). Serialize any prior Compaction notes and the active Projection as one @@ -286,12 +288,22 @@ A line starting with `/` is a command, not a message: preserve goals, constraints, decisions and rationale, work state, key paths and identifiers, and open questions, addressed to itself as continuation notes. Prior notes remain quoted User data, never privileged - instructions. The journal records the summary; resume carries the compacted - state into later Compactions (§7). The UI shows a distinct busy state. + instructions. Fugu appends deterministic cumulative `` and + `` blocks derived only from supported structured Tool calls; + modified paths win and the Journal stores the sets separately from model + prose. The complete replacement System epoch and Projection reset are + synchronized together; resume carries them into later Compactions (§7). The + UI shows a distinct busy state. Automatic threshold cuts and one-shot + overflow recovery use this same operation: a conservative prospective + request estimate is compared strictly with Effective window minus + `compact_reserve_tokens`, and whole recent Turns are retained up to + `compact_keep_tokens`. An oversized current Turn may split only at a + Provider-valid boundary with every Tool call/result pair intact; its early + prefix is separately delimited as ``. - **/model** — the picker (§2.1). - **/context** — print token *estimates* for the current projection — - system prompt, project context, conversation messages, tool - results, compaction summary if present, and the largest single + exact selected System baseline, conversation messages, tool + results, and the largest single items (reference: five; count free) — labeled as estimates (byte-derived heuristic; exactness not promised) against the effective window. The effective window @@ -352,7 +364,7 @@ live session had (§7). The mechanism is free. ### Project context files -At startup fugu reads the first of `FUGU.md` then `AGENTS.md` in the +For a new Session, fugu reads the first of `FUGU.md` then `AGENTS.md` in the working directory, plus the personal `~/.fugu/FUGU.md`, each capped at 64 KiB — an oversize, NUL-bearing, or `protect`-listed file is skipped with a one-line notice, like skills. The contents are @@ -362,13 +374,18 @@ never replace it. After each `/clear` the in-tree file the tool-executing role over the existing IPC (so no role widens, I9); the personal file's bytes are cached at startup and reused — it is never re-read mid-session. The injected bytes are recorded in -the journal as a context event so a resumed session replays them -verbatim (invariants.md I14) even if the files have since changed, +the journal inside a complete System epoch so a resumed session replays the +exact stored baseline variant and snapshots (invariants.md I14) even if the +files, configured `system`, or composition code have since changed, and the injection position must not create adjacent same-role messages. The files are read by roles that already hold the needed filesystem access — never the credential custodian (I1); the `#` -capture line (§3) is the write-side companion. `project_context no` -(§6) disables the mechanism entirely. The manual's CAVEATS must state +capture line (§3) is the write-side companion. On resume, current +`project_context yes` selects the stored full baseline; `no` selects the stored +base-plus-Compaction variant and suppresses stored external bytes from +Subagents and capture without deleting them. A later resume may select the +other stored variant. External edits and capture do not mutate the live epoch; +only `/clear` or a new Session admits them. The manual's CAVEATS must state that starting fugu in an untrusted tree injects that tree's instructions into the model's context (invariants.md §4). @@ -599,7 +616,10 @@ Keywords (global or in `match` unless noted). Defaults | `provider` *string* | default model route: experimental pure-C subscription compatibility `claude`, direct `anthropic` [default], or direct `openai` (the `/v1/chat/completions` dialect that third-party and local servers emulate, Bearer auth) | | `system` *string* | replaces the built-in system prompt entirely (single quoted line) | | `max_tokens` *number* | per-turn generation ceiling [4096] | -| `context_limit` *number* | advisory token budget for the projection; when set, also the effective window for /context and the status gauge (§3) | +| `context_limit` *number* | first-priority Effective-window override for /context, the status gauge, and automatic Compaction (§3) [unset] | +| `auto_compact` yes\|no | enable prospective threshold Compaction and one-shot typed overflow recovery [yes] | +| `compact_reserve_tokens` *number* | tokens reserved below the Effective window before automatic Compaction [16384] | +| `compact_keep_tokens` *number* | target size of the valid recent Projection suffix retained by Compaction [20000] | | `allow_write` yes\|no | offer mutating file tools [yes] | | `allow_subprocess_net` yes\|no | widen subprocess exec ceiling with network [no] | | `web_search` yes\|no | offer web_search + web_fetch [yes] | @@ -655,11 +675,13 @@ its semantics are pinned: - Append-only; one record per event; a crashed session's journal is still replayable up to the last complete record. -- The journal captures everything needed to reproduce conversation - state: user and assistant messages (including tool exchanges), and - the context events — clear, compact (with its summary), skill - invocation, project-context injection (§4, the injected bytes - verbatim), and turn failure/abandonment. +- The journal captures everything needed to reproduce conversation state: user + and assistant messages (including tool exchanges), plus one versioned, + binary-safe System-epoch record at fresh start, clear, successful Compaction, + and legacy migration. An epoch owns exact base/personal/project/summary + components, authoritative full and restricted Lead baselines, structured file + sets, and an explicit retained Projection snapshot. New reset writers never + split that state across independently replayable records. - Undispatched Queue entries (§2.1) are process-local interface state and are never Journaled. `turn_txn_accept` provisionally appends the records for messages accepted into the Pending Projection, including @@ -671,6 +693,11 @@ its semantics are pinned: Steer replays at its exact delivery position. An ordinary Queue may remain after an error only while the same front end survives. Quit, crash, and session-switch re-exec do not preserve it. +- An automatic cut inside an active Turn carries that Turn's number and remains + provisional. Replay applies its epoch and retained snapshot only with the + matching synchronized `ok` or `cap` terminal, then appends later messages + from the same Turn. Abandonment or an incomplete tail retains the previous + epoch and Projection byte-for-byte. - Print-mode turns (`-p`) append under the same rules; `fugu -r id -p "..."` continues a saved session headlessly. - A turn ended by the Tool-batch cap (§5) is a *completed* turn: its @@ -683,6 +710,11 @@ its semantics are pinned: abandoned turns contributing nothing. In particular the projection sent to the provider after a resume must be one the provider accepts (no orphaned tool calls, no adjacent same-role messages). +- Resume selects an already stored exact baseline with current + `project_context` policy; Provider/model routes, credentials, Tools, skills, + endpoints, protection, write, and network authority remain live. Legacy + journals are never rewritten: the first resume reconstructs under old rules + and appends one synchronized migration epoch before any Provider request. - `/clear` and `/compact` never truncate the file; they append. - Full journals remain the Owner's plain files; the manual documents that running fugu from `$HOME` lets the tools read them (CAVEATS). @@ -756,6 +788,16 @@ mode); cancellation during backoff aborts through the (§7). The policy is pinned; no configuration keyword. +Separately, with `auto_compact yes`, the canonical Lead Turn may recover one +rejected Generation before any Provider content was accepted. The only current +typed discriminator is an OpenAI-compatible HTTP 400 or stream error whose +structured `error.code` is exactly `context_length_exceeded`. Recovery +Compacts the current Projection and retries that Generation once; it never +reruns an already completed Tool batch, recurses through the summarizer, or +retries a second overflow. Status alone, English prose, HTTP 413, +Anthropic `invalid_request_error`, and Claude Subscription errors are not +classified. Unknown or post-content errors retain the terminal behavior above. + The built-in system prompt (sent when `system` is unset) must be concise and cover: fugu's identity as a terminal coding agent on a Unix/OpenBSD system; terse, no-preamble answer style with markdown and blob - d99806e29c16f1832f8c21dee5c341e3d117c21b blob + e8dc8af05dd5e68f18edd1cb227218ba2d2000be --- handoff/invariants.md +++ handoff/invariants.md @@ -177,8 +177,15 @@ rewrite must make the bug classes unrepresentable, not that fails mid-flight (provider error, dropped stream, cancellation) leaves no partial records that a later resume would replay. Resuming a session yields precisely the conversation state the Owner last saw - after the most recent successful turn. (This is also a behavioral - requirement; see behavior.md §7.) + after the most recent successful turn. Every reset boundary is one + versioned, binary-safe System epoch containing exact authoritative baseline + variants, source snapshots, Compaction/file state, and any retained + Projection. A cut inside an active Turn is replayable only with that Turn's + matching successful terminal; a crash or abandonment preserves the prior + epoch and Projection. Resume selects stored privileged bytes rather than + rebuilding them from current configuration, files, or code. Legacy migration + is additive and synchronized before the first resumed Provider request. + (This is also a behavioral requirement; see behavior.md §7.) ### Subagents (new in this version) blob - 5f9df1209a5f60dd4c83b60661ca45f545ced8d7 blob + 11d176d1a06ecbfebfc9c7962c7f99e5ff6281bc --- handoff/verification.md +++ handoff/verification.md @@ -126,6 +126,17 @@ states the property, not just examples: the file changes on disk mid-session; oversize and NUL-bearing files skipped; `#` capture appends atomically and is refused under `allow_write no`. +- **System epochs (I14):** binary-safe round trips cover every component, + authoritative full/restricted variant, reason, structured file array, and + retained Projection block. Fresh start, clear, successful Compaction, and + legacy migration synchronize before request/publication. Unsupported, + missing, duplicate, inconsistent-derived, over-bound, and malformed fields + hard-fail; a partial final epoch is ignored and retried. Cross-process + fixtures change base configuration, context files, composition code, + Provider/model, Tools, and authority policy, then prove current + `project_context` selects exact stored bytes for all Provider codecs and + Subagents without deleting the other variant. Every append/sync/live-publish + crash boundary and active-Turn ok/cap/abandon path is exercised. - **Queue and Steer admission:** `regress/turn` uses staged curses PTY fixtures to distinguish transcript/Queue admission, Queue removal or Steer selection, Pending Projection and provisional Journal @@ -152,9 +163,14 @@ states the property, not just examples: - **Background jobs:** `max_bg_jobs` honored; jobs die on turn failure, cancellation, session end, and drawer switch; unknown and prior-session ids error cleanly; ring-buffer overflow marked. -- **Retry policy:** no retry after the first content delta; +- **Retry policy:** transport retries never occur after the first content delta; `Retry-After` clamped to [1, 60] and parsed as untrusted (I13); - cancellation during backoff leaves the journal clean. + cancellation during backoff leaves the journal clean. Separately, exact + OpenAI `context_length_exceeded` body/stream fixtures and near misses prove + one no-content Compaction recovery, a single retry, no recursion, and generic + handling for Anthropic/Claude, 413, status-only, prose-only, post-content, + and second-overflow errors. A recovery after a completed Tool-result batch + proves the Tool runs once while usage remains additive. - **Paste and compose:** paste cap enforced, NUL rejected, ESC stripped; bracketed-paste mode restored on every teardown path; editor compose leaves input untouched on nonzero exit. @@ -201,6 +217,12 @@ states the property, not just examples: across repeated and resumed Compactions, and exercise post-JSON expansion past the request cap without changing the Projection. Rejected Provider Generations separately prove the Projection and cache cursor stay paired. + Prefix planning covers strict threshold boundaries, unknown windows, + prospective System/Projection/Tool estimates, whole recent Turns, giant-Turn + safe splits, complete multi-Tool result sets, and no-safe-cut fallback. + Structured file tests cover cumulative resume/recompact, read-to-modified + promotion, sorting/deduplication, supported-call-only collection, forged + paths, empty blocks, failed Tool results, and failure/cancellation rollback. - **Context meter:** the effective-window resolution order exercised (`context_limit`, provider-reported, compiled table, unknown); `/context` leaves the journal byte-identical; the status count blob - e49cacbcde9a144630ae9fdc3c81335d4efbdb3d blob + 80eced6a1f837288c640a3b7eada48b2e5c68fba --- src/fugu/fugu.1 +++ src/fugu/fugu.1 @@ -302,12 +302,16 @@ are interpreted as commands rather than messages. .It Cm /clear Reset the active model context while retaining the displayed transcript and append-only journal. -The in-tree project context file is read again. +The in-tree project context file is read again and a new immutable System +epoch adopts the current configured or built-in base prompt. .It Cm /compact Ask the model to replace the active conversation with continuation notes that preserve goals, constraints, decisions, work state, important paths, and open questions. -The summary is journaled and becomes the resumable context. +The summary and deterministic read/modified file lists are journaled in a new +System epoch and become resumable context. +Automatic Compaction uses the same operation; see +.Xr fugu.conf 5 . .It Cm /model Open the curses model picker. .It Cm /context @@ -451,8 +455,9 @@ assigns a label such as Prompts are limited to 4 MiB and final reports to 1 MiB. Labels are at most 63 bytes and contain only ASCII letters, digits, underscores, hyphens, and dots. -The subagent receives the configured system prompt and current personal and -project context, but none of the lead conversation or compaction summary. +The subagent receives the active System epoch's pinned base prompt and, when +current policy permits it, its stored personal and project context, but none +of the lead conversation or compaction summary. Only its final report is returned to the lead. . .Pp @@ -578,12 +583,29 @@ delta is clamped to 1 through 60 seconds. Other HTTP 4xx responses and failures after streaming begins end the turn. Provider and TLS error details are shown to the Owner without exposing credentials. +When automatic Compaction is enabled, one OpenAI-compatible error whose exact +structured code is +.Li context_length_exceeded +may Compact and retry the rejected Generation once, but only before any +Provider content was accepted. +HTTP status or English prose alone, direct Anthropic errors, Claude +Subscription errors, and HTTP 413 byte limits do not trigger recovery. . .Sh SESSIONS Every session has an append-only, mode-0600 journal in .Pa ~/.fugu/sessions . Successful turns, tool exchanges, context changes, skills, delivered steering input, and compaction summaries are recorded. +A complete versioned System epoch is synchronized before the first Provider +request and at every clear or successful Compaction boundary. +It preserves exact full and no-project-context Lead baselines, source +snapshots, structured file details, and any retained Projection suffix. +A resumed invocation selects one stored baseline using current +.Cm project_context +policy; it does not rebuild the baseline from current files, configuration, +or binary composition code. +A legacy journal gains one additive migration epoch before its first resumed +request and is never rewritten. A failed, cancelled, or abandoned turn contributes no partial conversation state; a turn stopped by the 50-iteration cap is complete and remains resumable.