Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.

fix(audit): 턴 귀속을 응답 그룹의 첫 행에 맞추고 라벨 고착을 없앤다 (gen25) - #376

Merged
ictechgy merged 5 commits into
mainfrom
fix/audit-turn-attribution
Sep 6, 2026
Merged

ictechgy merged 5 commits into
mainfrom
fix/audit-turn-attribution

Conversation

@ictechgy

@ictechgy ictechgy commented Sep 6, 2026

Copy link
Copy Markdown
Owner

머지 방식: merge commit 필수. squash 하면 main 이 깨진다.
이 PR 은 Gate-B 세대(bless + reapply 3 + record)를 담고 있고, 롤백 증명이 그 다섯 커밋을 subject 로 찾아 각각이 main 의 조상인지 확인한다. squash 는 그것들을 합쳐 버린다. 실제로 #351 에서 한 번 일어났고 복구에 #354 가 필요했다.

두 결함, 반드시 함께 고쳐야 한다

라벨 고착. claude_transcript_cost_audit.pylast_result 를 파일 시작에서만 초기화한다. 그래서 새 도구 결과가 없는 턴이 앞 턴의 라벨을 물려받는다. 코드 자신의 주석(:1465-1467)과 사용자용 노트가 반대를 약속하고 있었다.

스냅샷/reducer 불일치. 스냅샷은 accepted 행마다 찍히고 매번 비워지는데, reducer 는 한 응답 그룹의 마지막 행을 고른다. 여러 행으로 나뉜 응답은 이미 비워진 스냅샷을 읽는다.

순서가 결론이다. 라벨만 고치면 지금보다 나빠진다. 다중 행 턴이 전부 no_tool_result 로 재분류되기 때문이다. 이 PR 의 테스트는 라벨을 카운터보다 먼저 단언하므로, 앵커 없이 라벨만 고친 트리는 여기서 실패한다(변이 A1 로 확인).

창 누적을 별도 합산 없이 얻는 방법

검증에서 "바이트 카운터는 창 누적이 필요하다" 는 지적이 나왔다. 스냅샷 초기화를 행마다가 아니라 응답 그룹 경계에서만 하도록 옮겨서 해결했다. 그러면 그룹 첫 행의 스냅샷이 곧 "이전 응답이 시작된 이후 도착한 전부"가 되고, 소비자는 #375 가 노출한 group_first_row_ordinal 한 곳만 읽으면 된다. 그룹 중간에 도착한 tool_result 가 아무 턴에도 귀속되지 않던 문제도 함께 사라진다.

multi_result_turns108 에서 2,150 으로 올라 독립 측정치(~2,137)와 맞는다.

세 갈래 캐시 축

cache_read <= cache_creation 하나로 묶으면 콜드 스타트가 재작성에 섞인다. 실측으로 콜드 스타트가 전체 토큰의 18.75%다. 그래서 셋으로 가른다.

토큰 점유
cold_start (cache_read 없음) 138 42,784,745 18.75%
cache_rewrite (0 < cache_read <= cache_creation) 373 132,584,619 58.10%
incremental 28,204 52,839,755 23.15%

rows 의 행이 아니라 직교 필드다. 행으로 두면 이 버킷이 1위가 되어 covers_all_turns 가 거짓이 되고 aim 권고가 그 버킷을 지목한다. 테스트가 행이 되지 않는 것을 고정한다.

권고도 함께 고쳤다

접두사 쓰기가 우세하면 도구를 지목하지 않는다. 고치기 전에는 no_tool_result 를 지목하며 "규칙 파일과 MCP 카탈로그를 보라" 고 했는데, 그 버킷의 대부분이 접두사 재작성이라 틀린 곳을 가리킨다.

[P1] Most new tokens come from prefix writes, not from tool results
     76.7% of cache_creation tokens landed in 509 turns that wrote the cached
     prefix rather than extending it (372 rewrites, 137 cold starts).

교정된 수치

배포본 이 PR
no_tool_result 점유 ~1.5-3% 71.81%
Bash 점유 75.7% 17.68%
multi_result_turns 108 2,150

CHANGELOG.md:53-55 의 "81.5% of new tokens per turn landing right after a Bash result" 는 별도 PR 에서 정정한다. 정직한 대체값도 측정해 뒀다. incremental 턴만 보면 Bash 가 69.32% 로 여전히 1위이므로 escrow 기본값 결정 자체는 그 근거로 살아남는다.

