Skip to content

[Bug]: Session scanner marks a replaced transcript as already imported on Windows #14838

Closed

Description

@aharlap

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. On Windows (NTFS), import a provider transcript.
  2. Replace that transcript file with a different session at the same path, with the same size and mtime, created within the same millisecond.
  3. Scan again.

The existing replaced-transcript test in apps/server/src/project/AgentSessionScanner.test.ts (around line 1940) reproduces it. It fails on Windows at the Importable assertion.

Expected behavior

The replaced transcript is Importable.

Actual behavior

The scanner returns AlreadyImported without reading the file, and the new session is never imported.

Impact

Rare: it needs an equal-size, equal-mtime replacement within the same millisecond. When it happens, a session silently goes missing from history import.

Version or commit

v0.0.44 (451afcb22d93); also present on main at b33eda13998b (2026-10-01).

Environment

Windows 11 x64, NTFS, Node 24.19.0.

Logs or stack traces

Cause: @effect/platform-node-shared's NodeFileSystem requests bigint stats, then converts ino to an optional JS number, so IDs above Number.MAX_SAFE_INTEGER become None. NTFS file IDs are routinely that large (a probe saw 5764607523043455093 and 5764607523044503669 for two distinct files). The scanner records the missing inode as null (AgentSessionScanner.ts around line 590), its identity comparison treats two null inodes as equal (around 608), and with equal size, mtime and creation millisecond the completed-metadata shortcut (around 1399) returns AlreadyImported.

The root fix is to keep large file IDs instead of dropping them, for example by carrying ino as a bigint or string through the stat result, so the existing identity check works on NTFS.

We tried a local alternative: when the inode is missing, re-read and parse the completed transcript and compare session IDs before returning AlreadyImported. It fixes the replacement case, but on Windows nearly every file has a missing inode, so every completed transcript is re-read on every scan. Those re-reads spend the scan budget (MAX_IMPORT_TRANSCRIPTS = 100), so a user with many imported sessions can stop seeing new ones. That's worse than the original rare bug, so we didn't ship it.

Activity

  1. juliusmarminge commented on Oct 2, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for the precise root cause, @aharlap. It's also good to know you tried the re-read fallback and why it doesn't hold up. Confirmed on current main (54084ae1). The replaced-transcript test already expects the intended behavior, Importable.

    FileSystem.File.Info.ino is Option<number>, and @effect/platform-node-shared intentionally drops IDs outside the safe integer range. transcriptIdentity stores that as null (AgentSessionScanner.ts around line 596), sameTranscriptIdentity treats null === null as the same file (around line 613), and the completed-history shortcut (around line 1403) returns AlreadyImported without reading the transcript. Both NTFS IDs in your report are above Number.MAX_SAFE_INTEGER. directoryIdentity already falls back to the path for unsafe inodes, but that wouldn't work here because the path is the same and the file isn't.

    One detail about the test: sameTranscriptIdentity also requires equal birthtimeMs. writeTranscript only forces mtime, and Windows utimes doesn't change creation time, so the test only fails when the replacement lands in the same millisecond. A regression test should pin both birthtimes so it fails reliably on Windows.

    I agree the re-read fallback is the wrong fix. A missing inode is the normal case on Windows, so every scan would re-parse every completed transcript and use up MAX_IMPORT_TRANSCRIPTS (100). The metadata shortcut should stay and compare the full 64-bit file ID instead, the way media files already keep ino from a bigint stat as a decimal string. AgentSessionImportSource.inode is currently NullOr(Number), so existing stored sources still need to decode. Once the wider ID is stored, one later scan can backfill old null sources, but it shouldn't re-read them on every scan.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 2, 2026
  3. added a commit that references this issue on Oct 16, 2026
    1fed345
  4. added a commit that references this issue on Oct 16, 2026
    9460b00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions