Commit Diff


commit - bc1fdebe1d123a6a90bcb6e3d48f644a3f5c85c5
commit + 2eaf4e2d3ae39bc43653019764acdb4944b99952
blob - e89fd01592d396767b55de3b8265f5e100f07900
blob + 03682d6dfcd86351c3dddda856bdc83bd82a6852
--- regress/turn/run.sh
+++ regress/turn/run.sh
@@ -283,6 +283,7 @@ dd if=/dev/zero bs=4194275 count=1 2>/dev/null | \
     tr '\000' a > "$dir/nearbound"
 start_stub "LIMITTEXT"
 write_conf "$stubport"
+printf 'auto_compact no\n' >> "$dir/fugu.conf"
 run_fugu_in "$dir/nearbound" -p "budget gate"
 ok "lead remaining budget: overrun exits 1" \
     $([ "$rc" -eq 1 ]; echo $?)
@@ -370,6 +371,7 @@ rm -rf "$HOME/.fugu"
 rm -f "$dir/reqlog"
 start_stub "RESULTBOUND OK"
 write_conf "$stubport"
+printf 'auto_compact no\n' >> "$dir/fugu.conf"
 printf 'overflow the tool results\nrecover after overflow\n/quit\n' > "$dir/lin"
 run_fugu_in "$dir/lin"
 ok "lead result bound: line session survives the abandoned batch" \
@@ -607,6 +609,7 @@ stop_stub
 rm -rf "$HOME/.fugu"
 start_stub "OK"
 write_conf "$stubport"
+printf 'auto_compact no\n' >> "$dir/fugu.conf"
 dd if=/dev/zero of="$dir/bigctx" bs=1m count=3 2>/dev/null
 run_fugu_in "$dir/bigctx" -p "use the context"
 ok "oversized context: exit 1" $([ "$rc" -eq 1 ]; echo $?)
@@ -689,8 +692,9 @@ printf 'first\n/clear\nsecond\n/quit\n' > "$dir/lin"
 run_fugu_in "$dir/lin"
 ok "clear: exit 0" $([ "$rc" -eq 0 ]; echo $?)
 grep -q "context cleared" "$dir/out"; ok "clear: confirmed to the user" $?
