From 79ea2d529f1d3d81302433c2ae0ff1279f9cd4e2 Mon Sep 17 00:00:00 2001 From: Rene Richter Date: Tue, 19 May 2026 12:26:10 +0200 Subject: [PATCH] fix: handle # compound-path separator in fingerprintFile The Cursor provider encodes workspace context into source paths using a `#cursor-ws=` suffix (e.g. `state.vscdb#cursor-ws=__orphan__`). `fingerprintFile` only had a fallback for `:` separators (OpenCode sessions), so Cursor sources silently returned null on macOS/Linux where paths contain no colons, causing them to be skipped entirely. Add a `#` fallback before the existing `:` check. The first `stat()` on the full path still succeeds for real files containing `#`, so there is no regression for legitimate paths. Includes 4 new test cases covering both separators, the combined case, and the null case for non-existent base files. --- src/session-cache.ts | 15 +++++++++++++++ tests/session-cache.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/session-cache.ts b/src/session-cache.ts index 5534ad6..948ed38 100644 --- a/src/session-cache.ts +++ b/src/session-cache.ts @@ -239,6 +239,21 @@ export async function fingerprintFile(filePath: string): Promise#cursor-ws=` (workspace-aware routing) + // - OpenCode: `:` (session scoping) + // These compound paths don't exist on disk; strip the suffix to stat the + // underlying file. Try `#` first (rare in real paths), then `:` (must use + // lastIndexOf to tolerate Windows drive letters like C:\...). + const hashIdx = filePath.indexOf('#') + if (hashIdx > 0) { + try { + const s = await stat(filePath.slice(0, hashIdx)) + return { dev: s.dev, ino: s.ino, mtimeMs: s.mtimeMs, sizeBytes: s.size } + } catch { + // fall through to colon check + } + } const colonIdx = filePath.lastIndexOf(':') if (colonIdx > 0) { try { diff --git a/tests/session-cache.test.ts b/tests/session-cache.test.ts index 8da5153..e919d0b 100644 --- a/tests/session-cache.test.ts +++ b/tests/session-cache.test.ts @@ -185,6 +185,42 @@ describe('fingerprintFile', () => { const fp = await fingerprintFile('/no/such/file') expect(fp).toBeNull() }) + + it('resolves compound path with # separator (Cursor workspace)', async () => { + await mkdir(TMP_DIR, { recursive: true }) + const filePath = join(TMP_DIR, 'state.vscdb') + await writeFile(filePath, 'cursor-data') + + const fp = await fingerprintFile(`${filePath}#cursor-ws=__orphan__`) + expect(fp).not.toBeNull() + expect(fp!.sizeBytes).toBe(11) + }) + + it('resolves compound path with : separator (OpenCode session)', async () => { + await mkdir(TMP_DIR, { recursive: true }) + const filePath = join(TMP_DIR, 'opencode.db') + await writeFile(filePath, 'opencode-data') + + const fp = await fingerprintFile(`${filePath}:ses_abc123`) + expect(fp).not.toBeNull() + expect(fp!.sizeBytes).toBe(13) + }) + + it('returns null when base file does not exist for compound path', async () => { + const fp = await fingerprintFile('/no/such/file.db#cursor-ws=workspace') + expect(fp).toBeNull() + }) + + it('prefers # separator over : when both present', async () => { + await mkdir(TMP_DIR, { recursive: true }) + const filePath = join(TMP_DIR, 'state.vscdb') + await writeFile(filePath, 'both-seps') + + // Path has both # and : — should strip at # first and find the base file + const fp = await fingerprintFile(`${filePath}#cursor-ws=ws:extra-colon`) + expect(fp).not.toBeNull() + expect(fp!.sizeBytes).toBe(9) + }) }) // ── reconcileFile ──────────────────────────────────────────────────────