Skip to content

fix(ai): prevent stale agent edit overwrites - #309

Open
arhxam wants to merge 1 commit into
getopenscreen:mainfrom
arhxam:codex/prevent-stale-agent-overwrites
Open

fix(ai): prevent stale agent edit overwrites#309
arhxam wants to merge 1 commit into
getopenscreen:mainfrom
arhxam:codex/prevent-stale-agent-overwrites

Conversation

@arhxam

@arhxam arhxam commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • capture the project document and revision atomically when an AI turn starts
  • refuse to apply a returned full-document edit when any local document mutation advanced that revision
  • leave the user's live document and on-disk project untouched on conflict, while still retaining the assistant's chat response
  • show a localized warning explaining why the agent edits were not applied
  • preserve intentional chat rewind behavior by allowing that confirmed action to replace the current revision

Related issue

Fixes #284

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

Not included; the visible change is a localized warning toast on a concurrent-edit conflict.

Testing

  • Started with a failing regression test before the revision guard existed
  • npx vitest --run src/lib/ai-edition/store/agentDocumentApply.test.ts (3 passed)
  • npm run test (1,680 passed, 1 skipped across 141 files)
  • npx tsc --noEmit
  • npx tsc -p tsconfig.test.json --noEmit
  • npx biome check on the implementation, test, component, and locale files
  • npm run docs:check
  • npm run i18n:check
  • npm run build-vite

Authored with Codex assistance and manually verified against the issue's concurrent-edit reproduction.

Summary by CodeRabbit

  • New Features

    • Agent-generated edits are now protected from overwriting newer manual project changes.
    • Conflicting edits trigger a warning instead of being applied.
    • Rewind actions continue to replace the current project content as expected.
  • Localization

    • Added conflict warnings across all supported languages.
  • Bug Fixes

    • Improved reliability when agent processing overlaps with project edits.

@arhxam
arhxam requested a review from EtienneLescot as a code owner August 8, 2026 18:26
@coderabbitai

coderabbitai Bot commented Aug 8, 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b834f314-0582-4d7b-96b2-1715d71a4a8b

📥 Commits

Reviewing files that changed from the base of the PR and between 4e7a85b and 3a2255d.

📒 Files selected for processing (16)
  • src/components/ai-edition/LeftPanel.tsx
  • src/i18n/locales/ar/editor.json
  • src/i18n/locales/en/editor.json
  • src/i18n/locales/es/editor.json
  • src/i18n/locales/fr/editor.json
  • src/i18n/locales/it/editor.json
  • src/i18n/locales/ja-JP/editor.json
  • src/i18n/locales/ko-KR/editor.json
  • src/i18n/locales/pt-BR/editor.json
  • src/i18n/locales/ru/editor.json
  • src/i18n/locales/tr/editor.json
  • src/i18n/locales/vi/editor.json
  • src/i18n/locales/zh-CN/editor.json
  • src/i18n/locales/zh-TW/editor.json
  • src/lib/ai-edition/store/agentDocumentApply.test.ts
  • src/lib/ai-edition/store/agentDocumentApply.ts

📝 Walkthrough

Walkthrough

Changes

Agent document conflict handling

Layer / File(s) Summary
Document application helper and tests
src/lib/ai-edition/store/agentDocumentApply.ts, src/lib/ai-edition/store/agentDocumentApply.test.ts
Added applyAgentDocumentIfCurrent and its result type. The helper validates documents, checks optional revisions, updates project state, persists documents, and supports rewind behavior. Tests cover application, conflicts, and rewinds.
Chat revision checks and localized feedback
src/components/ai-edition/LeftPanel.tsx, src/i18n/locales/*/editor.json
LeftPanel captures the document revision before each chat run. Returned agent documents apply only when the revision is unchanged. Conflicts show a localized warning, while rewinds remain revisionless.

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

Sequence Diagram(s)

sequenceDiagram
  participant LeftPanel
  participant chatRun
  participant applyAgentDocumentIfCurrent
  participant useProjectStore
  LeftPanel->>useProjectStore: Capture document and revision
  LeftPanel->>chatRun: Start agent run
  chatRun-->>LeftPanel: Return agent document
  LeftPanel->>applyAgentDocumentIfCurrent: Apply with expected revision
  applyAgentDocumentIfCurrent->>useProjectStore: Check current revision
  alt Revision unchanged
    applyAgentDocumentIfCurrent->>useProjectStore: Set and save document
    applyAgentDocumentIfCurrent-->>LeftPanel: Return applied
  else Revision changed
    applyAgentDocumentIfCurrent-->>LeftPanel: Return conflict
    LeftPanel->>LeftPanel: Show warning toast
  end
Loading

Suggested reviewers: etiennelescot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: preventing stale AI agent edits from overwriting current document changes.
Description check ✅ Passed The description follows the repository template and includes the change summary, issue link, classifications, impact, and detailed testing information.
Linked Issues check ✅ Passed The implementation detects revision conflicts, preserves local and saved changes, shows localized warnings, retains chat responses, and preserves rewind behavior for issue #284.
Out of Scope Changes check ✅ Passed The changes are limited to revision-safe agent document application, conflict localization, and regression tests required by issue #284.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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.

@EtienneLescot EtienneLescot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at the PR head (3a2255db). Verdict: ship with nits, but please read §1 — the failure mode is more common than it looks.

The guard itself is correct for the window it targets, and I checked the things that usually go wrong here and did not find them: documentRevision is read from the same getState() snapshot as documentSnapshot (LeftPanel.tsx:930-932) and before the await chatRun, so there is no read-after-await bug. The comparison expectedRevision !== undefined && store.revision !== expectedRevision correctly treats revision 0 as a real value, and !== rather than < also catches a counter moving backwards. No stale closure — applyAgentDocument is a []-dep wrapper over an imported module function reading live state via getState() at call time.

Blast radius is genuinely small: only two call sites apply an agent document, and both go through the new module. I confirmed no chat event carries a document (AiEditionChatEvent in src/native/contracts.ts:779-784 is text/thinking/toolStart/toolEnd/error only), and that runTimelineOperation (chat-service.ts:526) has no IPC handler and no renderer caller, so it is not a live bypass.

i18n is clean — npm run i18n:check passes, all 13 locales have the key at the same position after chat.applyEditsFailed, every value is genuinely translated (zh-TW is correctly distinct from zh-CN, not a copy), and terminology for "agent" matches the neighbouring keys in each language. Both typecheck configs, biome and the new tests all pass.

1. A conflict discards the whole turn, and background writers will trigger it routinely

This is the finding I'd most like addressed before merge. The conflict path drops result.document on the floor with no recovery, and the thing that moves revision is usually not the user.

transcriptionStore.ts transcribes every asset that lands in the document automatically in the background and finishes with saveDocument at :398revision + 1. Same for the load-time dimension probe (useTimeline.ts:140-157) and the camera auto-link (projectStore.ts:172).

Concretely: import a fresh 5-minute recording, ask "cut the silences". Whisper finishes 20s into the agent turn. applyResult === "conflict". The user's 30 seconds and their tokens are gone, result.document is unrecoverable except by re-asking (which races again) — and the chat still renders the assistant's "done, I removed 14 silences" text plus its green tool-call summary chips, because LeftPanel.tsx:953-966 appends the message unconditionally. The only feedback is a toast blaming "the project changed" for something the user never did.

Minimal improvement — keep the document and make the toast actionable:

const pending = result.document;
toast.warning(t("chat.agentEditConflict"), {
  action: { label: t("chat.applyAnyway"), onClick: () => void applyAgentDocumentIfCurrent(pending) },
});

Better long-term would be re-running the turn against the fresh snapshot.

2. The guard closes the read window but not the write window

agentDocumentApply.ts:23-24 — the check happens before the write, but setDocument + saveDocument is not atomic.

projectStore.saveDocument (projectStore.ts:214-226) awaits nativeBridgeClient.aiEdition.save(document) and only then calls set({ document: parsed, revision: +1 }). So: guard passes → setDocument(agentDoc) repaints the timeline with the agent's cuts → the user immediately drags a clip (the moment they are most likely to react) → useTimeline calls its own saveDocument → both IPC calls are in flight independently → whichever set() resolves last wins in memory and on disk. The user's drag is silently reverted.

The PR narrows the window from "the whole agent turn" (seconds) to "one IPC + disk write" (tens of ms), which is a real improvement — but it is the same bug the PR is named after, just smaller. Cheapest fix inside the module is to re-check after the await. The structurally better version is to move the compare-and-swap into projectStore.saveDocument itself (optional expectedRevision arg), so useTimeline/transcriptionStore/captions all inherit it — or to route agent applies through the existing serialising queue in useSequentialTimelineOps.ts, whose file header documents precisely this "two concurrent calls both read the pre-edit doc and the second clobbers the first" race.

3. On a save failure, the UI shows the edits while the toast says they were rejected

agentDocumentApply.ts:23-24setDocument(parsed) mutates the store (and sets dirty: true) before saveDocument can throw.

Project file locked by another process, or disk full → saveDocument throws → LeftPanel.tsx:947-951 shows "Could not apply the agent's edits" → but the timeline is displaying the agent's edits anyway, dirty is true, and the next unrelated edit's saveDocument persists the agent's document to disk. The user was told it was rejected.

const prev = store.document;
store.setDocument(parsed);
try {
  await store.saveDocument(parsed);
} catch (e) {
  if (prev) store.setDocument(prev);
  throw e;
}

(Pre-existing ordering, but the new module is where it now lives.)

4. The rewind button undoes the protection the conflict just gave

LeftPanel.tsx:1026confirmRewind calls applyAgentDocument(doc) with no expectedRevision, deliberately. But it sits right next to the message that just conflicted: conflict toast appears → user clicks ↩ to retry the turn → chatRewind returns the checkpoint recorded before the turn (chat-service.ts:355-361) → the manual edit is overwritten, five seconds after the app told the user it was preserving it. The rewind popover copy doesn't warn that current work will be replaced.

Either pass the revision on that path too with a second confirmation, or amend the confirmation copy to say the current document will be replaced by the checkpoint.

Nits

  • LeftPanel.tsx:930-943expectedRevision is passed even when documentSnapshot is undefined. In that case runChat runs the agent against emptyDocumentForTextOnly(projectId) (chat-service.ts:401) carrying the real project id, so if the agent mutates it and the revision happens to match, a near-empty document gets written over a real project. I could not construct this today (loadProject sets projectId and document in the same set(), clear() nulls both), so purely defensive: if (result.document && documentSnapshot).
  • LeftPanel.tsx:876-879applyAgentDocument is a useCallback identity wrapper around an already-stable module-level function. Five lines of indirection plus a pointless entry in confirmRewind's dep array. Calling applyAgentDocumentIfCurrent(...) directly at both sites would be clearer.
  • agentDocumentApply.ts — the comment explaining why both setDocument and saveDocument are called did not survive the move. setDocument now looks redundant next to saveDocument (which also sets document), so the obvious "simplification" is to delete it — which silently breaks Ctrl+Z after an agent edit, since setDocument is the only thing that pushes the previous document onto the undo stack (projectStore.ts:228-236). Worth carrying that sentence over.
  • Extracting to a module was the right call despite the guard being ~3 lines — LeftPanel.tsx is 1976 lines with no test file, so this is what makes the tests possible at all.

One coverage gap worth knowing: the three tests exercise the module, but nothing pins the thing that would actually break. Move the documentRevision read below the await chatRun and all three still pass with the bug fully restored. A render test on ChatStripPanel, or extracting the send-and-apply sequence, would cover it.

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.

[Bug]: edits made while the AI agent is running are overwritten when it answers

2 participants