Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion plugins/boxlite-agent-tooling/.agents/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ flowchart TD
| **Incomplete snapshot** | Gate | Evidence could not be captured completely |
| **Dossier** | Auditor | Its assessment: PASS, FAIL, or IN_PROGRESS, with findings |

The gate reads the transcript back from its end to the last real user message
([reader](../lib/verdict-audit-state.sh)). It decodes at most 64 MiB of that final
turn and only hashes earlier history, so a long session never makes evidence
incomplete ([#262](https://github.com/boxlite-ai/agent-tooling/issues/262)).

The runner gives the auditor a [private copy of the snapshot](audit/run-verdict-audit.sh).
Re-entry means running the gate again to validate the returned dossier against the
current session, audit generation, and repository state.
Expand All @@ -123,7 +128,7 @@ current session, audit generation, and repository state.

The regression test follows this sequence:

1. The **65 MiB original transcript** exceeds the reader's limit.
1. The **65 MiB original transcript** holds only NUL bytes, which the reader refuses.
2. The gate saves an **incomplete snapshot** containing `truncated: true` and `records: []`.
3. The test deletes the **original transcript**.
4. The fix preserves the snapshot through retries and reloads it during **gate re-entry**.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ classifier_timeout_seconds=20
classifier_max_bytes=12000
classifier_output_max_bytes=4096
verdict_output_max_bytes=8192
transcript_source_max_bytes=67108864
extractor_transcript_max_bytes=67108864
transcript_turn_max_bytes=262144
transcript_snapshot_truncated=false

Expand Down Expand Up @@ -1623,7 +1623,7 @@ prepare_audit_transcript() {
mkdir -p "$(dirname "$payload_transcript_file")" 2>/dev/null || return 1
if [[ -n "${VERDICT_EXTRACTOR_CMD:-}" ]] \
&& raw_snapshot="$(verdict_audit_read_regular_state \
"$source_path" "$transcript_source_max_bytes" json 2>/dev/null)" \
"$source_path" "$extractor_transcript_max_bytes" json 2>/dev/null)" \
&& [[ "$raw_snapshot" == *$'\n'* ]]; then
raw_body="${raw_snapshot#*$'\n'}"
if ! staged_identity="$(printf '%s' "$raw_body" \
Expand Down Expand Up @@ -1666,7 +1666,7 @@ prepare_audit_transcript() {
fi
elif staged_identity="$(verdict_audit_snapshot_final_turn_identity \
"$source_path" "$staged" \
"$transcript_source_max_bytes" "$transcript_turn_max_bytes")"; then
"$verdict_audit_final_turn_max_bytes" "$transcript_turn_max_bytes")"; then
snapshot_state="$(verdict_audit_read_regular_state \
"$staged" "$transcript_turn_max_bytes" json \
"$staged_identity" 2>/dev/null)" || snapshot_state=""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3069,6 +3069,111 @@ else
fail=$((fail+1)); printf ' FAIL %s (out=%s)\n' "codex turn-level" "$out"
fi; rm -rf "$R"

echo
echo "## Final-turn reader: parsing tracks the turn, not the session"
# The reader scans back from the end of the transcript to the last real user message,
# so earlier history is hashed, never parsed. A whole-file cap made every Stop after a
# session passed 64 MiB fail as unreadable evidence (#262).
R="$(setup)"
perl -e 'my $pad = "x" x 1048000; print qq({"type":"progress","data":"$pad"}\n) for 1 .. 66' \
> "$R/transcript.jsonl"
append_user "$R" "How is the session going?"
append_assistant "$R" "Nothing here is a verdict; just chatting."
long_decision="$(decide "$R")"
long_rung="none"
grep -q 'extract truncated-block' "$R"/.agents/state/verdict-decisions.log* 2>/dev/null \
&& long_rung="extract truncated-block"
if [[ "$long_decision" == allow && "$long_rung" == none ]]; then
pass=$((pass + 1))
printf ' PASS a 66 MiB session with a small final turn is judged, not truncated\n'
else
fail=$((fail + 1))
printf ' FAIL a 66 MiB session with a small final turn is judged, not truncated (decision=%s rung=%s)\n' \
"$long_decision" "$long_rung"
fi
rm -rf "$R"

snapshot_of() { # transcript turn-max -> snapshot JSON; non-zero when the reader refuses
local dir rc=0
dir="$(mktemp -d)"
( source "$REPO_ROOT/.agents/lib/verdict-audit-state.sh"
verdict_audit_snapshot_final_turn "$1" "$dir/snapshot.json" "$2" 262144 ) 2>/dev/null \
&& cat "$dir/snapshot.json" || rc=1
rm -rf "$dir"
return "$rc"
}
sha256_of() { perl -MDigest::SHA -e 'print Digest::SHA->new(256)->addfile($ARGV[0])->hexdigest' "$1"; }

# Lines come back exactly as a forward read gives them: one spanning several 64 KiB
# blocks, a blank line, CRLF, and a last line without its LF.
R="$(mktemp -d)"; t="$R/transcript.jsonl"
long_claim="$(perl -e 'print "y" x 153600')"
jq -nc '{type:"assistant", message:{content:[{type:"text",text:"OLD_TURN_CLAIM"}]}}' > "$t"
jq -nc '{type:"user", message:{content:[{type:"text",text:"go on"}]}}' >> "$t"
jq -nc --arg c "$long_claim" '{type:"assistant", message:{content:[{type:"text",text:$c}]}}' >> "$t"
printf '\n' >> "$t"
printf '%s\r\n' "$(jq -nc '{type:"assistant", message:{content:[{type:"text",text:"crlf"}]}}')" >> "$t"
printf '%s' "$(jq -nc '{type:"assistant", message:{content:[{type:"text",text:"no final LF"}]}}')" >> "$t"
boundary_snapshot="$(snapshot_of "$t" 67108864)" || boundary_snapshot=""
boundary_texts="$(jq -c '[.records[] | select(.kind == "assistant") | .texts[]]' \
<<<"$boundary_snapshot" 2>/dev/null)"
if [[ "$boundary_texts" == "$(jq -nc --arg c "$long_claim" '[$c, "crlf", "no final LF"]')" \
&& "$(jq -r '.source_sha256' <<<"$boundary_snapshot")" == "$(sha256_of "$t")" \
&& "$(jq -r '.source_bytes' <<<"$boundary_snapshot")" == "$(wc -c < "$t" | tr -d ' ')" ]]; then
pass=$((pass + 1))
printf ' PASS block-spanning, blank, CRLF and unterminated lines read as a forward read does\n'
else
fail=$((fail + 1))
printf ' FAIL block-spanning, blank, CRLF and unterminated lines read as a forward read does (texts=%.80s)\n' \
"$boundary_texts"
fi
rm -rf "$R"

# The decode bound covers the final turn, not the file.
R="$(mktemp -d)"; t="$R/transcript.jsonl"
perl -e 'print qq({"type":"progress","data":"), "z" x 100000, qq("}\n)' > "$t"
jq -nc '{type:"user", message:{content:[{type:"text",text:"go on"}]}}' >> "$t"
jq -nc '{type:"assistant", message:{content:[{type:"text",text:"small turn"}]}}' >> "$t"
small_turn_rc=0; snapshot_of "$t" 4096 >/dev/null || small_turn_rc=$?
jq -nc --arg c "$(perl -e 'print "w" x 8192')" \
'{type:"assistant", message:{content:[{type:"text",text:$c}]}}' >> "$t"
large_turn_rc=0; snapshot_of "$t" 4096 >/dev/null || large_turn_rc=$?
if [[ "$small_turn_rc" == 0 && "$large_turn_rc" != 0 ]]; then
pass=$((pass + 1)); printf ' PASS the decode bound covers the final turn, not the file\n'
else
fail=$((fail + 1))
printf ' FAIL the decode bound covers the final turn, not the file (small=%s large=%s)\n' \
"$small_turn_rc" "$large_turn_rc"
fi
rm -rf "$R"

# History is hashed, not parsed: NUL anywhere still fails, as does a malformed final-turn
# line, but a malformed line before the last real user message no longer does.
R="$(mktemp -d)"
for history_case in nul malformed-turn malformed-history; do
t="$R/$history_case.jsonl"
case "$history_case" in
nul) printf '{"type":"progress","data":"a\0b"}\n' > "$t" ;;
malformed-turn) printf '{"type":"progress","data":"ok"}\n' > "$t" ;;
malformed-history) printf '{"type":"progress","data":\n' > "$t" ;;
esac
jq -nc '{type:"user", message:{content:[{type:"text",text:"go on"}]}}' >> "$t"
jq -nc '{type:"assistant", message:{content:[{type:"text",text:"turn"}]}}' >> "$t"
[[ "$history_case" == malformed-turn ]] && printf '{"type":"assistant","message":\n' >> "$t"
done
nul_rc=0; snapshot_of "$R/nul.jsonl" 67108864 >/dev/null || nul_rc=$?
turn_rc=0; snapshot_of "$R/malformed-turn.jsonl" 67108864 >/dev/null || turn_rc=$?
history_rc=0; snapshot_of "$R/malformed-history.jsonl" 67108864 >/dev/null || history_rc=$?
if [[ "$nul_rc" != 0 && "$turn_rc" != 0 && "$history_rc" == 0 ]]; then
pass=$((pass + 1))
printf ' PASS NUL and malformed final-turn lines fail; malformed history is only hashed\n'
else
fail=$((fail + 1))
printf ' FAIL NUL and malformed final-turn lines fail; malformed history is only hashed (nul=%s turn=%s history=%s)\n' \
"$nul_rc" "$turn_rc" "$history_rc"
fi
rm -rf "$R"

echo
echo "## Harness-agnostic extraction: conventions, not schema lists"
R="$(setup)"; write_codex_transcript "$R" "Root cause is the missing bind mount; all tests pass now."
Expand Down Expand Up @@ -4216,8 +4321,8 @@ echo "## a transcript unreadable at one moment is re-read, not judged incomplete
# The live failure: the gate reads the transcript while the harness is still appending
# to it, the extractor's two passes disagree on the bytes, and the hollow snapshot it
# publishes reads downstream as "evidence incomplete" — forcing a FAIL dossier about a
# turn nobody actually looked at. A regular file the extractor refuses on sight (over
# the source ceiling) reproduces that moment deterministically, and a helper swaps in
# turn nobody actually looked at. A regular file the extractor refuses on sight (only
# NUL bytes) reproduces that moment deterministically, and a helper swaps in
# the settled transcript inside the retry window.
R="$(setup)"
write_transcript "$R" "Nothing here is a verdict; just chatting."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ audit_verified_identity=""
audit_success_pending=false
transcript_snapshot_dir=""
transcript_snapshot_path=""
transcript_source_max_bytes=67108864
transcript_turn_max_bytes=262144
previous_dossier_max_bytes=65536
previous_dossier_input_path=""
Expand Down Expand Up @@ -754,9 +753,9 @@ snapshot_transcript() {
fi
elif ! verdict_audit_snapshot_final_turn \
"$transcript_path" "$transcript_snapshot_path" \
"$transcript_source_max_bytes" "$transcript_turn_max_bytes"; then
printf 'run-verdict-audit.sh: transcript must be stable, regular JSONL no larger than %s bytes; auditor was not started.\n' \
"$transcript_source_max_bytes" >&2
"$verdict_audit_final_turn_max_bytes" "$transcript_turn_max_bytes"; then
printf 'run-verdict-audit.sh: transcript must be stable, regular JSONL whose final turn is at most %s bytes; auditor was not started.\n' \
"$verdict_audit_final_turn_max_bytes" >&2
return 1
fi
transcript_path="$transcript_snapshot_path"
Expand Down
6 changes: 3 additions & 3 deletions plugins/boxlite-agent-tooling/.agents/lib/reply-summary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
# Readability thresholds are independent of the shared prompt's prose budget.
reply_summary_paragraph_max_words=80
reply_summary_item_max_words=40
# The same bounds the verdict check puts on the transcript it reads.
reply_summary_transcript_max_bytes=67108864
# The same snapshot bound the verdict check puts on the transcript it reads.
reply_summary_snapshot_max_bytes=262144

# A word is a whitespace-separated token holding a letter or digit, so table pipes,
Expand Down Expand Up @@ -127,8 +126,9 @@ reply_summary_is_dense() { # text
_reply_summary_snapshot() { # transcript-path scratch-dir -> bounded JSON
local snapshot="$2/final-turn.json" identity state
[[ -n "${1-}" && -f "$1" && ! -L "$1" && -d "${2-}" ]] || return 1
# shellcheck disable=SC2154 # Assigned by verdict-audit-state.sh, which callers source.
identity="$(verdict_audit_snapshot_final_turn_identity "$1" "$snapshot" \
"$reply_summary_transcript_max_bytes" "$reply_summary_snapshot_max_bytes" \
"$verdict_audit_final_turn_max_bytes" "$reply_summary_snapshot_max_bytes" \
2>/dev/null)" || return 1
state="$(verdict_audit_read_regular_state "$snapshot" \
"$reply_summary_snapshot_max_bytes" json "$identity" 2>/dev/null)" || return 1
Expand Down
Loading
Loading