commit - bc1fdebe1d123a6a90bcb6e3d48f644a3f5c85c5
commit + 2eaf4e2d3ae39bc43653019764acdb4944b99952
blob - e89fd01592d396767b55de3b8265f5e100f07900
blob + 03682d6dfcd86351c3dddda856bdc83bd82a6852
--- regress/turn/run.sh
+++ regress/turn/run.sh
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 $?)
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" \
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 $?)
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
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) ---------------
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"
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" | \
> "$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"
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"
$([ $? -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
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 $?)
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" $?
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" $?
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
* (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.
"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"
"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"
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));
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));