Commit Diff


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
+`<turn-context>` 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
 </conversation>
 ```
 
+For an oversized active Turn, the summarized early prefix follows as a second
+quoted section:
+
+```text
+<turn-context>
+[User]: current Turn input
+
+[Assistant]: early work within this Turn
+</turn-context>
+```
+
 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
+<read-files>
+src/read-only.c
+</read-files>
+
+<modified-files>
+src/changed.c
+</modified-files>
+```
+
+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 `<read-files>` and
+  `<modified-files>` 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 `<turn-context>`.
 - **/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.