Skip to content

feat(cursor): repetition breaker for external-model replay priming - #2667

Closed
lidge-jun wants to merge 2 commits into
codex/cursor-gap-8from
codex/cursor-gap-9
Closed

feat(cursor): repetition breaker for external-model replay priming#2667
lidge-jun wants to merge 2 commits into
codex/cursor-gap-8from
codex/cursor-gap-9

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

  • Breaks the identical-response repetition loop on the cursor route (user report: a kimi-k3 app session emitted the byte-identical commentary "원격 ocx 상태를 다시 확인합니다" + the same ssh probe 6+ times in a row; same class as the earlier S2a 180x tool-call loop). Mechanism: external-model full-replay flattens history to text, so N identical rounds replay as N identical assistant lines and PRIME the model to produce the line an (N+1)th time — a self-reinforcing loop.
  • Fix in the external root-replay assembly (protobuf-request.ts): consecutive duplicate assistant/tool-result entries collapse into ONE entry carrying "[note: this exact output was produced N times in a row]", and when any run reaches 3+, a single imperative context note is appended ("Repeating it again is a failure. Take a DIFFERENT action now, or state plainly what is blocking progress." — no denial framing per gap-8 discipline). User messages reset the run; native composer models are untouched; tool call/result pairing is unaffected (structured pairing rides turns[], text replay is presentation).

Stacked on #2665 (codex/cursor-gap-8) — completes the gap-1..gap-9 chain.

Verification

  • bun test tests/cursor-repetition-breaker.test.ts — 5 pass (collapse+marker, note at >=3, no note at 2, distinct entries untouched, user-separated duplicates preserved).
  • bun test tests/cursor-blob.test.ts tests/cursor-request-builder.test.ts tests/cursor-adapter.test.ts tests/cursor-tool-continuation.test.ts tests/cursor-protobuf-events.test.ts — 211 pass 0 fail.
  • bun x tsc --noEmit — clean.

Checklist

  • Focused tests green
  • Typecheck clean
  • Native models unaffected; pairing unaffected
  • Devlog updated (120)

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 26, 2026 05:23
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e5fefaa9-4e4c-4805-b97e-6ebcbbb3b11e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3b9eb5df7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