변이 테스트

변이 결과
A1 앵커 없이 라벨만 수정 잡힘 (다중 행 테스트에서)
A2 라벨 수정 없이 앵커만 잡힘
A3 행마다 초기화로 되돌림 잡힘
A4 cold_start 를 cache_rewrite 에 합침 잡힘

검증

python3 scripts/verify_gate_b_rollback.py   OK (active_generation=gen25, generations=25)
tests.test_gate_b_rollback_proof            Ran 73 OK, exit 0
audit 관련 스위트 3종                        Ran 74 OK, exit 0
scripts/ci_test_gate.py fast                Ran 76 OK, exit 0
실제 3.11 compileall                         exit 0
scripts/sync_plugin_copies.py --check       plugin copies synchronized
scripts/prepublish_check.py --skip-tests    prepublish check: OK
scripts/release_preflight.py                경고 없음

preflight 이 세대 밖 동결 편집을 실제로 잡는지 대조 실험으로 확인했다. 같은 편집을 세대 없이 별도 워크트리에 커밋하면 ::warning::Gate-B frozen paths changed (gen24) 가 나온다.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: dfd82206-eb3f-44e3-bad1-74e7ee80425b


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ictechgy
ictechgy force-pushed the fix/audit-turn-attribution branch from 709a50f to 9849a81 Compare September 6, 2026 11:23
ictechgy and others added 5 commits September 6, 2026 20:44
턴 귀속을 응답 그룹의 첫 행에 맞추고, 새 도구 결과가 없는 턴이 앞 턴의 라벨을
물려받지 않게 한다. 두 결함은 함께 고쳐야 한다. 라벨만 고치면 여러 행으로 나뉜 응답이
전부 no_tool_result 로 잘못 분류돼 지금보다 나빠진다.

스냅샷 초기화를 행마다가 아니라 응답 그룹 경계에서만 하도록 옮겼다. 그래서 그룹 첫 행의
스냅샷이 곧 '이전 응답이 시작된 이후 도착한 전부'가 되고, 소비자는 group_first_row_ordinal
한 곳만 읽으면 된다. 그룹 중간에 도착한 tool_result 가 아무 턴에도 귀속되지 않던 문제도
함께 사라진다.

cache_read 와 cache_creation 의 관계로 턴을 cold_start / cache_rewrite / incremental 셋으로
가르는 직교 필드를 추가했다. rows 의 행이 아니라 별도 필드다. 행으로 두면 그 버킷이 1위가
되어 covers_all_turns 가 거짓이 되고 aim 권고가 그 버킷을 지목한다.

parse_json_line 이 올리던 전역 재귀 한도를 scan() 이 끝날 때 되돌린다. 안 되돌리면 같은
프로세스의 다른 코드가 적대적 깊이를 조용히 받아들인다. 실제로 이 PR 의 새 테스트가 core
파티션에서 audit 스캔을 더 일찍 돌리자, 벤치마크 스트림 파서가 2,000단 중첩을 거부하지
못해 CI 가 두 번 실패했다. 이 변경과 무관한 테스트였다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh
…chor

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh
턴 귀속을 응답 그룹의 첫 행에 맞추고, 새 도구 결과가 없는 턴이 앞 턴의 라벨을
물려받지 않게 한다. 두 결함은 함께 고쳐야 한다. 라벨만 고치면 여러 행으로 나뉜 응답이
전부 no_tool_result 로 잘못 분류돼 지금보다 나빠진다.

스냅샷 초기화를 행마다가 아니라 응답 그룹 경계에서만 하도록 옮겼다. 그래서 그룹 첫 행의
스냅샷이 곧 '이전 응답이 시작된 이후 도착한 전부'가 되고, 소비자는 group_first_row_ordinal
한 곳만 읽으면 된다. 그룹 중간에 도착한 tool_result 가 아무 턴에도 귀속되지 않던 문제도
함께 사라진다.

cache_read 와 cache_creation 의 관계로 턴을 cold_start / cache_rewrite / incremental 셋으로
가르는 직교 필드를 추가했다. rows 의 행이 아니라 별도 필드다. 행으로 두면 그 버킷이 1위가
되어 covers_all_turns 가 거짓이 되고 aim 권고가 그 버킷을 지목한다.

