fix(opencode): Windows path case mismatch in SQLite scanner#617
fix(opencode): Windows path case mismatch in SQLite scanner#617spralle wants to merge 4 commits into
Conversation
normalizePath lowercases paths but the DB stores original casing, so the WHERE clause never matched on Windows.
There was a problem hiding this comment.
Findings
- [Major] DB priming can replay stale parts from the whole session, evidence
cli/src/opencode/utils/opencodeStorageScanner.ts:408. - [Major] Seeded/resumed sessions never select the SQLite storage source, evidence
cli/src/opencode/utils/opencodeStorageScanner.ts:536.
Summary
- Review mode: initial
- The new SQLite scanner path has correctness risks for replay filtering and resumed sessions.
Testing
- Not run (automation; review-only, did not execute PR code).
| messageIds.push(msg.id); | ||
| this.messageDbVersion.set(msg.id, msg.time_updated); | ||
|
|
||
| if (msg.time_created >= replayThresholdMs) { |
There was a problem hiding this comment.
[MAJOR] msg.time_created >= replayThresholdMs only gates message.updated; the later part loop still iterates every messageId and emits every matching part in the session. The file-backed path records replayMessageIds and only replays parts for messages created near scanner startup, so a DB-backed session with older messages can resend stale text/tool parts into HAPI as fresh output.
Suggested fix:
const replayMessageIds = new Set<string>();
// inside the message loop
if (msg.time_created >= replayThresholdMs) {
replayMessageIds.add(msg.id);
// emit message.updated
}
// before loading parts for a message
for (const messageId of messageIds) {
if (!replayMessageIds.has(messageId)) {
continue;
}
// load and emit parts
}| // Use the same storage source as setActiveSession for consistency | ||
| if (this.activeStorageSource === 'database' && this.dbReady && this.db) { | ||
| await this.scanMessagesAndPartsFromDatabase(sessionId); | ||
| } else if (this.activeStorageSource === 'files' || !this.activeStorageSource) { |
There was a problem hiding this comment.
[MAJOR] When opts.sessionId is already known, the constructor sets activeSessionId, so discoverSessionId() never calls setActiveSession(). With activeStorageSource still null, this branch scans only files and never reads opencode.db; DB-backed resume sessions can miss all message/part updates until a different session id is observed.
Suggested fix:
private async ensureActiveStorageSource(sessionId: string): Promise<void> {
if (this.activeStorageSource) {
return;
}
this.messageRoles.clear();
this.messageDbVersion.clear();
this.partDbVersion.clear();
if (this.dbReady && this.db) {
await this.primeSessionFilesFromDatabase(sessionId);
this.activeStorageSource = 'database';
return;
}
if (await this.ensureStorageDir()) {
await this.primeSessionFilesFromFiles(sessionId);
this.activeStorageSource = 'files';
}
}
// before scanMessagesAndParts(this.activeSessionId)
await this.ensureActiveStorageSource(this.activeSessionId);120s was too tight — heavier projects need more time for opencode to create the session entry.
There was a problem hiding this comment.
Findings
- [Major] Database priming replays stale parts —
primeSessionFilesFromDatabase()emits every part for every message in the session, while the file-backed path only replays parts for messages newer thanreferenceTimestampMs - REPLAY_CLOCK_SKEW_MS. Once SQLite is available, switching toactiveStorageSource = 'database'can therefore resend older assistant text/tool parts into the live HAPI transcript when attaching to a session with existing history. Evidence:cli/src/opencode/utils/opencodeStorageScanner.ts:451
Suggested fix:const replayMessageIds = new Set<string>(); // inside the message loop, only mark messages selected for replay if (msg.time_created >= replayThresholdMs) { replayMessageIds.add(msg.id); // emit message.updated } // inside the part loop if (!replayMessageIds.has(messageId)) { continue; }
Questions
- None.
Summary
Review mode: initial
The PR changes OpenCode local tracking from file-only polling to SQLite-first polling. The main regression is that the new database priming path does not preserve the existing replay window for parts. Scanner coverage is also absent: not found in repo/docs for opencodeStorageScanner tests.
Testing
- Not run (automation):
bun run typecheck:clifailed becausebunis not installed in this runner.
HAPI Bot
| messageID: partRow.message_id, | ||
| sessionID: partRow.session_id | ||
| }; | ||
| if (this.shouldEmitPart(part, messageId)) { |
There was a problem hiding this comment.
[MAJOR] This database priming path emits parts for every messageId, but the existing file-backed path only emits parts for messages added to replayMessageIds after passing the startup replay threshold. With SQLite available, attaching to a session that already has history can resend old assistant text/tool parts into the current transcript. Preserve the same replay gate here.
const replayMessageIds = new Set<string>();
if (msg.time_created >= replayThresholdMs) {
replayMessageIds.add(msg.id);
// emit message.updated
}
if (!replayMessageIds.has(messageId)) {
continue;
}
Builds on #589 — the
WHERE directory = ?query never matched on Windows becausenormalizePath()lowercases paths but the DB stores original casing (C:\foovsc:\foo).Uses
LOWER(directory)on win32 so the comparison works. Tested locally, session discovery now succeeds.