#64195·TypeScript

LSP: file contents are not re-read when `didChangeWatchedFiles` reports `Created` (type 1) for a file already in the program

Author: shuto-masudaCreated Sep 8, 2026Updated Sep 14, 2026
LabelsNeeds Investigation

Search Terms

didChangeWatchedFiles
FileChangeType.Created
type 1 Created event stale contents
language server stale file contents
LSP requires restart types out of sync
atomic save rename language server
has no exported member after the file was fixed
watched files not reloaded
tsc --lsp

Version & Regression Information

  • I was unable to test this on prior versions because the LSP server (tsc --lsp) is new in TypeScript 7; TypeScript 6.x and earlier have no equivalent workspace/didChangeWatchedFiles handling to compare against, so this is not a regression from a previous release.

Reproduces on both:

every-ts bisection is not applicable for the same reason — the code path does not exist in earlier versions.

⏯ Playground Link

Not applicable. The bug is in how the language server reacts to file-system change notifications, so it cannot be reproduced in the Playground. A standalone LSP client script that reproduces it in isolation is attached instead (see Code).

Code

Three files:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler"
  },
  "include": ["src/**/*"]
}
// src/main.ts
import { thing } from './helper';

export const used = thing;
// src/helper.ts  — starts out without `thing`, so main.ts has an error
export const other = 1;

Then, over LSP with main.ts open and helper.ts not open:

  1. initialize (advertising workspace.didChangeWatchedFiles.dynamicRegistration), initialized, then textDocument/didOpen for src/main.ts.

  2. textDocument/diagnostic for main.tsModule '"./helper"' has no exported member 'thing'. (expected at this point)

  3. Fix src/helper.ts on disk to export const thing = 1;, written the way an atomic save does: write helper.ts.tmp, then rename("helper.ts.tmp", "helper.ts").

  4. Send the change notification with the events a client actually sends for that rename (VS Code's watcher reports a rename-over as a create):

    { "changes": [
        { "uri": ".../src/helper.ts.tmp", "type": 1 },
        { "uri": ".../src/helper.ts",     "type": 1 },
        { "uri": ".../src/helper.ts.tmp", "type": 3 } ] }
    
  5. textDocument/diagnostic for main.ts again.

Attached: repro-created-event.js — performs exactly these steps against tsc --lsp --stdio. Node only, no dependencies, no editor involved:

$ node repro-created-event.js <path-to-tsc>
PASS  Changed (type 2) only  — control
FAIL  Created (type 1) only
        still reported: Module '"./helper"' has no exported member 'thing'.
FAIL  what VS Code sends for an atomic save
        still reported: Module '"./helper"' has no exported member 'thing'.

repro-created-event.js

Actual behavior

After helper.ts is fixed on disk, textDocument/diagnostic for main.ts keeps returning Module '"./helper"' has no exported member 'thing'. The server never re-reads helper.ts, so the program holds the old contents indefinitely — the stale diagnostic survives further edits to other files and only goes away on a server restart, or on textDocument/didOpen of helper.ts (which supplies the contents explicitly).

The trigger is the change type. For the same on-disk change:

notification for the changed file result
type: 2 (Changed) only ✓ error clears (correct)
type: 1 (Created) only ✗ error persists
tmp create → target create → tmp delete (what VS Code sends) ✗ error persists

Expected behavior

All three should clear the diagnostic, since the file's contents on disk changed.

A Created event for a path that is already in the program should invalidate that file's cached contents, the same way Changed does. A client cannot reliably tell the two apart: on Linux, rename() over an existing path produces IN_MOVED_TO, which is a create from the watcher's point of view — VS Code therefore sends type: 1 for what is semantically a modification.

This matters in practice because saving atomically (write a temp file, then rename over the target) is a common pattern — formatters, code generators, editors with files.atomicSave, and AI coding tools all do it. Every such save takes this path, so the user-visible result is a permanent, wrong has no exported member on an import line, "fixed" only by restarting the language server.

Additional information about the issue

Environment

  • TypeScript 7.0.2 and 7.1.0-dev.20260907.1, Linux (arm64), Node 24.14.0
  • Language server started as tsc --lsp --stdio
  • Also observed through VS Code 1.127.0 with js/ts.experimental.useTsgo: true and the "TypeScript 7" extension [email protected] (whose bundled binary also reports 7.0.2)

What is not the cause

  • The watcher globs the server registers are **/* (the extension-suffixed globs from microsoft/typescript-go#2506 are gone), so the event does reach the server — VS Code's own logs show handled method 'workspace/didChangeWatchedFiles' for these changes.
  • The server is robust in every adjacent scenario tested: didChange, didClose + didOpen, type: 2 on an unopened file, 20 rapid successive writes, delete + create of a previously unknown file, and create-then-modify before notifying. Only "type: 1 for a path already in the program" fails.
  • Not a client-side file-watching problem: reproduced with a plain Node LSP client, with no editor in the picture.

Possibly related

  • microsoft/typescript-go#2506 — same area (watched-file handling), different cause (extension-suffixed watch globs and failedLookupLocations for unresolved modules). Already fixed: a Created event for a path that is not yet in the program does work.
  • JetBrains WEB-76338 "Typescript Go LSP often requires restart as types get out of sync" — same user-visible symptom from a different client, no root cause identified. Possibly the same bug, since JetBrains also saves atomically.

Workarounds

  • Open the changed file in the editor (didOpen re-reads from disk).
  • Restart the language server.
  • Follow the atomic write with an in-place rewrite of identical bytes (produces IN_MODIFYtype: 2). Note the client may coalesce the two events if they are close together: with VS Code's watcher, a gap of ~600ms or more was needed before the second event was reported as an update rather than another create.