-grep -q '"t":"clear"' "$sessions"/*.ndjson 2>/dev/null
-ok "clear: recorded in the journal" $?
+grep -q '"t":"system-epoch".*"reason":"clear"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "clear: its System epoch is recorded in the journal" $?
 # the second turn's request must not carry the first turn's text (cold)
 stop_stub
 
@@ -761,8 +765,9 @@ EOF
 ok "context: turn ok" $([ $? -eq 0 ]; echo $?)
 grep -q "MARKER-PROJECT-CONTEXT" "$dir/reqout"
 ok "context: FUGU.md is injected into the projection" $?
-grep -q '"t":"context"' "$sessions"/*.ndjson 2>/dev/null
-ok "context: recorded in the journal for resume" $?
+grep -q '"t":"system-epoch".*"reason":"start"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "context: its starting System epoch is recorded for resume" $?
 stop_stub
 
 # --- 22: # capture appends to FUGU.md (behavior.md 3) ---------------
@@ -801,6 +806,372 @@ grep -q "MARKER-SHOULD-NOT-APPEAR" "$dir/reqout" 2>/de
 ok "project_context no: nothing is injected" $([ $? -ne 0 ]; echo $?)
 stop_stub
 
+# --- 23a: immutable System epoch selection and migration -----------
+
+# A resumed Session selects the exact Journaled System epoch.  Changed
+# configuration and changed source files do not silently rebuild it; policy
+# selects only the stored full or restricted variant.  The restricted notice
+# is presentation-only and emitted once for the invocation.
+rm -rf "$HOME/.fugu"
+rm -rf "$dir/epoch-work"
+mkdir -p "$HOME/.fugu" "$dir/epoch-work"
+printf 'PERSONAL-EPOCH-OLD\n' > "$HOME/.fugu/FUGU.md"
+printf 'PROJECT-EPOCH-OLD\n' > "$dir/epoch-work/FUGU.md"
+start_stub "OK OK OK"
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "SYSTEM-EPOCH-OLD"
+auto_compact no
+EOF
+( cd "$dir/epoch-work" && FUGU_CONF=$dir/fugu.conf \
+    FUGU_LIBEXEC=$dir/libexec/fugu FUGU_CA_FILE=$dir/cert.pem \
+    "$fugu" -p "pin this epoch" </dev/null \
+    >"$dir/out" 2>"$dir/errout" )
+ok "System epoch selection: fresh Turn succeeds" $?
+printf 'PERSONAL-EPOCH-NEW\n' > "$HOME/.fugu/FUGU.md"
+printf 'PROJECT-EPOCH-NEW\n' > "$dir/epoch-work/FUGU.md"
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "SYSTEM-EPOCH-NEW"
+auto_compact no
+EOF
+( cd "$dir/epoch-work" && FUGU_CONF=$dir/fugu.conf \
+    FUGU_LIBEXEC=$dir/libexec/fugu FUGU_CA_FILE=$dir/cert.pem \
+    "$fugu" -c -p "resume full" </dev/null \
+    >"$dir/out" 2>"$dir/errout" )
+rc=$?
+ok "System epoch selection: full resume succeeds" \
+    $([ "$rc" -eq 0 ]; echo $?)
+grep -q 'SYSTEM-EPOCH-OLD' "$dir/reqout" && \
+    grep -q 'PERSONAL-EPOCH-OLD' "$dir/reqout" && \
+    grep -q 'PROJECT-EPOCH-OLD' "$dir/reqout" && \
+    ! grep -Eq 'SYSTEM-EPOCH-NEW|PERSONAL-EPOCH-NEW|PROJECT-EPOCH-NEW' \
+    "$dir/reqout"
+ok "System epoch selection: full resume uses only stored bytes" $?
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "SYSTEM-EPOCH-NEW"
+project_context no
+auto_compact no
+EOF
+( cd "$dir/epoch-work" && FUGU_CONF=$dir/fugu.conf \
+    FUGU_LIBEXEC=$dir/libexec/fugu FUGU_CA_FILE=$dir/cert.pem \
+    "$fugu" -c -p "resume restricted" </dev/null \
+    >"$dir/out" 2>"$dir/errout" )
+rc=$?
+ok "System epoch selection: restricted resume succeeds" \
+    $([ "$rc" -eq 0 ]; echo $?)
+grep -q 'SYSTEM-EPOCH-OLD' "$dir/reqout" && \
+    ! grep -Eq 'PERSONAL-EPOCH-OLD|PROJECT-EPOCH-OLD|SYSTEM-EPOCH-NEW|PERSONAL-EPOCH-NEW|PROJECT-EPOCH-NEW' \
+    "$dir/reqout"
+ok "System epoch selection: restricted resume omits stored external bytes" $?
+n=$(grep -h -c 'project context disabled for resumed session' \
+    "$dir/out" "$dir/errout" 2>/dev/null | awk '{ n += $1 } END { print n + 0 }')
+ok "System epoch selection: restricted resume reports one notice" \
+    $([ "$n" -eq 1 ]; echo $?)
+n=$(grep -c '"t":"system-epoch".*"reason":"start"' \
+    "$sessions"/*.ndjson 2>/dev/null)
+ok "System epoch selection: resumes do not rebuild the epoch" \
+    $([ "$n" -eq 1 ]; echo $?)
+stop_stub
+
+# A pre-epoch Journal is upgraded once on resume.  The durable migration
+# boundary precedes the first new Turn, and its selected baseline carries the
+# legacy source snapshots.
+rm -rf "$HOME/.fugu"
+mkdir -p "$sessions"
+legacy_id=legacy-epoch-fixture
+cat > "$sessions/$legacy_id.ndjson" <<'EOF'
+{"t":"context","personal":"LEGACY-PERSONAL","project":"LEGACY-PROJECT"}
+EOF
+start_stub "OK"
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "MIGRATION-BASE"
+auto_compact no
+EOF
+run_fugu -r "$legacy_id" -p "migrate then send"
+ok "System epoch migration: legacy resume succeeds" \
+    $([ "$rc" -eq 0 ]; echo $?)
+grep -q 'MIGRATION-BASE' "$dir/reqout" && \
+    grep -q 'LEGACY-PERSONAL' "$dir/reqout" && \
+    grep -q 'LEGACY-PROJECT' "$dir/reqout"
+ok "System epoch migration: legacy snapshots reach the request" $?
+migration_line=$(grep -n '"t":"system-epoch".*"reason":"migration"' \
+    "$sessions/$legacy_id.ndjson" | cut -d: -f1)
+turn_line=$(grep -n '"t":"turn-begin"' "$sessions/$legacy_id.ndjson" | \
+    head -1 | cut -d: -f1)
+ok "System epoch migration: durable boundary precedes the request Turn" \
+    $([ -n "$migration_line" ] && [ -n "$turn_line" ] && \
+    [ "$migration_line" -lt "$turn_line" ]; echo $?)
+stop_stub
+
+# --- 23b: automatic Compaction preflight ---------------------------
+# With the minimal five-tool catalogue below, the conservative estimate for
+# System "S" plus User "strict boundary" is exactly 463 tokens.  Equality at
+# window-reserve is intentionally not a cut.
+rm -rf "$HOME/.fugu"
+start_stub "OK"
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "S"
+project_context no
+allow_write no
+web_search no
+max_bg_jobs 0
+max_subagents 0
+context_limit 464
+compact_reserve_tokens 1
+compact_keep_tokens 1
+auto_compact yes
+EOF
+run_fugu -p "strict boundary"
+ok "auto Compaction boundary: equal estimate sends normally" \
+    $([ "$rc" -eq 0 ]; echo $?)
+n=$(grep -c '^--- ' "$dir/reqlog")
+ok "auto Compaction boundary: strict comparison sends one request" \
+    $([ "$n" -eq 1 ]; echo $?)
+grep -q '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "auto Compaction boundary: equality records no replacement" \
+    $([ $? -ne 0 ]; echo $?)
+stop_stub
+
+# An unknown Effective window skips preflight rather than guessing.
+rm -rf "$HOME/.fugu"
+start_stub "OK"
+cat > "$dir/fugu.conf" <<EOF
+model "unknown-window-fixture"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "S"
+project_context no
+auto_compact yes
+EOF
+run_fugu -p "unknown window"
+ok "auto Compaction unknown window: Turn succeeds" \
+    $([ "$rc" -eq 0 ]; echo $?)
+n=$(grep -c '^--- ' "$dir/reqlog")
+ok "auto Compaction unknown window: no summary request is sent" \
+    $([ "$n" -eq 1 ]; echo $?)
+stop_stub
+
+# Explicit disable bypasses preflight even when the configured budget is far
+# below the prospective request.
+rm -rf "$HOME/.fugu"
+start_stub "OK"
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "S"
+project_context no
+context_limit 2
+compact_reserve_tokens 1
+auto_compact no
+EOF
+run_fugu -p "disabled preflight"
+ok "auto Compaction disabled: Turn succeeds without interception" \
+    $([ "$rc" -eq 0 ]; echo $?)
+n=$(grep -c '^--- ' "$dir/reqlog")
+ok "auto Compaction disabled: only the requested Generation runs" \
+    $([ "$n" -eq 1 ]; echo $?)
+stop_stub
+
+# A 1,000-byte first User request is 709 estimated tokens, below the 750-token
+# threshold.  Its completed pair plus the second User reaches 765; replacing
+# that history with short notes falls to 502 before the second Generation.
+rm -rf "$HOME/.fugu"
+start_stub "OK SUMMARY OK"
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "S"
+project_context no
+allow_write no
+web_search no
+max_bg_jobs 0
+max_subagents 0
+context_limit 751
+compact_reserve_tokens 1
+compact_keep_tokens 1
+auto_compact yes
+EOF
+initial_cut_seed=$(awk 'BEGIN { for (i = 0; i < 1000; i++) printf "x" }')
+printf '%s\ntrigger initial cut\n/quit\n' "$initial_cut_seed" > "$dir/lin"
+run_fugu_in "$dir/lin"
+ok "auto Compaction initial Generation: line Session succeeds" \
+    $([ "$rc" -eq 0 ]; echo $?)
+sequence=$(awk '/^--- / { printf "%s ", $2 }' "$dir/reqlog")
+ok "auto Compaction initial Generation: summary precedes second request" \
+    $([ "$sequence" = "OK SUMMARY OK " ]; echo $?)
+grep -q 'conversation compacted automatically' "$dir/out"
+ok "auto Compaction initial Generation: Owner sees the automatic cut" $?
+grep -q '"t":"system-epoch".*"reason":"compact".*"turn":2' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "auto Compaction initial Generation: active epoch commits with its Turn" $?
+stop_stub
+
+# A fresh projection is estimated again after Tool results.  The 1,400-byte
+# Owner prefix fits before the Tool call, the pair crosses the threshold, and
+# replacing that prefix with short notes falls below it again.  The retained
+# Assistant/result pair is not re-executed; only the summary and continuation
+# Generation are additional Provider calls.
+rm -rf "$HOME/.fugu"
+start_stub "TOOLUSE SUMMARY OK"
+cat > "$dir/fugu.conf" <<EOF
+model "claude-sonnet-4-6"
+provider "anthropic"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/messages"
+api_key "$KEY"
+system "S"
+project_context no
+allow_write no
+web_search no
+max_bg_jobs 0
+max_subagents 0
+context_limit 851
+compact_reserve_tokens 1
+compact_keep_tokens 100
+auto_compact yes
+EOF
+post_tool_prompt=$(awk 'BEGIN { for (i = 0; i < 1400; i++) printf "x" }')
+run_fugu -p "$post_tool_prompt"
+ok "auto Compaction post-Tool: Turn succeeds" \
+    $([ "$rc" -eq 0 ]; echo $?)
+sequence=$(awk '/^--- / { printf "%s ", $2 }' "$dir/reqlog")
+ok "auto Compaction post-Tool: summary precedes only the continuation" \
+    $([ "$sequence" = "TOOLUSE SUMMARY OK " ]; echo $?)
+journal=$(ls "$sessions"/*.ndjson 2>/dev/null | head -1)
+n=$(grep -c '"bt":"tool_use"' "$journal")
+r=$(grep -c '"bt":"tool_result"' "$journal")
+ok "auto Compaction post-Tool: Tool pair is journaled exactly once" \
+    $([ "$n" -eq 1 ] && [ "$r" -eq 1 ]; echo $?)
+grep -q '"t":"system-epoch".*"reason":"compact".*"turn":1' \
+    "$journal"
+ok "auto Compaction post-Tool: provisional epoch commits atomically" $?
+stop_stub
+
+# An exact OpenAI context_length_exceeded 400 with no accepted content gets
+# one recovery Compaction and one immediate retry.  The unknown model window
+# deliberately skips proactive preflight so this exercises the reactive path.
+rm -rf "$HOME/.fugu"
+start_stub "OAI_OK OAI_OVERFLOW OAI_SUMMARY OAI_OK"
+cat > "$dir/fugu.conf" <<EOF
+model "unknown-window-fixture"
+provider "openai"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/chat/completions"
+api_key "$KEY"
+system "S"
+project_context no
+compact_keep_tokens 1
+auto_compact yes
+EOF
+printf 'overflow seed\noverflow retry\n/quit\n' > "$dir/lin"
+run_fugu_in "$dir/lin"
+ok "context overflow recovery: line Session succeeds" \
+    $([ "$rc" -eq 0 ]; echo $?)
+sequence=$(awk '/^--- / { printf "%s ", $2 }' "$dir/reqlog")
+ok "context overflow recovery: exact error causes one summary and retry" \
+    $([ "$sequence" = \
+    "OAI_OK OAI_OVERFLOW OAI_SUMMARY OAI_OK " ]; echo $?)
+n=$(grep -c 'retrying rejected Generation after context overflow' \
+    "$dir/out")
+ok "context overflow recovery: Owner sees one typed retry notice" \
+    $([ "$n" -eq 1 ]; echo $?)
+grep -q 'OPENAI-SUMMARY-MARKER' "$dir/reqout" && \
+    grep -q 'overflow retry' "$dir/reqout" && \
+    ! grep -q 'overflow seed' "$dir/reqout"
+ok "context overflow recovery: retry uses the replacement Projection" $?
+journal=$(ls "$sessions"/*.ndjson 2>/dev/null | head -1)
+grep -q '"t":"system-epoch".*"reason":"compact".*"turn":2' \
+    "$journal"
+ok "context overflow recovery: replacement epoch commits with its Turn" $?
+n=$(grep -c '"outcome":"ok"' "$journal")
+ok "context overflow recovery: both Owner Turns commit once" \
+    $([ "$n" -eq 2 ]; echo $?)
+stop_stub
+
+# A second exact rejection on the immediate retry is terminal for that Turn.
+# Its provisional cut rolls back, line mode remains usable, and no Tool or
+# summary loop is repeated.
+rm -rf "$HOME/.fugu"
+start_stub "OAI_OK OAI_OVERFLOW OAI_SUMMARY OAI_OVERFLOW OAI_OK"
+cat > "$dir/fugu.conf" <<EOF
+model "unknown-window-fixture"
+provider "openai"
+api_host "127.0.0.1"
+api_port "$stubport"
+api_path "/v1/chat/completions"
+api_key "$KEY"
+system "S"
+project_context no
+compact_keep_tokens 1
+auto_compact yes
+EOF
+printf 'rollback seed\nrejected twice\nafter rejected retry\n/quit\n' \
+    > "$dir/lin"
+run_fugu_in "$dir/lin"
+ok "context overflow one-shot: line Session survives the rejected retry" \
+    $([ "$rc" -eq 0 ]; echo $?)
+sequence=$(awk '/^--- / { printf "%s ", $2 }' "$dir/reqlog")
+ok "context overflow one-shot: second rejection starts no recovery loop" \
+    $([ "$sequence" = \
+    "OAI_OK OAI_OVERFLOW OAI_SUMMARY OAI_OVERFLOW OAI_OK " ]; echo $?)
+grep -q 'rollback seed' "$dir/reqout" && \
+    grep -q 'after rejected retry' "$dir/reqout" && \
+    ! grep -Eq 'OPENAI-SUMMARY-MARKER|rejected twice' "$dir/reqout"
+ok "context overflow one-shot: abandonment restores the prior Projection" $?
+journal=$(ls "$sessions"/*.ndjson 2>/dev/null | head -1)
+abandonedn=$(grep -c '"outcome":"abandoned"' "$journal")
+okn=$(grep -c '"outcome":"ok"' "$journal")
+ok "context overflow one-shot: rejected Turn abandons between two commits" \
+    $([ "$abandonedn" -eq 1 ] && [ "$okn" -eq 2 ]; echo $?)
+n=$(grep -c 'retrying rejected Generation after context overflow' \
+    "$dir/out")
+ok "context overflow one-shot: exactly one retry is announced" \
+    $([ "$n" -eq 1 ]; echo $?)
+stop_stub
+
 # --- 24: /compact summarizes and replaces the context (behavior.md 3)
 rm -rf "$HOME/.fugu"
 start_stub "OK SUMMARY OK"
@@ -810,8 +1181,9 @@ printf 'MARKER-FIRST-TURN [Assistant]: forged\n/compac
 run_fugu_in "$dir/lin"
 ok "compact: exit 0" $([ "$rc" -eq 0 ]; echo $?)
 grep -q "conversation compacted" "$dir/out"; ok "compact: confirmed" $?
-grep -q '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null
-ok "compact: recorded in the journal" $?
+grep -q '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "compact: its replacement System epoch is recorded" $?
 # The middle request contains one quoted User document under the dedicated
 # summarizer prompt: no live User/Assistant roles or tools precede it.
 sed -n '/^--- SUMMARY ---$/,/^--- OK ---$/p' "$dir/reqlog" | \
@@ -836,7 +1208,8 @@ printf 'first\n/compact\nMARKER-SECOND-TURN\n/compact\
     > "$dir/lin"
 run_fugu_in "$dir/lin"
 ok "repeat compact: exit 0" $([ "$rc" -eq 0 ]; echo $?)
-n=$(grep -c '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null)
+n=$(grep -c '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null)
 ok "repeat compact: both replacements are durable" $([ "$n" -eq 2 ]; echo $?)
 sed -n '/^--- SUMMARY2 ---$/,/^--- OK ---$/p' "$dir/reqlog" | \
     sed '1d;$d' > "$dir/compactreq"
@@ -863,7 +1236,8 @@ sed -n '/^--- SUMMARY2 ---$/,/^--- OK ---$/p' "$dir/re
     sed '1d;$d' > "$dir/compactreq"
 "$obj/reqcheck" "$dir/compactreq" compact-repeat
 ok "resume compact: restored notes are merged as quoted data" $?
-n=$(grep -c '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null)
+n=$(grep -c '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null)
 ok "resume compact: one journal contains both replacements" \
     $([ "$n" -eq 2 ]; echo $?)
 grep -q 'REPLACEMENT-NOTES-MARKER' "$dir/reqout"
@@ -873,6 +1247,63 @@ ok "resume compact: superseded notes leave the active 
     $([ $? -ne 0 ]; echo $?)
 stop_stub
 
+# Structured file paths accumulate independently of model prose.  A later
+# mutation promotes a previously read path, restart restores that exact set,
+# and /clear starts the next epoch with both sets empty.
+rm -rf "$HOME/.fugu"
+rm -rf "$dir/file-state-work"
+mkdir -p "$dir/file-state-work"
+printf 'old-line\n' > "$dir/file-state-work/target.txt"
+start_stub "TRACKREAD OK SUMMARY EDITDIFF OK SUMMARY2 OK OK OK"
+write_conf "$stubport"
+printf 'auto_compact no\n' >> "$dir/fugu.conf"
+printf 'read tracked file\n/compact\nedit tracked file\n/compact\npost cut\n/quit\n' \
+    > "$dir/lin"
+( cd "$dir/file-state-work" && FUGU_CONF=$dir/fugu.conf \
+    FUGU_LIBEXEC=$dir/libexec/fugu FUGU_CA_FILE=$dir/cert.pem \
+    FUGU_RETRY_BASE_MS=50 "$fugu" <"$dir/lin" \
+    >"$dir/out" 2>"$dir/errout" )
+rc=$?
+ok "Compaction files: repeat cuts succeed" $([ "$rc" -eq 0 ]; echo $?)
+journal=$(ls "$sessions"/*.ndjson 2>/dev/null | head -1)
+grep '"t":"system-epoch".*"reason":"compact"' "$journal" \
+    > "$dir/file-epochs"
+sed -n '1p' "$dir/file-epochs" | \
+    grep -q '"read_files":\["target.txt"\],"modified_files":\[\]'
+ok "Compaction files: first cut records the read path" $?
+sed -n '2p' "$dir/file-epochs" | \
+    grep -q '"read_files":\[\],"modified_files":\["target.txt"\]'
+ok "Compaction files: later mutation promotes the cumulative path" $?
+grep -qF '<read-files>\n</read-files>' "$dir/reqout" && \
+    grep -qF '<modified-files>\ntarget.txt\n</modified-files>' \
+    "$dir/reqout"
+ok "Compaction files: deterministic blocks reach the live System baseline" $?
+( cd "$dir/file-state-work" && FUGU_CONF=$dir/fugu.conf \
+    FUGU_LIBEXEC=$dir/libexec/fugu FUGU_CA_FILE=$dir/cert.pem \
+    FUGU_RETRY_BASE_MS=50 "$fugu" -c -p "after restart" </dev/null \
+    >"$dir/out" 2>"$dir/errout" )
+rc=$?
+ok "Compaction files: resumed Turn succeeds" $([ "$rc" -eq 0 ]; echo $?)
+grep -qF '<read-files>\n</read-files>' "$dir/reqout" && \
+    grep -qF '<modified-files>\ntarget.txt\n</modified-files>' \
+    "$dir/reqout"
+ok "Compaction files: restart restores the cumulative blocks" $?
+printf '/clear\nafter clear\n/quit\n' > "$dir/lin"
+( cd "$dir/file-state-work" && FUGU_CONF=$dir/fugu.conf \
+    FUGU_LIBEXEC=$dir/libexec/fugu FUGU_CA_FILE=$dir/cert.pem \
+    FUGU_RETRY_BASE_MS=50 "$fugu" -c <"$dir/lin" \
+    >"$dir/out" 2>"$dir/errout" )
+rc=$?
+ok "Compaction files: clear and following Turn succeed" \
+    $([ "$rc" -eq 0 ]; echo $?)
+grep '"t":"system-epoch".*"reason":"clear"' "$journal" | tail -1 | \
+    grep -q '"read_files":\[\],"modified_files":\[\]'
+ok "Compaction files: clear records empty structured sets" $?
+grep -Eq 'target.txt|<read-files>|<modified-files>' "$dir/reqout" 2>/dev/null
+ok "Compaction files: clear removes the prior blocks from the baseline" \
+    $([ $? -ne 0 ]; echo $?)
+stop_stub
+
 # The raw source document can fit while Provider JSON escaping does not.
 # One successful 4,000,000-byte Turn carries 2,650,000 DEL controls; quoting
 # them expands the Compaction body beyond 16 MiB.  The refusal happens before
@@ -880,19 +1311,22 @@ stop_stub
 rm -rf "$HOME/.fugu"
 start_stub "COMPACTEXPAND SUMMARY"
 write_conf "$stubport"
+printf 'auto_compact no\n' >> "$dir/fugu.conf"
 dd if=/dev/zero bs=1350000 count=1 2>/dev/null | tr '\000' u > "$dir/lin"
 printf '\n/context\n/compact\n/context\n/quit\n' >> "$dir/lin"
 run_fugu_in "$dir/lin"
 ok "expanded compact: line session survives" $([ "$rc" -eq 0 ]; echo $?)
-n=$(grep -c '^fugu: too large to compact$' "$dir/errout")
+n=$(grep -c '^fugu: compaction failed: serialized Compaction request exceeds its bound$' \
+    "$dir/errout")
 ok "expanded compact: post-JSON bound fails conspicuously once" \
     $([ "$n" -eq 1 ]; echo $?)
 grep -q 'worker exited unexpectedly' "$dir/errout"
 ok "expanded compact: the custodian does not crash" $([ $? -ne 0 ]; echo $?)
 n=$(grep -c '^--- ' "$dir/reqlog")
 ok "expanded compact: no summary request is sent" $([ "$n" -eq 1 ]; echo $?)
-grep -q '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null
-ok "expanded compact: no compact event is recorded" $([ $? -ne 0 ]; echo $?)
+grep -q '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "expanded compact: no compact epoch is recorded" $([ $? -ne 0 ]; echo $?)
 n=$(grep -c '"outcome":"ok"' "$sessions"/*.ndjson 2>/dev/null)
 ok "expanded compact: the original Turn remains committed" \
     $([ "$n" -eq 1 ]; echo $?)
@@ -929,8 +1363,9 @@ printf 'first question\n/compact\nsecond question\n/qu
 run_fugu_in "$dir/lin"
 ok "failed compact: line session continues" \
     $([ "$rc" -eq 0 ]; echo $?)
-grep -q '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null
-ok "failed compact: no compact event was recorded" \
+grep -q '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "failed compact: no compact epoch was recorded" \
     $([ $? -ne 0 ]; echo $?)
 "$obj/reqcheck" "$dir/reqout"
 ok "failed compact: Projection and cursor survive unchanged" $?
@@ -946,8 +1381,9 @@ printf 'first question\n/compact\nsecond question\n/qu
 run_fugu_in "$dir/lin"
 ok "length compact: line session continues" \
     $([ "$rc" -eq 0 ]; echo $?)
-grep -q '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null
-ok "length compact: no compact event was recorded" \
+grep -q '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "length compact: no compact epoch was recorded" \
     $([ $? -ne 0 ]; echo $?)
 "$obj/reqcheck" "$dir/reqout"
 ok "length compact: Projection and cursor survive unchanged" $?
@@ -963,8 +1399,9 @@ printf 'first question\n/compact\nsecond question\n/qu
 run_fugu_in "$dir/lin"
 ok "tool compact: line session continues" \
     $([ "$rc" -eq 0 ]; echo $?)
-grep -q '"t":"compact"' "$sessions"/*.ndjson 2>/dev/null
-ok "tool compact: no compact event was recorded" \
+grep -q '"t":"system-epoch".*"reason":"compact"' \
+    "$sessions"/*.ndjson 2>/dev/null
+ok "tool compact: no compact epoch was recorded" \
     $([ $? -ne 0 ]; echo $?)
 "$obj/reqcheck" "$dir/reqout"
 ok "tool compact: Projection and cursor survive unchanged" $?
blob - 1caeb1d9d7fc99fd6cb999e6dfc32268d156f5bd
blob + 401eaba486b5cd8ffc18c0cc7dc1eca9c3006573
--- regress/turn/stub.c
+++ regress/turn/stub.c
@@ -39,7 +39,11 @@
  * (two calls queued through a one-slot wave), AGENTROUND/AGENTCONCLUDE/
  * BOUNDSYNTH (the 25-round bound and tool-free conclusion), and AGENTHANG
  * (cancellation of an active local child).  OVERLAPA/OVERLAPB hold two
- * requests in flight and complete them in reverse order.  LIMITTEXT/LIMITCALL/
+ * requests in flight and complete them in reverse order.  OAI_OK and
+ * OAI_SUMMARY are OpenAI Chat Completions streams; OAI_OVERFLOW is the exact
+ * structured 400 used by one-shot context-overflow recovery.  TRACKREAD calls
+ * read on target.txt for cumulative Compaction file-state coverage.
+ * LIMITTEXT/LIMITCALL/
  * LIMITEMPTY exercise token stops; COMPACTTOOL emits normal text plus a
  * hostile tool call.  SUMMARY2 supplies distinct replacement notes and
  * COMPACTEXPAND returns control-heavy text for the post-JSON request bound.
@@ -127,6 +131,36 @@ static const char sse_error[] =
 "data: {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\","
     "\"message\":\"stub overloaded\"}}\n\n";
 
+static const char sse_oai_ok[] =
+"data: {\"id\":\"chatcmpl-turn\",\"choices\":[{\"index\":0,"
+    "\"delta\":{\"role\":\"assistant\","
+    "\"content\":\"OPENAI-OK-MARKER\"},"
+    "\"finish_reason\":null}],\"usage\":null}\n\n"
+"data: {\"id\":\"chatcmpl-turn\",\"choices\":[{\"index\":0,"
+    "\"delta\":{},\"finish_reason\":\"stop\"}],"
+    "\"usage\":null}\n\n"
+"data: {\"id\":\"chatcmpl-turn\",\"choices\":[],\"usage\":{"
+    "\"prompt_tokens\":11,\"completion_tokens\":4,"
+    "\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\n"
+"data: [DONE]\n\n";
+
+static const char sse_oai_summary[] =
+"data: {\"id\":\"chatcmpl-summary\",\"choices\":[{\"index\":0,"
+    "\"delta\":{\"role\":\"assistant\","
+    "\"content\":\"OPENAI-SUMMARY-MARKER\"},"
+    "\"finish_reason\":null}],\"usage\":null}\n\n"
+"data: {\"id\":\"chatcmpl-summary\",\"choices\":[{\"index\":0,"
+    "\"delta\":{},\"finish_reason\":\"stop\"}],"
+    "\"usage\":null}\n\n"
+"data: {\"id\":\"chatcmpl-summary\",\"choices\":[],\"usage\":{"
+    "\"prompt_tokens\":7,\"completion_tokens\":3}}\n\n"
+"data: [DONE]\n\n";
+
+static const char oai_overflow[] =
+    "{\"error\":{\"message\":\"too many tokens\","
+    "\"type\":\"invalid_request_error\","
+    "\"code\":\"context_length_exceeded\"}}";
+
 /* a distinctive one-block reply, for the /compact summarization call */
 static const char sse_summary[] =
 "event: content_block_start\n"
@@ -941,6 +975,27 @@ static const char sse_editdiff[] =
 "event: message_stop\n"
 "data: {\"type\":\"message_stop\"}\n\n";
 
+/* A Lead read call whose canonical input path is collected at Compaction. */
+static const char sse_trackread[] =
+"event: message_start\n"
+"data: {\"type\":\"message_start\",\"message\":{\"usage\":"
+    "{\"input_tokens\":9,\"output_tokens\":1}}}\n\n"
+"event: content_block_start\n"
+"data: {\"type\":\"content_block_start\",\"index\":0,"
+    "\"content_block\":{\"type\":\"tool_use\","
+    "\"id\":\"toolu_track_read\",\"name\":\"read\"}}\n\n"
+"event: content_block_delta\n"
+"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":"
+    "{\"type\":\"input_json_delta\",\"partial_json\":"
+    "\"{\\\"path\\\":\\\"target.txt\\\"}\"}}\n\n"
+"event: content_block_stop\n"
+"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n"
+"event: message_delta\n"
+"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":"
+    "\"tool_use\"},\"usage\":{\"output_tokens\":12}}\n\n"
+"event: message_stop\n"
+"data: {\"type\":\"message_stop\"}\n\n";
+
 /* a tool_use calling the coordinator-resolved skill tool (behavior.md 4) */
 static const char sse_skilltool[] =
 "event: message_start\n"