parse_json_line 이 올리던 전역 재귀 한도를 scan() 이 끝날 때 되돌린다. 안 되돌리면 같은
프로세스의 다른 코드가 적대적 깊이를 조용히 받아들인다. 실제로 이 PR 의 새 테스트가 core
파티션에서 audit 스캔을 더 일찍 돌리자, 벤치마크 스트림 파서가 2,000단 중첩을 거부하지
못해 CI 가 두 번 실패했다. 이 변경과 무관한 테스트였다.

지문은 생산 canonicalizer 로 계산했고 기존 레코드와 지문은 건드리지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh
@ictechgy

ictechgy commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

core-pr 실패 추적: 제 변경이 드러낸 잠재 결함이었습니다

core-pr 이 두 번 실패했습니다. 저장소 규칙대로 대조 실험부터 했습니다.

1차 실패 4건 중 3건은 알려진 flaky. adversarial.test_g012_mcp_capabilitiestests/AGENTS.md 표에 등재돼 있고, contract.test_g008_runner 는 핸드오프에 "병렬 게이트에서 실패, 단독 통과" 로 기록돼 있습니다. 재실행에서 셋 다 사라졌습니다.

남은 1건은 재현됐고, 진짜였습니다. test_stream_parser_maps_runtime_decode_limits_to_fixed_codes 가 두 번 다 'success' != 'invalid_stream' 로 실패했습니다. 이 테스트 파일은 claude_transcript_cost_audit, transcript_usage_reducer, by_preceding_tool, group_first_row_ordinal 중 무엇도 참조하지 않고, 로컬에서는 3.11·3.12·3.14 전부, origin/main 에서도 통과합니다.

원인. claude_transcript_cost_audit.pyparse_json_line 이 프로세스 전역 재귀 한도를 1,000 에서 10,000 으로 올리고 되돌리지 않습니다. core 파티션은 1,797개를 한 프로세스에서 돌립니다. 이 PR 이 audit 스캔을 실행하는 테스트 4개를 추가하면서 그 누수가 벤치마크 스트림 테스트보다 먼저 일어났고, 2,000단 중첩 JSON 이 거부되지 않고 파싱됐습니다.

한 프로세스에서 순서대로 돌려 로컬 재현했습니다.

recursionlimit before: 1000
(audit 테스트 실행)
recursionlimit after:  10000
benchmark stream test -> FAIL

조치. 누수를 막았습니다. restored_recursion_limit() 컨텍스트 매니저가 scan() 을 감싸 끝날 때 원래 값으로 되돌립니다. 줄 단위 핫 패스인 parse_json_line 에서 매번 복원하지 않는 이유도 주석에 적었습니다. 회귀 테스트 두 개를 넣었습니다. 스캔 후 한도가 그대로인지, 그리고 예외가 나도 finally 로 복원되는지. 복원을 빼는 변이가 잡히는 것을 확인했습니다.

같은 프로세스에서 audit 테스트 뒤에 벤치마크 스위트를 돌려 191건이 모두 통과합니다.

이 수정은 동결 파일이라 세대를 최종 내용으로 다시 작성했습니다. 지문은 그대로 bd21e82a 입니다.

@ictechgy
ictechgy force-pushed the fix/audit-turn-attribution branch from 9849a81 to 6c3c11b Compare September 6, 2026 11:49
@ictechgy
ictechgy merged commit b0c7352 into main Sep 6, 2026
6 checks passed
@ictechgy
ictechgy deleted the fix/audit-turn-attribution branch September 6, 2026 12:17
ictechgy added a commit that referenced this pull request Sep 6, 2026
* docs: 0.13.0 의 81.5% 주장을 정정하고, 살아남는 근거를 대신 적는다

0.13.0 항목은 Bash escrow 를 기본으로 만든 근거로 "새 토큰의 81.5% 가 Bash 결과
직후에 온다" 를 든다. 그 표에 결함이 둘 있었고 #376 에서 고쳤다. last_result 가
파일 단위로만 초기화돼 새 도구 결과가 없는 턴이 앞 턴의 라벨을 물려받았고,
스냅샷은 accepted 행마다 찍히고 비워지는데 reducer 는 응답 그룹의 마지막 행을
고르므로 여러 행으로 나뉜 응답은 이미 비워진 스냅샷을 읽었다.