opts: { messageIndex: number; text?: string },
normalized: string,
): void => {
if (externalModel && lastReplayText !== undefined && normalized === lastReplayText && lastReplayEntry) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Detect interleaved repeated tool rounds

When an external-model tool loop has the normal shape assistant A → tool result R → assistant A → tool result R, each entry overwrites lastReplayText, so this comparison is always A versus R or R versus A. Consequently, neither value is collapsed and maxRunLength never reaches 3, meaning the breaker does nothing for the reported commentary-plus-probe loop; track repetition per role or compare complete rounds, and cover an interleaved assistant/tool-result transcript in the regression test.

Useful? React with 👍 / 👎.

Comment on lines +303 to +306
entries.push(rootBlobCandidate({
role: "user",
content: [{ type: "text", text: `[context note] The transcript above contains the same output repeated ${maxRunLength} times in a row. Repeating it again is a failure. Take a DIFFERENT action now, or state plainly what is blocking progress.` }],
}, "user", {}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the active tool result at the replay tail

When any repetition run reached three and the current request ends in a tool result, appending this synthetic user root after that result prevents the later trailing-toolResult scan from recognizing the active block. Under the 192-blob or 512 KiB replay cap, the current result is then treated as ordinary prior history and can be dropped instead of preserved or truncated, while the continuation action still tells the model that the result is in history; insert the note before the active block or make pruning explicitly retain both.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Comment on lines +302 to +305
if (externalModel && maxRunLength >= 3) {
entries.push(rootBlobCandidate({
role: "user",
content: [{ type: "text", text: `[context note] The transcript above contains the same output repeated ${maxRunLength} times in a row. Repeating it again is a failure. Take a DIFFERENT action now, or state plainly what is blocking progress.` }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop carrying a resolved repetition warning forward

Because maxRunLength records the maximum over the entire raw history and is never reset when a user message starts a new turn, one old three-output run causes this imperative strategy-change note to be appended to every subsequent request in the conversation. After the user changes tasks and the model makes unrelated progress—and even after replay pruning removes the repeated entries—the prompt still orders the model to take a different action; base the warning on the trailing/current run that actually survives selection rather than any historical run.

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 58 / 80

설명

이 풀 리퀘스트는 Cursor 바깥 모델이 같은 답을 여러 번 이어서 낼 때, 그 기록이 또 같은 답을 부르지 않게 접습니다. 지금 current dev HEAD 01b5da9f5의 src/adapters/cursor/protobuf-request.ts 197줄부터 338줄 rootPromptMessages는 예전 대화를 글자 덩어리로 다시 만듭니다. 216줄 isCursorExternalWireModel이 참이면 바깥 모델입니다. 221줄부터 263줄 반복문은 사용자, 도우미, 도구 결과를 각각 한 줄로 넣습니다. 같은 도우미 글자가 다섯 번 있어도 다섯 줄이 그대로 다시 재생됩니다. 그래서 모델이 여섯 번째도 같은 줄을 쓰기 쉽습니다. 본문이 말한 kimi-k3의 "원격 ocx 상태를 다시 확인합니다" 반복과 같은 종류입니다.

고침은 그 반복문 바로 앞에 pushDeduped를 둡니다. 바깥 모델일 때만, 바로 앞 줄과 글자가 같으면 새 줄을 넣지 않고 하나 남은 줄에 "N times in a row" 표시를 덮어씁니다. 사용자 말이 나오면 lastReplayText와 lastReplayEntry를 지웁니다. 그래서 사용자 말로 떨어진 같은 글자는 접지 않습니다. 한 번의 이어진 반복이 3번 이상이면 끝에 짧은 명령 한 줄을 붙입니다. 네이티브 composer는 discovery.ts 169-171줄에서 바깥 모델이 아니라서 접히지 않습니다. 도구 호출과 결과의 구조 짝 turns[]는 그대로 둡니다.

테스트는 tests/cursor-repetition-breaker.test.ts 다섯 개입니다. 모델 아이디 grok-4.6-high는 네이티브 composer가 아니므로 바깥 모델입니다. 다섯 번 반복은 한 줄로 접히고, 두 번은 접히되 명령 줄은 없고, 다른 글자는 그대로 두고, 사용자 말로 떨어진 같은 글자는 두 줄로 남습니다. 이 검사는 분명합니다. 라이브 증명은 데브로그에만 있고, 제품 코드가 아닙니다.

다만 이 PR의 베이스는 #2665의 브랜치 codex/cursor-gap-8입니다. 지금 dev 01b5da9 위에 혼자 합치면 #2650부터 #2665까지 부모 스택이 없습니다. collapsedRepeats는 숫자를 세고 아무 데도 쓰지 않습니다. 덮어쓸 때 entries.indexOf(lastReplayEntry)로 위치를 찾습니다. 명령 줄은 가짜 사용자 말이고 messageIndex가 없습니다. 같은 파일 265줄부터 330줄 바깥 모델 히스토리 자르기는 뒤에서부터 사용자 턴을 남깁니다. 잘리면 접힌 표시만 남거나 명령 줄만 남을 수 있습니다. composer-2.5가 접히지 않는지, 도구 결과 접힘이 맞는지는 테스트가 없습니다.

타입은 src/types.ts를 키우지 않습니다. 타입 분리 캠페인 때문에 닫을 이유는 없습니다. 미리보기 배포는 계획에 없습니다. 데브로그 120_repetition_breaker.md는 착지 대상이 아닙니다.

src/adapters/cursor/protobuf-request.ts:221 - 지금 HEAD는 같은 도우미 줄을 N번 그대로 다시 붙여 다음 답을 같은 말로 부릅니다
경로/심볼 - 베이스가 #2665라 현재 dev 01b5da9f5에 혼자 합칠 수 없습니다
경로/심볼 - collapsedRepeats는 세고 쓰지 않습니다
경로/심볼 - 전략 노트는 가짜 사용자 말이라 히스토리 자르기 뒤 접힌 표시와 노트가 따로 떨어질 수 있습니다
tests/cursor-repetition-breaker.test.ts - composer 네이티브가 접히지 않는지와 toolResult 접힘을 검사하지 않습니다

메인테이너의 판단이 필요한 지점

  • #2650부터 #2665가 먼저 들어간 뒤에만 이 PR을 볼지 정해야 합니다
  • 반복 명령을 가짜 사용자 말로 넣을지, 시스템이나 컨텍스트 칸으로 넣을지 정해야 합니다
  • 도구 결과도 도우미 글자와 같은 접기 칸에 둘지 정해야 합니다

너의 추천

#2650부터 #2665 스택이 먼저 들어간 뒤에 이 PR을 마지막에 합치세요. 지금 dev에 직접 합치지 마세요. 합치기 전에 collapsedRepeats를 지우거나 쓰게 하고, composer-2.5가 접히지 않는 테스트와 도구 결과 접힘 테스트를 넣으세요. 미리보기 배포는 계획에 없습니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun deleted the branch codex/cursor-gap-8 August 26, 2026 06:09
@lidge-jun lidge-jun closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant