fix: auto-re-read stale files in agent loop instead of failing (#450)#456
fix: auto-re-read stale files in agent loop instead of failing (#450)#456anandgupta42 wants to merge 11 commits intomainfrom
Conversation
… icon semantics, field validation, pre-delivery checklist Six improvements derived from session learnings — all general, none task-specific: - component-guide: lazy chart initialization pattern for multi-tab dashboards (Chart.js/Recharts/Nivo all render blank in display:none containers) - component-guide: data-code separation for programmatic HTML generation (f-string + JS curly braces cause silent parse failures) - SKILL.md Design Principles: dynamic color safety rule for external/brand colors - SKILL.md Design Principles: icon semantics check - SKILL.md Anti-Patterns: warn against filtering on unvalidated data fields - SKILL.md: pre-delivery checklist (tabs, fields, contrast, icons, tooltips, mobile)
UI Fixes: - Guard `isFirstTimeUser` on sync status — don't show beginner UI while sessions are loading (prevents flash on every startup) - Make Tips component reactive — tip pool now updates when `isFirstTime` changes (was locked at render time) Telemetry Fixes (privacy-safe): - Add `first_launch` event — fires once after install, contains only version string and is_upgrade boolean. No PII. Opt-out-able. - Use machine_id as ai.user.id fallback — IMPROVES privacy by giving each anonymous user a distinct random UUID instead of grouping all non-logged-in users under empty string "" Documentation: - telemetry.md: added `first_launch` to event table, new "New User Identification" section, "Data Retention" section - security-faq.md: added "How does Altimate Code identify users?" and "What happens on first launch?" sections All telemetry changes respect existing ALTIMATE_TELEMETRY_DISABLED opt-out. No PII is ever sent — machine_id is crypto.randomUUID(), email is SHA-256 hashed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- use `~/.altimate/machine-id` existence for robust `is_upgrade` flag - fix 3-state logic in `isFirstTimeUser` memo to prevent suppressed beginner UI - prevent tip re-randomization on prop change in `tips.tsx` - add missing `first_launch` event to telemetry tests - remove unused import
- Correct Nivo `Responsive*` behavior: `ResizeObserver` does re-fire when container becomes visible, not "never re-fires on show" - Add `altimate_change` marker around `installedVersion` banner line Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…stead of failing
When a file is modified externally (by a formatter, linter, or file watcher) between
a read and edit/write, the tools now auto-refresh the read timestamp and re-read the
file contents instead of throwing "modified since it was last read".
This prevents the agent from entering retry loops of hundreds of consecutive failures
when external processes modify files during editing sessions.
Changes:
- Add `FileTime.assertOrRefresh()` that returns `{ stale: boolean }` instead of throwing
- Update `EditTool` and `WriteTool` to use `assertOrRefresh()` and re-read file contents
- Update test to verify auto-refresh behavior
- Original `FileTime.assert()` preserved for backward compatibility
Closes #450
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review.
Tip: disable this comment in your organization's Code Review settings.
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughA new Changes
Sequence DiagramsequenceDiagram
participant Tool as Tool Execution
participant Processor as Processor
participant FileTime as FileTime
participant Filesystem as Filesystem
participant Logger as Logger
Tool->>Processor: Tool result (StaleFileError)
Processor->>Processor: Detect StaleFileError type
alt File Missing
Processor->>Processor: Append missing file note
else File Size > 50KB
Processor->>FileTime: Read to update metadata
FileTime-->>Processor: Metadata updated
Processor->>Processor: Append size warning
else File Size ≤ 50KB
Processor->>Filesystem: Read fresh file content
Filesystem-->>Processor: File content retrieved
Processor->>FileTime: Read to update metadata
FileTime-->>Processor: Metadata updated
Processor->>Logger: Log auto re-read event
Logger-->>Processor: Event logged
Processor->>Processor: Append content in fenced block
else Recovery Fails
Processor->>Logger: Log warning
Logger-->>Processor: Warning logged
Processor->>Processor: Use original error string
end
Processor->>Processor: Persist error with computed string
Estimated Code Review Effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/opencode/test/tool/edit.test.ts (1)
241-259:⚠️ Potential issue | 🟠 MajorStrengthen this test to prove the stale branch is actually exercised.
Right now the test validates success after external modification, but it doesn’t assert that the file was truly stale before
edit.execute. Add a strictFileTime.assertfailure check first to lock in the intended regression coverage.✅ Suggested assertion to make the precondition explicit
// Simulate external modification await fs.writeFile(filepath, "modified externally", "utf-8") + await expect(FileTime.assert(ctx.sessionID, filepath)).rejects.toThrow( + "modified since it was last read", + ) // Edit should succeed — auto-refreshes the stale read timestamp const edit = await EditTool.init()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/test/tool/edit.test.ts` around lines 241 - 259, The test needs an explicit precondition asserting the file is stale before calling EditTool.execute: after simulating the external modification (the fs.writeFile call) and before creating EditTool via EditTool.init()/calling edit.execute, invoke the stale-check helper (FileTime.assert or whatever function validates read timestamps) and assert it throws/fails to prove the stale branch is exercised; then proceed to create the EditTool and call edit.execute as now. Ensure you reference the same filepath used in the test and use the testing framework’s expect(...).toThrow/await expect(...).rejects.toThrow pattern so the test fails if the stale assertion does not occur.
🧹 Nitpick comments (1)
packages/opencode/src/tool/edit.ts (1)
89-91: Drop unusedstalebinding.
staleis never read after Line 90, so this can be simplified to avoid dead local state.♻️ Proposed cleanup
- const { stale } = await FileTime.assertOrRefresh(ctx.sessionID, filePath) + await FileTime.assertOrRefresh(ctx.sessionID, filePath)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/opencode/src/tool/edit.ts` around lines 89 - 91, The local binding "stale" returned from FileTime.assertOrRefresh is never used; replace the destructuring assignment in the edit flow by calling FileTime.assertOrRefresh(ctx.sessionID, filePath) without capturing the result (i.e., remove "const { stale } ="). Update the call site inside the function that references ctx.sessionID and filePath (the invocation of FileTime.assertOrRefresh) so it awaits the call but does not create an unused local variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/opencode/src/cli/cmd/tui/component/tips.tsx`:
- Around line 53-55: The comment above the tipIndex assignment is misleading by
referencing React's useMemo; update the comment to reference SolidJS semantics
(or createMemo) or simply state that Solid component initialization runs once so
a random tip can be selected on mount; locate the tipIndex constant declaration
(tipIndex = Math.random()) in tips.tsx and replace the mention of "useMemo" with
either "createMemo" or a note that no memo hook is required in SolidJS to avoid
confusion for future maintainers.
---
Outside diff comments:
In `@packages/opencode/test/tool/edit.test.ts`:
- Around line 241-259: The test needs an explicit precondition asserting the
file is stale before calling EditTool.execute: after simulating the external
modification (the fs.writeFile call) and before creating EditTool via
EditTool.init()/calling edit.execute, invoke the stale-check helper
(FileTime.assert or whatever function validates read timestamps) and assert it
throws/fails to prove the stale branch is exercised; then proceed to create the
EditTool and call edit.execute as now. Ensure you reference the same filepath
used in the test and use the testing framework’s expect(...).toThrow/await
expect(...).rejects.toThrow pattern so the test fails if the stale assertion
does not occur.
---
Nitpick comments:
In `@packages/opencode/src/tool/edit.ts`:
- Around line 89-91: The local binding "stale" returned from
FileTime.assertOrRefresh is never used; replace the destructuring assignment in
the edit flow by calling FileTime.assertOrRefresh(ctx.sessionID, filePath)
without capturing the result (i.e., remove "const { stale } ="). Update the call
site inside the function that references ctx.sessionID and filePath (the
invocation of FileTime.assertOrRefresh) so it awaits the call but does not
create an unused local variable.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 5d58c351-94f5-4dbe-b9a5-0e1da66f0664
📒 Files selected for processing (12)
.opencode/skills/data-viz/references/component-guide.mddocs/docs/reference/security-faq.mddocs/docs/reference/telemetry.mdpackages/opencode/src/altimate/telemetry/index.tspackages/opencode/src/cli/cmd/tui/component/tips.tsxpackages/opencode/src/cli/cmd/tui/routes/home.tsxpackages/opencode/src/cli/welcome.tspackages/opencode/src/file/time.tspackages/opencode/src/tool/edit.tspackages/opencode/src/tool/write.tspackages/opencode/test/telemetry/telemetry.test.tspackages/opencode/test/tool/edit.test.ts
| // Pick random tip index once on mount instead of recalculating randomly when props change | ||
| // Use useMemo without dependencies so it only evaluates once | ||
| const tipIndex = Math.random() |
There was a problem hiding this comment.
Fix misleading comment: "useMemo" is React terminology, not SolidJS.
The comment references "useMemo" which is a React hook. SolidJS uses createMemo. The approach works correctly (component functions run once in SolidJS), but the comment could confuse maintainers.
📝 Suggested fix
- // Pick random tip index once on mount instead of recalculating randomly when props change
- // Use useMemo without dependencies so it only evaluates once
+ // Pick random tip index once on mount instead of recalculating randomly when props change
+ // In SolidJS, component functions only run once, so this constant is stable across re-renders
const tipIndex = Math.random()📝 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.
| // Pick random tip index once on mount instead of recalculating randomly when props change | |
| // Use useMemo without dependencies so it only evaluates once | |
| const tipIndex = Math.random() | |
| // Pick random tip index once on mount instead of recalculating randomly when props change | |
| // In SolidJS, component functions only run once, so this constant is stable across re-renders | |
| const tipIndex = Math.random() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/opencode/src/cli/cmd/tui/component/tips.tsx` around lines 53 - 55,
The comment above the tipIndex assignment is misleading by referencing React's
useMemo; update the comment to reference SolidJS semantics (or createMemo) or
simply state that Solid component initialization runs once so a random tip can
be selected on mount; locate the tipIndex constant declaration (tipIndex =
Math.random()) in tips.tsx and replace the mention of "useMemo" with either
"createMemo" or a note that no memo hook is required in SolidJS to avoid
confusion for future maintainers.
…tools instead of failing" This reverts commit 946ec9f.
…rrent content When `edit` or `write` tools fail with "modified since it was last read", the agent loop now auto-re-reads the file and includes its current content in the error response. This gives the model the fresh file state so it can adjust its next edit accordingly, preventing infinite retry loops. This approach preserves the original safety check in `FileTime.assert()` (the tool still throws on stale files) but recovers at the agent level by: 1. Detecting stale file errors via regex match on the error message 2. Re-reading the file and updating `FileTime` read timestamp 3. Appending fresh file content to the error so the model sees current state Also handles "must read file before overwriting" errors the same way. Closes #450 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eads, safe stringification Addresses all findings from multi-model code review: - CRITICAL: Replace regex-based error detection with typed `StaleFileError` class that extends `Error` with a `filePath` property. Use `instanceof` check in processor instead of brittle string parsing. - MAJOR: Add 50KB size limit before auto-reading stale files to prevent OOM and token explosion. Files over limit get a message to use the Read tool instead. - MAJOR: Handle missing files (deleted between error and re-read) gracefully. - MINOR: Use `String(value.error ?? "Unknown error")` for null-safe stringification. - MINOR: Use markdown code fences instead of `<file>` XML tags to prevent prompt injection via unescaped content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…text to error - Add comprehensive test suite for `StaleFileError` class and recovery logic: - `instanceof` checks (typed error vs regular Error) - Small file auto-re-read with content in response - Large file rejection with size message - Missing file graceful handling - Null/undefined error safety - Regex-like error text on regular Error does NOT trigger recovery - Backtick fencing handles files containing markdown code blocks - FilePath preserved for paths with spaces/special chars - Append read failure context to error message when auto-re-read fails, so the model knows why re-reading didn't work Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Why agent-level, not tool-level?
We considered two approaches:
Approach 2 is better because the model context gets updated with current file content (e.g. after a SQL linter reformats select to SELECT), preventing secondary oldString match failures.
How it works
In processor.ts tool-error handler:
Context
984 stale-file errors in 7 days, bursts of up to 295 consecutive failures. Root cause: formatters/linters modify files between read and edit.
Closes #450
Summary by CodeRabbit