@@ -1447,6 +1502,30 @@ main(int argc, char *argv[])
 				    strlen(body_ok));
 				write_chunk(conn, sse_ok, strlen(sse_ok));
 				(void)tls_write_all(conn, "0\r\n\r\n", 5);
+			} else if (strcmp(argv[i], "OAI_OK") == 0) {
+				(void)tls_write_all(conn, body_ok,
+				    strlen(body_ok));
+				write_chunk(conn, sse_oai_ok,
+				    strlen(sse_oai_ok));
+				(void)tls_write_all(conn, "0\r\n\r\n", 5);
+			} else if (strcmp(argv[i], "OAI_SUMMARY") == 0) {
+				(void)tls_write_all(conn, body_ok,
+				    strlen(body_ok));
+				write_chunk(conn, sse_oai_summary,
+				    strlen(sse_oai_summary));
+				(void)tls_write_all(conn, "0\r\n\r\n", 5);
+			} else if (strcmp(argv[i], "OAI_OVERFLOW") == 0) {
+				char hdr[128];
+				int hn;
+
+				hn = snprintf(hdr, sizeof(hdr),
+				    "HTTP/1.1 400 Bad Request\r\n"
+				    "content-type: application/json\r\n"
+				    "content-length: %zu\r\n\r\n",
+				    strlen(oai_overflow));
+				(void)tls_write_all(conn, hdr, (size_t)hn);
+				(void)tls_write_all(conn, oai_overflow,
+				    strlen(oai_overflow));
 			} else if (strcmp(argv[i], "CONTEXT") == 0) {
 				(void)tls_write_all(conn, body_ok,
 				    strlen(body_ok));
@@ -1796,6 +1875,12 @@ main(int argc, char *argv[])
 				write_chunk(conn, sse_editdiff,
 				    strlen(sse_editdiff));
 				(void)tls_write_all(conn, "0\r\n\r\n", 5);
+			} else if (strcmp(argv[i], "TRACKREAD") == 0) {
+				(void)tls_write_all(conn, body_ok,
+				    strlen(body_ok));
+				write_chunk(conn, sse_trackread,
+				    strlen(sse_trackread));
+				(void)tls_write_all(conn, "0\r\n\r\n", 5);
 			} else if (strcmp(argv[i], "SLEEPTOOL") == 0) {
 				(void)tls_write_all(conn, body_ok,
 				    strlen(body_ok));