Skip to content

세션 도구 결과에 이어보기 커서 — 절단이 손실로 끝나지 않게 (#4854) - #4863

Closed
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:harness-cursor
Closed

세션 도구 결과에 이어보기 커서 — 절단이 손실로 끝나지 않게 (#4854)#4863
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:harness-cursor

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

이슈 #4854 의 처리 결과다.

문제

#3787 S7 의 자원 상한(maxMatches·maxChars)은 컨텍스트 범람을 막지만 이어보기와 짝을
이루지 않았다
. 그래서 호출자는 둘 중 하나만 고를 수 있었다 — 상한을 켜고 뒤쪽을 잃거나,
상한을 끄고 범람하거나.

hwp_doc_search 가 특히 분명했다. 입력이 docId·query·caseSensitive·maxMatches
넷뿐이고 구현이 take(n) 이라 n+1 번째 이후 매치에 도달할 인자 자체가 없었다.

전/후 실측

같은 문서·같은 검색어·같은 창 크기로 똑같은 요청 3건을 두 바이너리에 던졌다. BEFORE 는
src/mcp_serve.rs·src/main.rsupstream/devel 627c8c49a 와 동일한 빌드다.

전후 비교

문서: samples/hwp3-sample.hwp · 검색어 "의" · 전체 매치 276건 · maxMatches=3

[BEFORE (devel 627c8c49a)]
  요청 offset=0   → 매치 ['0:16:25', '0:16:131', '0:18:50']  nextOffset=없음
  요청 offset=3   → 매치 ['0:16:25', '0:16:131', '0:18:50']  nextOffset=없음
  요청 offset=6   → 매치 ['0:16:25', '0:16:131', '0:18:50']  nextOffset=없음
  → 창이 전진하지 않는다. 나머지 273건은 이 도구로 도달할 수단이 없다.

[AFTER (이 PR)]
  요청 offset=0   → 매치 ['0:16:25', '0:16:131', '0:18:50']  nextOffset=3
  요청 offset=3   → 매치 ['0:18:91', '0:18:129', '0:18:152']  nextOffset=6
  요청 offset=6   → 매치 ['0:18:223', '0:18:264', '0:18:415']  nextOffset=9
  → 창이 매 홉 전진한다. 중복 0·누락 0, 전수 276건에 닿는다.

변경

추가 전용(additive)이다. 인자를 생략하면 종전과 바이트까지 같은 봉투가 나간다.

추가 의미
hwp_doc_search 입력 offset (0 이상, 기본 0) 창의 시작 매치 번호
hwp_doc_text 입력 charOffset (0 이상, 기본 0) 선택 쪽 범위를 이어 붙인 좌표의 시작 문자
두 봉투 nextOffset 남은 분량이 있을 때만 실린다
두 봉투 offset·charOffset 에코 0 이 아닐 때만 실린다

설계에서 지킨 것.

  1. nextOffset 의 있음/없음이 유일한 종료 신호다. truncated 는 "이 응답이 전체가
    아니다"라는 뜻이라 마지막 창에서도 true 일 수 있어 종료 판정에 쓸 수 없다 — 스키마 설명에
    이 구별을 명시했다.
  2. 총량은 창과 무관하게 고정이다. totalMatchCount 가 오프셋에 따라 흔들리면 "몇 건 중
    몇 건"이라는 계약이 무너진다.
  3. 오프셋의 0 은 유효값이다. 상한의 0("아무것도 주지 마라")과 뜻이 정반대라
    opt_limit 이 아니라 별도 opt_offset 을 뒀다. -1·2.5·"3" 은 거부한다 — 오타를
    "생략"으로 뭉개면 창이 조용히 처음으로 되돌아가 같은 구간을 무한히 다시 읽는다.
  4. 총량을 넘긴 오프셋은 오류가 아니다. 빈 결과 + nextOffset 없음으로 성공 처리한다.
    여기서 오류를 내면 성실한 호출자의 마지막 한 번이 항상 실패한다.
  5. 쪽 주소를 보존한다. 다 건너뛴 쪽도 pages[] 에서 빼지 않는다 — 빼면 pageCount
    줄어 문서가 실제보다 짧아 보인다([보안 설계] 간접 프롬프트 인젝션 내성 — 문서가 에이전트를 조종하지 못하게 (#3630 보안 확장) #3787 S7 이 절단에서 지킨 규칙과 같은 이유).

검증

게이트 결과
cargo test --test mcp_result_cursor_contract 8 passed (신규)
boundary_integrity_contract 20 passed
mcp_session_query_contract 6 passed
mcp_server_contract 25 passed
mcp_spec_ledger_contract 4 passed
mcp_arg_validation_contract 9 passed
mcp_tool_annotations_contract 5 passed
mcp_next_call_contract 3 passed
cargo clippy --all-targets -- -D warnings 통과 (exit 0)
rustfmt --check (변경 파일) 통과

신규 계약이 못 박는 것 중 핵심 셋.

  • search_offset_reaches_matches_beyond_max_matchesmaxMatches:1 로 창을 넘겨 전수
    276건 도달
    . 종전에는 존재할 수 없던 검사다.
  • search_window_partition_is_exact_for_larger_windows — 창 2·3·7 에서 이어 붙인 결과가
    전수와 정확히 일치(중복 0·누락 0·순서 보존).
  • omitting_offset_keeps_legacy_envelope_byte_identical — 인자 생략 = offset:0, 봉투
    원문이 바이트 동일.

알려진 비용·비목표

  • 비용: page 를 생략한 hwp_doc_text 는 호출마다 전 쪽을 추출하므로 창을 잘게 쪼갤수록
    전체 훑기가 제곱으로 비싸진다. 이 PR 이 만든 비용이 아니라 종전 호출 비용이 홉 수만큼
    곱해지는 것이다. 쪽을 아는 경우 page 로 좁히면 비용은 그 쪽에만 든다.
  • 비목표: 상한의 기본값을 바꾸지 않는다("생략=무제한"은 [보안 설계] 간접 프롬프트 인젝션 내성 — 문서가 에이전트를 조종하지 못하게 (#3630 보안 확장) #3787 S7 의 의도된 계약이다).
    무상태 CLI 표면도 건드리지 않는다. 커서를 불투명 토큰으로 만들지 않는다 — 정수 오프셋이
    결정론적이고 제3자가 손으로 검증할 수 있다.

처리 결과 문서: mydocs/report/task_m100_4854_report.md

Closes #4854

edwardkim#3787 S7 의 자원 상한(maxMatches·maxChars)은 컨텍스트 범람을 막지만 이어보기와
짝을 이루지 않았다. 그래서 호출자는 "상한을 켜고 뒤쪽을 잃거나" "상한을 끄고
범람하거나" 둘 중 하나만 고를 수 있었다.

hwp_doc_search 가 특히 분명했다. 입력이 docId·query·caseSensitive·maxMatches
넷뿐이고 구현이 take(n) 이라 **n+1 번째 이후 매치에 도달할 인자 자체가 없었다**.
실측(samples/hwp3-sample.hwp, "의" 276건, maxMatches=3): offset 을 0·3·6 으로
바꿔도 devel 은 매번 같은 앞 3건을 돌려준다 — 273건이 도달 불가.

추가 전용으로 창을 옮길 수단을 준다.

- hwp_doc_search 에 offset, hwp_doc_text 에 charOffset (둘 다 0 이상, 기본 0)
- 두 봉투에 nextOffset — **남은 분량이 있을 때만** 싣는다. 있음/없음 자체가
  "더 있다"의 신호라 호출자가 총량 산술로 끝을 추론하지 않아도 된다. truncated 는
  "이 응답이 전체가 아니다"라는 뜻이라 마지막 창에서도 true 일 수 있어 종료
  판정에 쓸 수 없다.
- totalMatchCount 는 창과 무관하게 고정 — 흔들리면 "몇 건 중 몇 건" 계약이 무너진다.
- 오프셋의 0 은 유효값이라 opt_limit 이 아닌 opt_offset 을 뒀다. -1·2.5·"3" 은 거부 —
  오타를 "생략"으로 뭉개면 창이 처음으로 되돌아가 같은 구간을 무한히 다시 읽는다.
- 총량을 넘긴 오프셋은 오류가 아니라 빈 결과 + nextOffset 없음. 여기서 오류를 내면
  성실한 호출자의 마지막 한 번이 항상 실패한다.
- 다 건너뛴 쪽도 pages[] 에서 빼지 않는다 — 빼면 pageCount 가 줄어 문서가 실제보다
  짧아 보인다(edwardkim#3787 S7 이 절단에서 지킨 규칙과 같은 이유).

인자를 생략하면 종전과 바이트까지 같은 봉투가 나간다(계약 테스트가 원문 비교로 고정).

검증: 신규 tests/mcp_result_cursor_contract.rs 8본 통과(창 1·2·3·7 에서 이어 붙인
결과가 전수와 정확히 일치 — 중복 0·누락 0·순서 보존), 인접 계약 72본 회귀 통과,
clippy --all-targets -D warnings 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jangster77

Copy link
Copy Markdown
Collaborator

통합 PR #4883(4412546)로 병합 완료했습니다.

원 head와 CI를 다시 확인해 누적 반영했고, 상세 검토·메인터너 보정·검증 근거는 archive 검토 기록에 남겼습니다.

중복 병합을 막기 위해 이 원 PR을 닫습니다. 감사합니다.

@jangster77 jangster77 closed this Aug 15, 2026
@jangster77

Copy link
Copy Markdown
Collaborator

통합 PR #4883(4412546)로 병합 완료했습니다.

원 head와 CI를 다시 확인해 누적 반영했고, 상세 검토·메인터너 보정·검증 근거는 archive 검토 기록에 남겼습니다.

중복 병합을 막기 위해 이 원 PR을 닫습니다. 감사합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants