Skip to content

fix: Files Changed cross-session contamination + external-change reactivity #741

Description

@jeonghun-jj-lee

Important

Problem — The Files Changed tab shows files that the current session's agent never edited. The root cause is the step-end event capturing all worktree changes between step-start and step-finish snapshots — including modifications by other concurrent sessions or external processes — and recording them as the session's changed files. The session.diff() endpoint trusts these records as the agent-touched-file filter, so the contamination propagates to the UI. Additionally, when the user reverts or further modifies an agent-edited file outside the agent's tools, the tab does not update — it only refreshes on agent tool completions and session status transitions, missing all external changes.

Approach — Three changes. (1) Fix the contamination by deriving the agent-touched-file filter in the server's session.diff() handler exclusively from tool filediff metadata, ignoring the workspace-wide file lists in patch/step-end parts. (2) Add external-change reactivity by removing the OPENCODE_EXPERIMENTAL_FILEWATCHER enable-gate (preserving the disable-gate as opt-out), adding a new file.watcher.updated handler in the client event-reducer that cross-references incoming events against the session's agent-touched file set, and debouncing a diff_version bump (1s trailing-edge). (3) Add a manual refresh button in the panel toolbar as a fallback.

Scope — in: contamination fix in the server's session.diff() handler, watcher flag cleanup, new client-side file.watcher.updated event handler, refresh button in the panel toolbar, new refresh icon SVG in the icon sprite. out: fixing patch/step-end part data at the processor source (existing records stay as-is), server-side per-session file tracking beyond the existing touchedFiles endpoint, showing user-only edits to files the agent never touched, detecting file deletions via bash rm (see Known Limitations).

Assumptions@parcel/watcher (FSEvents on macOS, inotify on Linux) is performant enough to run by default on typical repos; the existing watcher.ignore config mitigates large-repo inotify pressure. The agent-touched file set per session is small enough that per-event cross-referencing is negligible. Watcher event paths and touched-file paths can be normalized to the same absolute form for matching.

Acceptance Criteria

  • A session with no edit/write/patch/apply_patch tool calls shows zero files in the Files Changed tab
  • A session with edit tool calls shows exactly the files those tools modified (deduplicated by path), with M/A/D status derived from the server's git-state comparison
  • Concurrent sessions editing different files do not leak files into each other's Files Changed tabs
  • When a user reverts an agent-edited file externally (Ctrl+Z, git checkout, terminal edit), the Files Changed tab updates automatically (target: within 2s; manual verification, not CI-asserted)
  • When a user further edits an agent-edited file, the diff content updates automatically (same target)
  • A refresh button (circular-arrow icon, tooltip "Refresh") appears in the panel toolbar after the Unified/Split toggle and triggers a full diff refetch on click
  • The @parcel/watcher worktree watch runs by default (OPENCODE_EXPERIMENTAL_FILEWATCHER enable-gate removed); OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER preserved as opt-out
  • If @parcel/watcher fails to initialize, the tab continues to work via tool-completion and session-status triggers; the refresh button remains functional; no error is surfaced to the user

Testing Decisions

Extend the existing snapshot/session test suites:

  • Zero-edits test: construct a session with only read/grep/glob tool parts (no edit tools) and assert the diff response is empty
  • Contamination test: construct a session with patch parts containing extra files alongside tool parts with filediff metadata; assert session.diff() returns only the filediff files
  • Watcher reactivity: manual verification (file-system timing makes it flaky in CI)

Key Decisions

  • Filter at the query handler, not the event emitter. The patch/step-end data in the processor is left unchanged — filtering happens in the server's session.diff() handler. Simpler, no processor changes, no backward compatibility concerns with existing DB records.
  • New client-side file.watcher.updated handler. The SSE event type exists but the client event-reducer does not currently handle it. A new case is added that cross-references the file path (normalized to absolute) against the session's agent-touched file set and schedules a debounced diff_version bump on match. Watcher events for files not in the touched set are silently discarded — including the agent's own writes (which are already covered by the tool-completion trigger).
  • 1s trailing-edge debounce, per-session. Coalesces rapid external changes into a single refetch. Noted as a tunable, not a permanent decision — a single event (e.g., git checkout) incurs 1s latency with no coalescing benefit; adaptive dispatch could improve this later.
  • Refresh button in the panel toolbar. Placed after the Unified/Split toggle (right end of toolbarEnd), where its panel-wide scope matches its visual context. Visible even when zero files are shown.
  • New refresh icon SVG. No circular-arrow icon exists in the current sprite; one must be added.

Known Limitations

  • Files deleted via bash rm are not captured by the filediff-based filter. The bash tool does not emit filediff metadata, so agent-mediated deletions outside the edit/write/patch/apply_patch tools will not appear in the Files Changed tab. Tracked separately.

Constraints & Invariants

  • The patch/step-end part schema is unchanged — existing sessions continue to work.
  • Snapshot hashes on step-start and step-finish parts remain the source of truth for the diff range.
  • The diff query contract (GET /session/:id/diff) returns only agent-touched files, never workspace-wide changes.
  • The existing touchedFiles server endpoint (which derives from tool metadata) is the read path; no new server-side per-session tracking state is added.
  • Tool-completion and session-status triggers remain the baseline reactivity path; watcher-based reactivity is additive.
  • On session re-open (tab switch, extension reload), the diff is refetched from the server and watcher subscriptions are re-established.

Prior Art

Source

Follow-up to #733 (same root cause, different surface)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions