Skip to content

fix(adapters): classify failed exec wrappers by line scan, not backtracking - #2938

Closed
luvs01 wants to merge 1 commit into
lidge-jun:devfrom
luvs01:agent/exec-failed-wrapper-linear-scan
Closed

fix(adapters): classify failed exec wrappers by line scan, not backtracking#2938
luvs01 wants to merge 1 commit into
lidge-jun:devfrom
luvs01:agent/exec-failed-wrapper-linear-scan

Conversation

@luvs01

@luvs01 luvs01 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace FAILED_EXEC_OUTPUT_REGEX with isFailedEmptyExecWrapper, a line scan that classifies each line exactly once.
  • Fix a latent CRLF gap the rewrite made visible: a Windows failed wrapper was being reported as an empty success.
  • Add regressions for linear-time classification, the CRLF case, and shape-for-shape parity with the previous pattern.

The backtracking

The pattern chained [^\n]*, \n* and \s* groups that can all match the same whitespace. An input beginning with the literal prefix but never completing the match forces the engine to try every split between those groups, so cost grows quadratically with the padding length.

Measured on Bun 1.4.0+34cbb9a40, a single Script failed line followed by padding:

Padding Old regex Line scan
30k spaces ~820 ms ~0.02 ms
60k spaces ~3,115 ms ~0.02 ms

Doubling the input roughly quadrupled the time, which is the quadratic signature. This text arrives inside a tool result, so its shape is not fully under our control.

I first tried to keep a regex and only restructure the quantifiers. Every variant that preserved the accepted shapes still measured in the hundreds of milliseconds to several seconds, because the prefix scan and the trailing whitespace group inevitably overlap. The scan removes the class of problem rather than moving it.

The CRLF bug this exposes

While proving equivalence I found the old pattern accepted Script failed\n\nOutput: but rejected Script failed\r\n\r\nOutput:, because \n* cannot consume the \r. A CRLF wrapper therefore fell through to the empty-success guidance ("NOT lost context", "Do not re-run") and erased the only signal that the cell had failed — precisely the outcome the failure branch exists to prevent.

CRLF blank lines are now separators. That is a deliberate behavior change and the only one in this PR.

Equivalence

Blank-separator semantics are preserved exactly: a line of spaces is not a separator, only a genuinely empty one is, matching what \n* could consume. A differential run over 200,000 generated wrappers (varied fragments, LF and CRLF) found no behavior difference outside the CRLF case above.

Verification

  • Based on current dev@d882caed5eb2.
  • Bun 1.4.0+34cbb9a40, tests/cursor-exec-empty-result.test.ts: 11 passed, 0 failed.
  • Red-proven: with the source reverted, the timing regression fails at 3320 ms and the CRLF regression fails on classification.
  • Bun 1.4.0+34cbb9a40, with cursor-toolresult-normalize, kiro-adapter, and tool-catalog-nudge: 122 passed, 0 failed (507 expectations).
  • bun run typecheck: passed.
  • bun run privacy:scan: passed.
  • The repository-wide suite was intentionally not duplicated locally; hosted CI remains the full-matrix check.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. This removes a denial-of-service amplifier on adapter-visible text and logs nothing new.

Review readiness checklist

  • All CI tests are green on my local testing.
  • I pushed my PR to the latest dev commit.
  • I resolved all correct Codex and CodeRabbit findings.
  • My PR is ready for review.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of failed command results with empty output.
    • Correctly recognizes failure messages across different line-ending formats.
    • Prevents malformed responses from causing slow processing.
    • Preserves existing behavior for valid output and supported failure formats.

…acking

FAILED_EXEC_OUTPUT_REGEX chained `[^\n]*`, `\n*` and `\s*` groups that can all
match the same whitespace. An input that starts with the literal prefix but never
completes the match makes the engine try every split between them, so cost grows
quadratically with the padding length. Measured on Bun 1.4, a single
`Script failed` line padded with 30k spaces took ~820ms and 60k took ~3.1s. The
text comes from a tool result, so its shape is not fully under our control.

Replace the pattern with a line scan that classifies each line exactly once. The
same 60k input costs ~0.02ms.

The rewrite also closes a latent bug it made visible: the old `\n*` accepted an LF
blank line but not a CRLF one, so a Windows wrapper such as
`Script failed\r\n\r\nOutput:` fell through to the empty-SUCCESS guidance and erased
the only signal that the cell had failed. CRLF blank lines are now separators, as
they always should have been.

Every other accepted and rejected shape is unchanged; a differential run over
200k generated wrappers found no behavior difference outside the CRLF case.
@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.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 29, 2026
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed; the review readiness checklist is complete.

Review readiness checklist

  • ✅ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ✅ I resolved all correct Codex and CodeRabbit findings.
  • ✅ My PR is ready for review.

4/4 boxes ticked.

This pull request is already Ready for Review.
The review-ready label marks this PR as ready; review automation runs independently.
Maintainers: @lidge-jun @Ingwannu

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ef57184f-963e-4193-932f-21961c3e76d6

📥 Commits

Reviewing files that changed from the base of the PR and between d882cae and 00a30c3.

📒 Files selected for processing (3)
  • src/adapters/cursor/tool-result-normalize.ts
  • src/adapters/exec-tool-result-normalize.ts
  • tests/cursor-exec-empty-result.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

The change replaces regex-based failed empty-output detection with a shared line-scanning helper. Cursor normalization uses the helper. Tests cover long malformed wrappers, CRLF separators, and previously accepted wrapper shapes.

Changes

Exec wrapper normalization

Layer / File(s) Summary
Shared failed-wrapper classifier
src/adapters/exec-tool-result-normalize.ts
Replaces FAILED_EXEC_OUTPUT_REGEX with isFailedEmptyExecWrapper. The helper recognizes supported empty failed-wrapper shapes, preserves real output, supports CRLF separators, and avoids regex backtracking.
Cursor normalization integration
src/adapters/cursor/tool-result-normalize.ts, tests/cursor-exec-empty-result.test.ts
Cursor normalization paths use the shared helper. Tests cover 60,000-space malformed wrappers, CRLF wrappers, and compatible or non-matching wrapper shapes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 00a30

The PR replaces vulnerable backtracking classification with a linear line scan and correctly handles failed wrappers using CRLF separators; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: lidge-j

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: replacing backtracking-based regex classification with a line scan for failed exec wrappers. It is concise and specific.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 75 / 80

이 PR은 지금 dev HEAD d882caed5 (#2936, Cursor가 다시 그리는 도구 결과에 그보다 앞에 있는 호출만 이름을 붙이게 함) 바로 위에서, 실패한 exec 감싸기 글을 정규식 대신 줄 단위로 분류한다. 미리보기 배포는 계획에 없고, types.ts/config.ts 분할과도 안 겹친다. 손대는 파일은 src/adapters/exec-tool-result-normalize.ts, src/adapters/cursor/tool-result-normalize.ts, tests/cursor-exec-empty-result.test.ts 세 곳뿐이다.

쉽게 말하면 이렇다. 코드 모드 exec 칸이 실패하고 출력이 비면, 도구 결과는 Script failed 로 시작하는 감싸기 글이 온다. 예전에는 FAILED_EXEC_OUTPUT_REGEX 가 이 글을 잡았다. 패턴 안에 줄 안 글자, 여러 줄바꿈, 공백 묶음이 같은 공백을 나눠 먹을 수 있어서, 앞부분만 맞고 끝까지 안 맞는 입력이 오면 엔진이 분할을 전부 다시 시도한다. 작성자가 Bun 1.4 에서 잰 값은 Script failed 뒤에 공백 3만 개면 약 820ms, 6만 개면 약 3.1초다. 길이를 두 배로 하면 시간이 대략 네 배가 된다. 이 글은 도구 결과 안에 들어오므로 모양이 완전히 우리 손이 아니다. 느린 분류는 어댑터가 보이는 글에 대한 서비스 거부 증폭기가 된다.

고치는 방법은 isFailedEmptyExecWrapper 다. Script failed 로 시작하는지 본 뒤, 줄을 CRLF 또는 LF 로 나누고 각 줄을 한 번만 본다. Wall time, Output:, <empty> 순서를 예전과 같이 따른다. 빈 줄만 구분자로 쓴다. 공백만 있는 줄은 구분자가 아니다. 예전 줄바꿈 묶음이 먹던 것과 같다. 작성자가 감싸기 20만 개를 LF/CRLF 섞어 돌려 보니, 아래 CRLF 한 가지를 빼면 예전과 결과가 같다고 했다.

그 CRLF 한 가지가 이 PR의 두 번째 이유다. 예전 패턴은 LF 빈 줄은 먹고 캐리지 리턴은 못 먹는다. 그래서 Script failed\r\n\r\nOutput: 은 실패로 안 잡히고, 빈 성공 안내(NOT lost context, Do not re-run)로 떨어졌다. 실패 신호가 지워지는 바로 그 길이다. Windows 감싸기가 그 모양이다. 지금은 CRLF 빈 줄도 구분자로 센다. 의도한 동작 변경이고, 본문에 그 한 가지만 바뀐다고 적혀 있다.

호출 쪽도 같이 바뀐다. Cursor 쪽 normalizeCursorToolResultText 와 공유 normalizeEmptyExecToolResultText 가 둘 다 새 함수를 쓴다. HEAD 에서 FAILED_EXEC_OUTPUT_REGEX 를 쓰는 곳은 이 두 파일뿐이다. 내보내기를 지워도 다른 모듈이 깨지지 않는다. Kiro 는 이미 normalizeEmptyExecToolResultText 만 쓰므로 같은 분류를 탄다. Computer Use 분기는 예전처럼 isEmptyOrFailedExecWrapper 로 먼저 가고, 그 안에서 실패 팔을 새 함수로 바꾼다. isError 정책은 예전과 같다.

테스트는 세 갈래다. 6만 공백 잘못된 감싸기가 500ms 안에 분류되고 통과로 남는 시간 회귀, CRLF 가 실패 안내를 남기고 빈 성공 문구를 안 남기는 회귀, 예전 패턴이 받던 모양과 거절하던 모양을 하나씩 잠근 동등성이다. 소스를 되돌리면 시간 회귀와 CRLF 회귀가 빨개진다고 했다. 로컬에서 tests/cursor-exec-empty-result.test.ts 11개, 관련 묶음 122개, typecheck, privacy:scan 은 초록이다. 전체 저장소 스위트는 호스트 CI 에 맡긴다고 했다.

라인 - src/adapters/cursor/tool-result-normalize.tstext: - 고친 줄의 들여쓰기가 옆 isError/changed 보다 두 칸 얕다. 문법은 맞다. 포맷터가 다시 맞추면 끝난다. 머지를 막을 정도는 아니다.
경로/심볼 - EMPTY_EXEC_OUTPUT_REGEX - 성공 팔은 아직 예전 정규식이다. 줄 안 글자와 줄바꿈/공백 묶음이 겹칠 여지가 있다. 이번 범위는 실패 팔과 CRLF 뿐이다. 성공 팔을 같은 줄 스캔으로 옮길지는 후속이다.
경로/심볼 - tests 의 500ms 시간 문턱 - 선형 스캔 실제 비용(~0.02ms)보다 훨씬 높고, 예전 이차 비용보다 훨씬 낮다. 바쁜 CI 일꾼에서 드물게 흔들릴 수 있다. 작성자가 그 이유를 주석에 적어 두었다.
경로/심볼 - lines[i]! 비널 단언 - i < lines.length 를 본 뒤에만 쓴다. 안전하다. 다만 줄 스캔이 길어지면 읽기 부담이 생긴다.
경로/심볼 - startsWith("Script failed") - 예전처럼 첫 줄 나머지 글자는 버리고 다음 줄부터 본다. Script failed 가 아닌 실패 문구는 예전에도 안 잡았다. 범위는 같다.

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

  • 본 테스트 매트릭스가 이 헤드에서 초록이 된 뒤에 머지할지
  • EMPTY_EXEC_OUTPUT_REGEX 성공 팔도 같은 줄 스캔으로 옮기는 후속을 바로 열지, 이번엔 실패 팔만 둘지
  • 500ms 시간 문턱을 그대로 둘지, CI 흔들림이 보이면 조금 올릴지
  • 관련 이슈 번호가 안 묶여 있다. 어댑터 빈 결과/실패 감싸기 계열은 이미 dev 에 들어가 있고, 이 ReDoS+CRLF 전용 이슈는 없어도 된다

너의 추천
본 테스트가 이 헤드에서 초록이면 dev 로 머지하세요. 도구 결과에 실릴 수 있는 글에 대한 이차 시간 비용을 없애고, Windows CRLF 실패 감싸기가 빈 성공으로 떨어지던 구멍도 막았다. 동작 변경은 CRLF 구분자 하나뿐이고 동등성 테스트가 잠근다. types/config 분할과 무관하니 close-don't-rebase 대상이 아니다. 들여쓰기 두 칸과 성공 팔 정규식 잔여는 머지를 막을 정도가 아니다. 성공 팔 줄 스캔은 후속 PR 로 열면 된다. 머지 후 스냅샷에 exec-failed-wrapper-linear-scan 정도만 적으면 된다.

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

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed exact head 00a30c3 against dev@d882caed5eb212bf5737d3cb0022dace2dab418e.

The linear-time direction is justified: on the exact old pattern, the PR's 60,000-space malformed input took 5,385.6 ms on this host while isFailedEmptyExecWrapper took 0.083 ms. The focused Cursor/Kiro normalization set passes 102/102 under isolated HOME, OPENCODEX_HOME, and CODEX_HOME. I am requesting changes because the claimed old-shape equivalence is not yet true.

In src/adapters/exec-tool-result-normalize.ts, the Output branch both rejects a previously accepted marker and accepts a previously rejected duplicate marker:

  • "Script failed\nOutput:\n ": old=true, new=false. The old Output:\s* arm accepts indentation before the optional marker; the line scan requires the later line to equal "" byte-for-byte.
  • "Script failed\nOutput: \n": old=false, new=true. Once the inline marker has already been consumed, the unconditional later "" branch accepts a second marker that the old pattern treated as payload/malformed content.

The same results hold with two leading spaces and with a blank line before the duplicate marker. CRLF is the one intended false->true change and remains correct.

Please track whether the Output arm already consumed the marker, accept a later marker with surrounding horizontal whitespace only when the Output arm was bare, and do not consume a second marker. Add both directions to the equivalence regression. This keeps the performance/CRLF fix while preventing the classifier from silently changing which malformed or indented wrappers are erased.

The local typecheck currently reports only the three existing fetch(..., { timeout }) errors in claude-messages.ts and responses/fetch-helpers.ts; this diff does not touch those files. No repository-wide security scan was run.

@lidge-jun

Copy link
Copy Markdown
Owner

I ran an independent differential check on your exact head 00a30c3a76072b2d278118e404e09d43cdc3865b to give @Ingwannu's finding precise inputs rather than leaving you to guess at it. I loaded the old regex classifier straight from origin/dev, ran it and your line scan over a 28-entry corpus of exec-wrapper shapes, and got 10 disagreements.

Four of them are your intended CRLF correction and are improvements:

old=false new=true  "Script failed\r\n\r\n\r\nOutput:"
old=false new=true  "Script failed\r\n\r\n<empty>"
old=false new=true  "Script failed\r\n\r\nOutput:"
old=false new=true  "Script failed\r\nWall time 1s\r\n\r\nOutput:"

The other six are the behavioural divergence, and they flip in opposite directions — which is why a single "is it stricter or looser" framing does not catch them:

old=true  new=false "Script failed\nOutput:\n\n <empty>"
old=true  new=false "Script failed\nOutput:\n  <empty>"
old=true  new=false "Script failed\nOutput:\n <empty>"
old=false new=true  "Script failed\nOutput:\t<empty>\n<empty>"
old=false new=true  "Script failed\nOutput: <empty>\n\n<empty>"
old=false new=true  "Script failed\nOutput: <empty>\n<empty>"

Both directions matter, and the second group is the one I would fix first:

  • Indented <empty> after the marker used to classify and now does not (the first three). Those wrappers stop being recognised as failed-and-empty, so they survive into the transcript instead of being normalised.
  • Duplicate markers used to be rejected and now classify (the last three). A wrapper with a tab or a space after Output: plus a second <empty> now qualifies, which means it can be erased as an empty failed wrapper. Turning a previously-preserved payload into a deletion is the more damaging direction.

The relevant code is the Output branch of isFailedEmptyExecWrapper in src/adapters/exec-tool-result-normalize.ts (around line 49 on your head).

On the rest: the linear-time approach is right and the ReDoS concern is real, so I want this to land. Two things would get it there.

  1. Decide each of those six shapes deliberately — either preserve the old verdict, or state in the PR body that the new verdict is intended and why. "Equivalent except for CRLF" is the claim that is currently false.
  2. Your equivalence test is green while both divergent directions exist, so it is not covering them. Add the six strings above as explicit cases. tests/cursor-exec-empty-result.test.ts is the right home — I confirmed it is live coverage, not vacuous: mutating isFailedEmptyExecWrapper to always return false takes it from 11 pass to 8 pass / 3 fail.

Also worth rebasing: you are 2 commits behind dev (merge base d882caed5).

No changes pushed to your branch — the corpus is yours to use.

@lidge-jun

Copy link
Copy Markdown
Owner

I opened #2945 with your approach and the six divergences fixed, so the ReDoS gets closed either way. Your diagnosis and the linear-scan design are the substance of it and the PR says so.

To be explicit about the options, because I do not want to have talked you out of your own patch:

  • Push the two boundary fixes onto this branch and I will merge yours and close mine. The changes are small: allow whitespace between Output: and the marker so an indented <empty> still classifies, and require that only whitespace follows the marker so a duplicate <empty> still does not.
  • Or let fix(adapters): classify failed exec wrappers by forward scan, not backtracking #2945 land and this one close as superseded, with the credit standing.

Your call. If I hear nothing I will let #2945 go through, but I would rather merge yours.

One measurement you may want for your own notes: the old regex takes 1224 ms on "Script failed" + " ".repeat(60_000) + "\\nY" on this machine. #2945 pins that with a timing test whose strongest mutation is restoring the old regex — it fails at 1224 ms against a 500 ms bound, which proves the assertion measures the defect rather than passing because the runner is fast. Worth having, since a timing test that never demonstrates the slow path is indistinguishable from one that does nothing.

The CRLF half of your patch was a real find that I would not have looked for. A Windows-produced failed wrapper was falling through to the empty-success message, so the model was being told nothing had gone wrong when the cell had failed. That is carried over intact.

lidge-jun added a commit that referenced this pull request Aug 29, 2026
…ktracking

The previous classifier placed two adjacent unbounded whitespace runs over the
same span, so a long whitespace run followed by one non-matching character
forced the engine to retry prefixes.

Replaced with a single forward scan. Every loop advances an index monotonically,
the newline searches cover disjoint forward spans, and the token checks are
fixed-length, so no path retries a prefix.

Two boundaries are load-bearing and both were divergences in an earlier attempt
at this rewrite: whitespace after Output: may precede the marker, so an indented
<empty> still classifies; and only whitespace may follow it, so a duplicate
<empty> still does not. That second one matters most -- classifying it would
replace a real payload with the failed-wrapper guidance, turning a
normalization into data loss.

One intended behaviour change: CRLF blank separators now count. The old pattern
matched only \n, so a Windows-produced failed wrapper never classified and fell
through to the empty-SUCCESS message, telling the model nothing had gone wrong
when the cell had failed. Differential comparison over 63 shapes locally and an
independent 662-shape pairwise corpus found no disagreement outside that CRLF
blank-separator class.

Diagnosis and the linear-scan approach are @luvs01's from #2938; that PR could
not land as written because of the six divergences, which I posted there with
the exact inputs.

The bounded-work test measures process.cpuUsage() rather than elapsed wall time:
performance.now() counts OS descheduling, VM pauses and GC, so a loaded CI
runner can blow a wall-clock budget while the code under test did nothing wrong.
Reverting to the previous classifier spends 1230ms CPU against a 250ms bound.

Four mutations proven red at the intended test each: whitespace restricted to
CR/LF, accepting a second marker, removing CRLF handling, and reverting the
classifier.
lidge-jun added a commit that referenced this pull request Aug 29, 2026
…ktracking (#2945)

The previous classifier placed two adjacent unbounded whitespace runs over the
same span, so a long whitespace run followed by one non-matching character
forced the engine to retry prefixes.

Replaced with a single forward scan. Every loop advances an index monotonically,
the newline searches cover disjoint forward spans, and the token checks are
fixed-length, so no path retries a prefix.

Two boundaries are load-bearing and both were divergences in an earlier attempt
at this rewrite: whitespace after Output: may precede the marker, so an indented
<empty> still classifies; and only whitespace may follow it, so a duplicate
<empty> still does not. That second one matters most -- classifying it would
replace a real payload with the failed-wrapper guidance, turning a
normalization into data loss.

One intended behaviour change: CRLF blank separators now count. The old pattern
matched only \n, so a Windows-produced failed wrapper never classified and fell
through to the empty-SUCCESS message, telling the model nothing had gone wrong
when the cell had failed. Differential comparison over 63 shapes locally and an
independent 662-shape pairwise corpus found no disagreement outside that CRLF
blank-separator class.

Diagnosis and the linear-scan approach are @luvs01's from #2938; that PR could
not land as written because of the six divergences, which I posted there with
the exact inputs.

The bounded-work test measures process.cpuUsage() rather than elapsed wall time:
performance.now() counts OS descheduling, VM pauses and GC, so a loaded CI
runner can blow a wall-clock budget while the code under test did nothing wrong.
Reverting to the previous classifier spends 1230ms CPU against a 250ms bound.

Four mutations proven red at the intended test each: whitespace restricted to
CR/LF, accepting a second marker, removing CRLF handling, and reverting the
classifier.
@lidge-jun

Copy link
Copy Markdown
Owner

The ReDoS is fixed on dev as 8427efe6e80a5ce9488eab7b80b2b1663ab20579 (#2945). The diagnosis and the linear-scan design are yours — you found a real vulnerability in a shared adapter path and the approach you proposed is the one that shipped. Thank you for it.

Closing this as superseded rather than rejected. What differs from your branch is two boundary conditions, nothing about the strategy:

  • whitespace after Output: may precede the marker, so an indented <empty> keeps classifying;
  • only whitespace may follow the marker, so a duplicate <empty> keeps not classifying.

The second is the one I would flag for any future rewrite in this area: accepting it means an exec payload gets replaced by the failed-wrapper guidance, so a normalization turns into data loss. That is a worse outcome than the ReDoS, which is why the six divergences blocked an otherwise correct patch.

Your CRLF fix carried over intact and was a genuinely good catch that I would not have gone looking for — a Windows-produced failed wrapper was falling through to the empty-success message, so the model was told nothing had gone wrong when the cell had failed.

Two things from the review that are worth having if you work on this kind of change again. A differential harness against the old implementation is what makes an equivalence claim checkable — an independently generated 662-shape corpus confirmed no disagreement outside the CRLF class, which is a much stronger statement than a passing test suite. And a timing test measured in wall-clock can pass for the wrong reason on a fast machine and fail for the wrong reason on a loaded one; the merged version measures process.cpuUsage() and is mutation-proven by reverting the old pattern, which spends 1230 ms CPU against a 250 ms bound.

If you would still rather have your own commit in the history, say so — I am happy to revert 8427efe6e and merge your branch with those two boundaries applied instead. The offer is genuine; the fix mattered more to me than whose commit it is.

Please do keep sending these.

@lidge-jun lidge-jun closed this Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working review-ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants