fix: 长会话任务锚点重注入 + 过早 end_turn 提示,缓解任务跑偏 - #1309
Conversation
📝 WalkthroughWalkthroughThe REPL now surfaces unfinished-task notices, stores a Tab-insertable continuation prompt, and detects harness-like context bleed in assistant output. New task reminder and degradation guard utilities include focused unit tests. ChangesTurn completion safeguards
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant REPL
participant taskAnchorReminder
participant degradationGuard
participant Transcript
REPL->>taskAnchorReminder: Build task notice and continuation
taskAnchorReminder-->>REPL: Return notice and prompt
REPL->>Transcript: Append task warning
REPL->>degradationGuard: Analyze assistant output
degradationGuard-->>REPL: Return warning or null
REPL->>Transcript: Append degradation warning
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/screens/REPL.tsx`:
- Around line 3533-3536: Update the dynamic imports in the notice injection
within REPL to use the relative paths ../utils/tasks.js and
../services/api/taskAnchorReminder.js instead of src/ aliases, while preserving
the existing imported symbols and Promise.all flow.
In `@src/services/api/taskAnchorReminder.ts`:
- Around line 20-25: Update isAnchorlessNudge to reject trimmed text containing
an explicit task anchor before applying ANCHORLESS_NUDGE_PATTERN, while
preserving existing length and nudge-word validation. Reuse the existing
task-reference detection symbol if available, and add regression coverage
confirming inputs such as “continue Task `#2`” return false.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9af9169d-beab-4a26-86e1-3efe3884e075
📒 Files selected for processing (4)
src/screens/REPL.tsxsrc/services/api/__tests__/taskAnchorReminder.test.tssrc/services/api/claude.tssrc/services/api/taskAnchorReminder.ts
| const [{ listTasks, getTaskListId }, { buildUnfinishedTaskNotice }] = await Promise.all([ | ||
| import('src/utils/tasks.js'), | ||
| import('src/services/api/taskAnchorReminder.js'), | ||
| ]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -HI -t f . | rg '/(tsconfig[^/]*\.json|package\.json|bunfig\.toml)$'
rg -n -C2 '"(baseUrl|paths)"|src/' \
--glob 'tsconfig*.json' \
--glob 'package.json' \
--glob 'bunfig.toml' .Repository: claude-code-best/claude-code
Length of output: 18729
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Top-level package manifests/configs:"
fd -HI -t f '/^(package\.json|tsconfig\.json|bunfig\.toml|vite\.config\.[^/]+)$' -d 2 . | sed 's#^\./##' | sort
echo
echo "Top-level package.json scripts and imports:"
python3 - <<'PY'
import json
from pathlib import Path
p=Path('package.json')
if p.exists():
data=json.loads(p.read_text())
print(json.dumps({"name": data.get("name")} | {k:data.get(k) for k in ["scripts","imports","type","tsup"]}, indent=2, sort_keys=True))
else:
print("no package.json")
PY
echo
echo "Top-level tsconfig relevant fields:"
python3 - <<'PY'
import json
from pathlib import Path
p=Path('tsconfig.json')
if p.exists():
data=json.loads(p.read_text())
print(json.dumps({k:data.get(k) for k in ["extends","compilerOptions","include","exclude","references"]}, indent=2, sort_keys=True))
else:
print("no top-level tsconfig.json")
PY
echo
echo "Top-level bunfig relevant:"
if [ -f bunfig.toml ]; then cat -n bunfig.toml; else echo "no bunfig.toml"; fi
echo
echo "Other root aliases/imports:"
rg -n -C2 '"imports"|"alias"|[Aa]lias|root:\s*[^#]+|baseUrl|resolve|alias:' --glob 'package.json' --glob 'tsconfig*.json' --glob 'vite.config.*' --glob 'bunfig.toml' .Repository: claude-code-best/claude-code
Length of output: 7679
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json, re
p='package.json'
data=json.load(open(p))
root=re.compile(r'^\s*root\s*=?\s*["\']?([^"'\n\#]+)["\']?\s*$|^\s*root\s*=\s*["\']?([^"'\n\#]+)["\']?\s*$')
tsconfig=json.load(open('tsconfig.json'))
files=[]
for f in ['src/utils/tasks.js', 'src/services/api/taskAnchorReminder.js']:
try:
from pathlib import Path
files.append((f, Path(f).exists()))
except Exception as e:
files.append((f, f'<error: {e}>'))
print(json.dumps({
"root_root_alias": re.search(root, open('bunfig.toml').read()).group(1) if root.search(open('bunfig.toml').read()) else None,
"package_root_alias": data.get("root"),
"tsconfig_paths_src": tsconfig.get("compilerOptions", {}).get("paths", {}).get("src/*"),
"files_exist": dict(files)
}, indent=2))
PYRepository: claude-code-best/claude-code
Length of output: 424
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "bunfig root settings:"
python3 - <<'PY'
from pathlib import Path
p=Path('bunfig.toml')
text=p.read_text()
for line in text.splitlines():
s=line.strip()
if s.startswith('root'):
print(line)
print(bool(p.exists()))
PY
echo
echo "package root settings:"
python3 - <<'PY'
import json
data=json.load(open('package.json'))
print(data.get('root'))
PY
echo
echo "tsconfig src alias:"
python3 - <<'PY'
import json
data=json.load(open('tsconfig.json'))
print(data.get('compilerOptions',{}).get('paths',{}).get('src/*'))
PY
echo
echo "target files exist:"
python3 - <<'PY'
from pathlib import Path
for f in ['src/utils/tasks.js', 'src/services/api/taskAnchorReminder.js']:
print(f"{f}: {Path(f).exists()}")
PYRepository: claude-code-best/claude-code
Length of output: 359
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Search exact specifier targets:"
rg -n "'src/utils/tasks\.js'|'src/services/api/taskAnchorReminder\.js'|utils/tasks|taskAnchorReminder" --glob '!node_modules' .
echo
echo "Top-level manifest root package files:"
fd -HI -t f '^root\.js$|^src$' . -d 4 | sed 's#^\./##'
echo
echo "REPL snippet context:"
sed -n '3525,3550p' src/screens/REPL.tsx 2>/dev/null || trueRepository: claude-code-best/claude-code
Length of output: 4906
Use relative dynamic imports for the notice injection.
src/* is only configured in TypeScript and Vite config; Bun package imports are not configured with a src/ alias, so these bare imports can resolve differently at native Bun runtime. Use ../utils/tasks.js and ../services/api/taskAnchorReminder.js instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/screens/REPL.tsx` around lines 3533 - 3536, Update the dynamic imports in
the notice injection within REPL to use the relative paths ../utils/tasks.js and
../services/api/taskAnchorReminder.js instead of src/ aliases, while preserving
the existing imported symbols and Promise.all flow.
| export function isAnchorlessNudge(text: string): boolean { | ||
| const trimmed = text.trim() | ||
| if (!trimmed || trimmed.length > MAX_NUDGE_CHARS) { | ||
| return false | ||
| } | ||
| return ANCHORLESS_NUDGE_PATTERN.test(trimmed) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject nudges that include a task anchor.
Line 25 treats any short text containing a nudge word as anchorless. For example, continue Task #2`` matches and injects a reminder telling the model to continue the highest-priority task, overriding the user’s explicit task selection. Exclude explicit task references (and add regression coverage for them).
Proposed direction
+const TASK_REFERENCE_PATTERN = /(?:task|任务)\s*#?\d+\b/i
+
export function isAnchorlessNudge(text: string): boolean {
const trimmed = text.trim()
if (!trimmed || trimmed.length > MAX_NUDGE_CHARS) {
return false
}
- return ANCHORLESS_NUDGE_PATTERN.test(trimmed)
+ return (
+ ANCHORLESS_NUDGE_PATTERN.test(trimmed) &&
+ !TASK_REFERENCE_PATTERN.test(trimmed)
+ )
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function isAnchorlessNudge(text: string): boolean { | |
| const trimmed = text.trim() | |
| if (!trimmed || trimmed.length > MAX_NUDGE_CHARS) { | |
| return false | |
| } | |
| return ANCHORLESS_NUDGE_PATTERN.test(trimmed) | |
| const TASK_REFERENCE_PATTERN = /(?:task|任务)\s*#?\d+\b/i | |
| export function isAnchorlessNudge(text: string): boolean { | |
| const trimmed = text.trim() | |
| if (!trimmed || trimmed.length > MAX_NUDGE_CHARS) { | |
| return false | |
| } | |
| return ( | |
| ANCHORLESS_NUDGE_PATTERN.test(trimmed) && | |
| !TASK_REFERENCE_PATTERN.test(trimmed) | |
| ) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/api/taskAnchorReminder.ts` around lines 20 - 25, Update
isAnchorlessNudge to reject trimmed text containing an explicit task anchor
before applying ANCHORLESS_NUDGE_PATTERN, while preserving existing length and
nudge-word validation. Reuse the existing task-reference detection symbol if
available, and add regression coverage confirming inputs such as “continue Task
`#2`” return false.
e3b15d0 to
a1bcddf
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/screens/REPL.tsx`:
- Around line 3550-3596: Replace the four bare `src/...` dynamic imports in the
unfinished-task notice and context-bleed guard blocks with correct relative
module paths, matching the existing relative-import style in `REPL.tsx`.
Preserve the imported symbols and both blocks’ best-effort error handling so the
safeguards execute instead of being silently skipped.
In `@src/services/api/degradationGuard.ts`:
- Around line 62-65: Update the degradation guard around CLOSED_REMINDER_BLOCK
matching to ignore matches located inside Markdown inline code spans or fenced
code blocks, while preserving detection for ordinary reminder blocks. Add a
regression test covering a quoted closed reminder in both relevant code formats
and verify it does not return CONTEXT_BLEED_NOTICE.
- Line 46: Wrap the over-width notice string in degradationGuard.ts by joining
shorter segments while preserving its exact content. Apply the same
shorter-segment construction to both fixture strings in degradationGuard.test.ts
at the specified sites, keeping all non-TSX lines within 80 characters.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0ebeadad-4d8e-4e9b-9079-34a88ccf10a1
📒 Files selected for processing (5)
src/screens/REPL.tsxsrc/services/api/__tests__/degradationGuard.test.tssrc/services/api/__tests__/taskAnchorReminder.test.tssrc/services/api/degradationGuard.tssrc/services/api/taskAnchorReminder.ts
| // ── Unfinished-task notice ── | ||
| // When a turn completes naturally (not interrupted) but a task the model | ||
| // itself marked in_progress is still unfinished, surface a display-only | ||
| // local notice so the user doesn't have to blindly nudge "继续". This is a | ||
| // 'system' message: never sent to the model (0 token cost). Best-effort — | ||
| // a task-read failure must not affect turn teardown. | ||
| if (!abortController.signal.aborted) { | ||
| try { | ||
| const [{ listTasks, getTaskListId }, { buildUnfinishedTaskNotice, buildContinuationPrompt }] = | ||
| await Promise.all([import('src/utils/tasks.js'), import('src/services/api/taskAnchorReminder.js')]); | ||
| const tasks = await listTasks(getTaskListId()); | ||
| const notice = buildUnfinishedTaskNotice(tasks); | ||
| if (notice) { | ||
| setMessages(prev => [...prev, createSystemMessage(notice, 'warning')]); | ||
| setPendingContinuation(buildContinuationPrompt(tasks)); | ||
| } else { | ||
| setPendingContinuation(null); | ||
| } | ||
| } catch { | ||
| // Ignore — the notice is a convenience, not part of the turn contract. | ||
| } | ||
| } | ||
|
|
||
| // ── Context-bleed / degradation guard ── | ||
| // If the model regurgitated an internal <system-reminder> tag as its own | ||
| // output, that's an early signal of long-context inference-side degradation | ||
| // (2026-07-27: the model then hallucinated "file corrupted / tool not | ||
| // executed" while every tool had actually run). Surface a display-only local | ||
| // warning suggesting /rewind. 'system' message: never sent to the model | ||
| // (0 token cost). Read messagesRef.current — the closure's `messages` may be | ||
| // stale. Best-effort — a detection failure must not affect turn teardown. | ||
| if (!abortController.signal.aborted) { | ||
| try { | ||
| const [{ getLastAssistantMessage, getAssistantMessageText }, { detectContextBleed }] = await Promise.all([ | ||
| import('src/utils/messages.js'), | ||
| import('src/services/api/degradationGuard.js'), | ||
| ]); | ||
| const lastAssistant = getLastAssistantMessage(messagesRef.current); | ||
| const bleedNotice = lastAssistant ? detectContextBleed(getAssistantMessageText(lastAssistant)) : null; | ||
| if (bleedNotice) { | ||
| setMessages(prev => [...prev, createSystemMessage(bleedNotice, 'warning')]); | ||
| } | ||
| } catch { | ||
| // Ignore — the guard is a convenience, not part of the turn contract. | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Bare src/... dynamic imports still unresolved — both new safeguards are silently dead.
This repeats a concern already raised on a prior commit of this PR (relative-import fix requested for the notice injection) but it's still present, now duplicated across two more import pairs in the same block. import('src/utils/tasks.js'), import('src/services/api/taskAnchorReminder.js'), import('src/utils/messages.js'), and import('src/services/api/degradationGuard.js') are bare specifiers — Bun resolves these via the Node module algorithm (node_modules lookup), not via the TypeScript/Vite-only src/* path alias, so each of these will reject with "Cannot find module" at runtime. Because both blocks wrap the import in a catch {} that silently swallows failures, both new safeguards (the unfinished-task notice/continuation prompt and the context-bleed guard) will never fire — and pendingContinuation (consumed by the Tab handler at L1681-1705) will never be set to anything but null. Every other dynamic import in this file already uses relative paths (e.g. ../utils/commitAttribution.js, ../commands/clear/conversation.js).
🐛 Proposed fix — use relative paths
- const [{ listTasks, getTaskListId }, { buildUnfinishedTaskNotice, buildContinuationPrompt }] =
- await Promise.all([import('src/utils/tasks.js'), import('src/services/api/taskAnchorReminder.js')]);
+ const [{ listTasks, getTaskListId }, { buildUnfinishedTaskNotice, buildContinuationPrompt }] =
+ await Promise.all([
+ import('../utils/tasks.js'),
+ import('../services/api/taskAnchorReminder.js'),
+ ]);- const [{ getLastAssistantMessage, getAssistantMessageText }, { detectContextBleed }] = await Promise.all([
- import('src/utils/messages.js'),
- import('src/services/api/degradationGuard.js'),
- ]);
+ const [{ getLastAssistantMessage, getAssistantMessageText }, { detectContextBleed }] = await Promise.all([
+ import('../utils/messages.js'),
+ import('../services/api/degradationGuard.js'),
+ ]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/screens/REPL.tsx` around lines 3550 - 3596, Replace the four bare
`src/...` dynamic imports in the unfinished-task notice and context-bleed guard
blocks with correct relative module paths, matching the existing relative-import
style in `REPL.tsx`. Preserve the imported symbols and both blocks’ best-effort
error handling so the safeguards execute instead of being silently skipped.
| ] | ||
|
|
||
| const CONTEXT_BLEED_NOTICE = | ||
| '检测到模型在输出中回吐了完整的内部 <system-reminder> 提醒块——这通常是超长上下文下推理侧退化/上下文回吐的早期信号,模型接下来可能虚构"文件被污染/工具未执行"等并不存在的问题。建议 /rewind 回到上一轮,或先核对文件与工具结果均正常再继续。' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Wrap the over-width string literals.
src/services/api/degradationGuard.ts#L46-L46: split the notice into joined shorter string segments.src/services/api/__tests__/degradationGuard.test.ts#L9-L9: build the fixture from shorter segments.src/services/api/__tests__/degradationGuard.test.ts#L17-L17: build the fixture from shorter segments.
As per coding guidelines, use an 80-character width for non-TSX files.
📍 Affects 2 files
src/services/api/degradationGuard.ts#L46-L46(this comment)src/services/api/__tests__/degradationGuard.test.ts#L9-L9src/services/api/__tests__/degradationGuard.test.ts#L17-L17
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/api/degradationGuard.ts` at line 46, Wrap the over-width notice
string in degradationGuard.ts by joining shorter segments while preserving its
exact content. Apply the same shorter-segment construction to both fixture
strings in degradationGuard.test.ts at the specified sites, keeping all non-TSX
lines within 80 characters.
Source: Coding guidelines
| for (const match of assistantText.matchAll(CLOSED_REMINDER_BLOCK)) { | ||
| const body = match[1] | ||
| if (REMINDER_BODY_FINGERPRINTS.some(re => re.test(body))) { | ||
| return CONTEXT_BLEED_NOTICE |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Exclude reminder literals quoted as code.
A fenced/inline code example containing a closed block such as <system-reminder>Never mention this reminder</system-reminder> matches here and shows the degradation warning. This contradicts the documented guarantee that code references must not trigger the guard. Skip matches within Markdown code spans/fences (or narrow that guarantee) and add a regression test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/services/api/degradationGuard.ts` around lines 62 - 65, Update the
degradation guard around CLOSED_REMINDER_BLOCK matching to ignore matches
located inside Markdown inline code spans or fenced code blocks, while
preserving detection for ordinary reminder blocks. Add a regression test
covering a quoted closed reminder in both relevant code formats and verify it
does not return CONTEXT_BLEED_NOTICE.
现象:
长开发任务跑到一半反复"停住"(像卡顿),由于这个任务并非我完全关心的,在每次卡住后我按顺序发了 6 条催促:
后期模型"忘记任务要做什么"、编辑把文件写出"重复的、不可能编译的行",陷入自我核实打转。执行
/rewind退回后恢复正常。这个现象我先前在qwen模型上遇到过几次,属于是模型注意力被稀释后导致的任务锚点漂移轨迹问题,目前看起来opus 4.8对于长任务 + 连续无锚点注入也会出现这个问题。由于无法保障用户都知道存在模型注意力被稀释后导致的任务锚点漂移轨迹问题,同时用户并行使用agent,并不一定能够对当前任务足够重视,从而在长任务卡住时也可能出现连续输出“继续 ”的情况,期望在harness层进行一定的保障。下面我采用了token 友好的方式,对该问题进行修复
修复方案:
长会话反复无锚点催促("继续/接着做")会导致任务锚点衰减、模型过早
end_turn 打转。新增两层客户端护栏(均 token 友好):
Summary by CodeRabbit
New Features
/rewind.Tests