@@ -474,7 +474,16 @@ else
474474 echo " ::warning::Could not read changed files."
475475 FILES=" Could not read changed files."
476476fi
477- { echo ; echo " ## Changed files" ; printf ' %s\n' " $FILES " ; } >> " $CTX "
477+ # Capped, because `--paginate` returns up to GitHub's 3,000-file ceiling and a line per
478+ # file is around 60 bytes -- 180 KB, over the whole budget, from a block with no cap of its
479+ # own. It matters more than its size suggests: this is the block the full diff's omission
480+ # notice sends the reviewer to, so it is the last one that should be able to overflow. An
481+ # eighth of the budget, the same share as the conversation.
482+ FILES_FILE=" ${RUNNER_TEMP} /changed-files.md"
483+ printf ' %s\n' " $FILES " | strip_block_tags > " $FILES_FILE "
484+ cap_file_escaped " $FILES_FILE " $(( PROMPT_BUDGET / 8 )) \
485+ " changed file list truncated; read the rest with gh pr view --json files"
486+ { echo ; echo " ## Changed files" ; cat " $FILES_FILE " ; } >> " $CTX "
478487
479488# The reviewer cannot run tests -- no dependencies are installed and the allowlist
480489# would refuse anyway -- but CI already ran them. Whether they passed is the one
@@ -587,13 +596,30 @@ if [ -n "$LAST_SHA" ] && [ "$LAST_SHA" != "null" ] && [ "$LAST_SHA" != "$HEAD_SH
587596 SINCE_LINES=$( awk ' END {print NR}' " $SINCE_FILE " )
588597 SINCE_USED=$SINCE_LINES
589598 if [ " $SINCE_USED " -gt " $SINCE_MAX " ]; then SINCE_USED=$SINCE_MAX ; fi
599+ # Line-capped and then byte-capped, because a line cap does not bound bytes: at the
600+ # 1.20x this file measures for quote-dense JSON, 2,000 lines of dashboard patch is
601+ # about 120 KB escaped, which is over CTX_MAX_BYTES on its own. This block is written
602+ # *above* the full diff, so without the byte cap it is the block that drives the tail
603+ # cut on a cycle-2+ generated-file PR -- and what the tail cut then removes is the
604+ # full diff's omission notice and the conversation, not the since-diff that spent the
605+ # budget. Half the budget, the same share the threads block gets, so the pair of
606+ # blocks the reviewer cannot re-fetch can never crowd out everything else together.
607+ #
608+ # cap_file_escaped rather than a smaller SINCE_MAX: the prefix is still what this
609+ # block wants, for the reason the full diff below no longer keeps one -- `gh api
610+ # .../compare` is not allowlisted, so a notice in place of this patch leaves the
611+ # reviewer nothing it can fetch instead.
612+ SINCE_CAPPED=" ${RUNNER_TEMP} /since-capped.diff"
613+ head -n " $SINCE_MAX " " $SINCE_FILE " > " $SINCE_CAPPED "
614+ if [ " $SINCE_LINES " -gt " $SINCE_MAX " ]; then
615+ echo " (${NOTICE_LINES} ${SINCE_MAX} of ${SINCE_LINES} lines)" >> " $SINCE_CAPPED "
616+ fi
617+ cap_file_escaped " $SINCE_CAPPED " $(( PROMPT_BUDGET / 2 )) \
618+ " ${NOTICE_LINES} ${SINCE_MAX} of ${SINCE_LINES} lines, then cut to fit the prompt"
590619 {
591620 echo
592621 echo " ## Diff since your last review (${LAST_SHA} to ${HEAD_SHA} )"
593- head -n " $SINCE_MAX " " $SINCE_FILE "
594- if [ " $SINCE_LINES " -gt " $SINCE_MAX " ]; then
595- echo " (${NOTICE_LINES} ${SINCE_MAX} of ${SINCE_LINES} lines)"
596- fi
622+ cat " $SINCE_CAPPED "
597623 } >> " $CTX "
598624 else
599625 {
614640FULL_DIFF_MAX=$(( DIFF_BUDGET_LINES - SINCE_USED))
615641if [ " $FULL_DIFF_MAX " -gt " $DIFF_MAX " ]; then FULL_DIFF_MAX=$DIFF_MAX ; fi
616642
643+ # Issue comments, not the pull comments above: the PR conversation is a separate
644+ # endpoint from the inline review threads, and only the threads were ever passed.
645+ #
646+ # Fetched and capped here, above the full diff, and written below it -- see the write site
647+ # for why the two are separated. Capped in its own file rather than appended straight to
648+ # $CTX, so the cap is on this block and not on the whole context: appending first and
649+ # capping after is the tail cut, which is what put this block's heading off the end of the
650+ # prompt on www.hotdata.dev#332. Stripped before the cap for the reason strip_block_tags
651+ # always runs first -- the substitution grows the text, so a cap on the unstripped file
652+ # bounds a smaller string than the one emitted.
653+ ISSUE_COMMENTS_JQ=' [.[][]] | if length == 0 then "No PR conversation comments." else sort_by(.created_at) | map("--- \(.user.login) at \(.created_at)\n\((.body // "")[0:3000])") | join("\n") end'
654+ if CONVO_JSON=$( gh api " repos/${REPO} /issues/${PR_NUMBER} /comments" --paginate) ; then
655+ CONVO=$( printf ' %s' " $CONVO_JSON " | jq -s -r " $ISSUE_COMMENTS_JQ " 2> /dev/null) \
656+ || CONVO=" Could not parse PR conversation comments."
657+ else
658+ echo " ::warning::Could not read PR conversation comments."
659+ CONVO=" Could not read PR conversation comments."
660+ fi
661+ CONVO_FILE=" ${RUNNER_TEMP} /pr-conversation.md"
662+ printf ' %s\n' " $CONVO " | strip_block_tags > " $CONVO_FILE "
663+ cap_file_escaped " $CONVO_FILE " " $CONVO_MAX_BYTES " \
664+ " ${NOTICE_CONVO} ; read the rest with gh pr view"
665+ # What the block will really cost, heading included, rather than what it was allowed to.
666+ CONVO_BYTES=$(( $(escaped_bytes "$CONVO_FILE ") + 100 ))
667+
617668DIFF_FILE=" ${RUNNER_TEMP} /pr.diff"
618669if fetch_raw " $DIFF_FILE " pr diff " $PR_NUMBER " --repo " $REPO " ; then
619670 DIFF_LINES=$( awk ' END {print NR}' " $DIFF_FILE " )
@@ -624,8 +675,15 @@ if fetch_raw "$DIFF_FILE" pr diff "$PR_NUMBER" --repo "$REPO"; then
624675 # the end of this file strips before it measures.
625676 strip_block_tags < " $CTX " > " ${RUNNER_TEMP} /fit-ctx"
626677 strip_block_tags < " $DIFF_FILE " > " ${RUNNER_TEMP} /fit-diff"
627- DIFF_ALLOWANCE=$(( CTX_MAX_BYTES - $(escaped_bytes "${RUNNER_TEMP} / fit- ctx") - CONVO_MAX_BYTES ))
678+ DIFF_ALLOWANCE=$(( CTX_MAX_BYTES - $(escaped_bytes "${RUNNER_TEMP} / fit- ctx") - CONVO_BYTES ))
628679 DIFF_ESCAPED=$( escaped_bytes " ${RUNNER_TEMP} /fit-diff" )
680+ # Set in the omission branch below and read after the group command. `{ ... } >> file` is
681+ # a group, not a subshell, so the assignment survives -- which is the point: spelling the
682+ # condition a second time thirty lines down leaves the two free to drift, and the drift is
683+ # silent in exactly the direction that matters. The block would render the omission text
684+ # while the annotation said nothing, or the reverse, which is the invisible failure this
685+ # annotation exists to end.
686+ DIFF_OMITTED=0
629687 {
630688 echo
631689 echo " ## Full diff"
@@ -651,6 +709,7 @@ if fetch_raw "$DIFF_FILE" pr diff "$PR_NUMBER" --repo "$REPO"; then
651709 # attempts over those two weeks. The since-diff above keeps its prefix precisely
652710 # because it has no such escape -- `gh api .../compare` is not allowlisted, so
653711 # trading its prefix for a notice would trade partial information for none.
712+ DIFF_OMITTED=1
654713 echo " (${NOTICE_OMITTED} ; ${DIFF_LINES} lines)"
655714 echo
656715 echo " The patch is NOT below. Nothing has been shown to you and nothing has been"
@@ -665,8 +724,7 @@ if fetch_raw "$DIFF_FILE" pr diff "$PR_NUMBER" --repo "$REPO"; then
665724 echo " - Say in your review that the diff was omitted and which files you read."
666725 fi
667726 } >> " $CTX "
668- if [ " $DIFF_LINES " -gt 0 ] \
669- && { [ " $DIFF_LINES " -gt " $FULL_DIFF_MAX " ] || [ " $DIFF_ESCAPED " -gt " $DIFF_ALLOWANCE " ]; }; then
727+ if [ " $DIFF_OMITTED " -eq 1 ]; then
670728 # A notice, not a warning, for the same reason the byte cap below uses one: a
671729 # generated-file PR reaches this legitimately and a warning that cried regression on
672730 # every one of them would stop being read. It exists so an operator reading a thin
@@ -680,25 +738,15 @@ else
680738 { echo ; echo " ## Full diff" ; echo " Could not read the diff; run gh pr diff." ; } >> " $CTX "
681739fi
682740
683- # Issue comments, not the pull comments above: the PR conversation is a separate
684- # endpoint from the inline review threads, and only the threads were ever passed.
685- ISSUE_COMMENTS_JQ=' [.[][]] | if length == 0 then "No PR conversation comments." else sort_by(.created_at) | map("--- \(.user.login) at \(.created_at)\n\((.body // "")[0:3000])") | join("\n") end'
686- if CONVO_JSON=$( gh api " repos/${REPO} /issues/${PR_NUMBER} /comments" --paginate) ; then
687- CONVO=$( printf ' %s' " $CONVO_JSON " | jq -s -r " $ISSUE_COMMENTS_JQ " 2> /dev/null) \
688- || CONVO=" Could not parse PR conversation comments."
689- else
690- echo " ::warning::Could not read PR conversation comments."
691- CONVO=" Could not read PR conversation comments."
692- fi
693- # Capped in its own file rather than appended straight to $CTX, so the cap is on this block
694- # and not on the whole context: appending first and capping after is the tail cut, which is
695- # what put this block's heading off the end of the prompt on www.hotdata.dev#332. Stripped
696- # before the cap for the reason strip_block_tags always runs first -- the substitution grows
697- # the text, so a cap on the unstripped file bounds a smaller string than the one emitted.
698- CONVO_FILE=" ${RUNNER_TEMP} /pr-conversation.md"
699- printf ' %s\n' " $CONVO " | strip_block_tags > " $CONVO_FILE "
700- cap_file_escaped " $CONVO_FILE " " $CONVO_MAX_BYTES " \
701- " ${NOTICE_CONVO} ; read the rest with gh pr view"
741+ # Written last, prepared above the full diff. The ordering argument for writing it last is
742+ # unchanged -- it is the block the reviewer can most afford to lose. But the diff's fit
743+ # decision has to subtract what this block will actually weigh, and a reserve of
744+ # CONVO_MAX_BYTES is not that: the common case is "No PR conversation comments.", 29 bytes,
745+ # and reserving an eighth of the budget against it hands back around 1,200 patch lines that
746+ # nothing will spend. That over-reserve was cheap while the shortfall cost the diff a
747+ # prefix; now it costs the whole block, so it would convert directly into omissions on pull
748+ # requests whose diff would have fit. Fetching here and measuring the capped file makes the
749+ # reserve exact. This endpoint does not depend on the diff, so nothing else moves.
702750{ echo ; echo " ## PR conversation" ; cat " $CONVO_FILE " ; } >> " $CTX "
703751
704752# CTX_MAX_BYTES is derived above, beside the threads cap it is computed from, because the
0 commit comments