교정하면 no_tool_result 약 72%, Bash 약 18% 이고 multi_result_turns 는 약 108 에서
약 2,150 으로 간다.

중요한 것은 escrow 기본값 결정 자체는 다른 근거로 살아남는다는 점이다. 턴은 두 번째
축으로도 갈린다. 캐시 접두사를 다시 썼는가, 이어 붙였는가. 도구가 돌려준 양에 비례해
과금되는 것은 incremental 턴뿐이고, 그 안에서는 Bash 가 여전히 약 69% 를 선행한다.
접두사를 쓴 턴은 cache_creation 의 약 77% 인데 어떤 도구 훅도 줄일 수 없고, 이제
audit 이 cold_start / cache_rewrite / incremental 필드로 따로 보고하며 권고에서도
그 사실을 말한다.

발행된 0.13.0 항목 본문은 고치지 않고 정정을 가리키는 한 줄만 덧붙였다. 이미 나간
릴리스 노트를 조용히 다시 쓰지 않기 위해서다.

수치는 살아 있는 디렉터리에서 나오므로 반올림해 적고 방법은 safety-reference 를
가리킨다. 이 세션 안에서도 같은 스크립트가 76.7 에서 76.9 로 움직였다.

audit 스킬 문서도 고쳤다. "가장 많이 선행한 도구를 앞세우라" 는 지시가 접두사 쓰기가
우세한 코퍼스에서 틀린 곳을 가리킨다. 세 필드를 먼저 읽고 incremental 턴이 도구를
가리킬 때만 도구를 앞세우도록 바꿨다.

검증: 인용한 네 수치를 병합된 main 에서 재측정해 확인.
tests.test_contextguard_stage2_feasibility + standing_cost 13건 OK,
ci_test_gate fast 76건 OK, prepublish_check --skip-tests OK, preflight 경고 없음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh

* docs: GLM 리뷰 blocker — "어떤 도구 훅도 줄일 수 없다" 의 범위를 한정한다

GLM 이 정확히 짚었다. "no tool hook can reduce them" 은 무제한 보편 부정인데
측정된 바 없고 턴 간 효과를 보면 거짓이다. 컨텍스트를 작게 유지하는 훅은 이후
턴의 접두사 크기를 줄이고, 그것이 곧 재작성 비용이다. 이 플러그인의 escrow 가
바로 그런 종류다.

이제 이렇게 적는다. "하나의 도구 결과만 잘라내는 것으로는 그 턴들이 달라지지
않는다. 크기가 그 시점의 컨텍스트 전체를 따라가기 때문이다. 다만 세션 내내
컨텍스트를 작게 유지하는 훅은 나중 재작성이 써야 할 양을 줄인다."

같은 검토에서 나온 나머지도 반영했다.
- "the only ones that bill in proportion to what a tool returned" 의 only 를
  뺐다. incremental 턴에도 도구 결과가 아닌 텍스트가 붙고, 재작성 턴의 새
  접미사에도 도구 반환물이 들어간다.
- "escrow default itself survives" 를 순위 서술로 낮췄다. 한 코퍼스에서의
  순위이지 측정된 절감이 아니라는 것을 문장 안에 적었다.
- SKILL.md 의 지시가 실행 불가능했다. by_preceding_tool 표는 모든 턴을 덮는데
  "incremental 턴이 도구를 가리킬 때만" 을 그 표만으로 판단할 수 없다. 표가
  전 턴 기준임을 밝히고, 접두사 쓰기가 우세하면 표의 1위를 지목하지 말고 그
  사실을 말하라고 바꿨다.

같은 문구가 claude_transcript_cost_audit.py:3365 의 권고 텍스트에도 있다. 그
파일은 Gate-B 동결이라 별도 세대로 고친다.

검증: tests.test_contextguard_stage2_feasibility + standing_cost 13건 OK,
ci_test_gate fast 76건 OK, sync_plugin_copies --check 동기화 확인.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh

* docs: GLM 후속 non-blocker — 발행 항목 처리 방식을 정확히 적는다

"Historical entries below are left as they were published" 라고 썼는데 실제로는
0.13.0 항목에 정정을 가리키는 괄호 문장을 넣었다. 내부 모순이라 "발행 당시의
본문은 그대로 두되, 영향받은 수치 옆에 이 정정을 가리키는 표시를 덧붙였다" 로
고친다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE3AupXcyv64SEuQeTckSh

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant