Skip to content

Commit 310cc98

Browse files
committed
fix: make session shard export deterministic
exportedAt used Date.now(), so the shard payload changed on every export even when the session itself did not. git saw every shard as modified and autoPushOnIdle produced a commit touching every session file on each idle. Derive exportedAt from the session's own time_updated instead, so a repeat export of an unchanged session is byte-identical and pushes stay incremental. The existing regression test never caught this: under vitest the dynamic import("node:sqlite") fails to resolve, sqliteAvailable() reported false and every session test returned early. Fall back to createRequire so the builtin loads under test runners and the session tests actually execute. Refs #3
1 parent f9b2c20 commit 310cc98

3 files changed

Lines changed: 29 additions & 6 deletions

File tree

‎src/core/sessions.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ function buildShard(db: SqliteDatabase, sessionId: string, tables: string[]): Se
165165
formatVersion: 1,
166166
session: sessionRow,
167167
tables: {},
168-
exportedAt: Date.now(),
168+
// Derived from the session itself, never wall-clock time. A repeat export
169+
// of an unchanged session must produce identical bytes, otherwise every
170+
// push rewrites every shard and import/pull churn is unavoidable.
171+
exportedAt: Number(sessionRow.time_updated ?? 0),
169172
};
170173

171174
const projectId = sessionRow.project_id;

‎src/core/sqlite.ts‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* disabled with a clear message rather than failing halfway through.
1515
*/
1616

17+
import { createRequire } from "node:module";
18+
1719
export interface SqliteStatement {
1820
all(...params: unknown[]): any[];
1921
run(...params: unknown[]): unknown;
@@ -39,6 +41,23 @@ async function importBunSqlite(): Promise<any> {
3941
return import(/* @vite-ignore */ /* webpackIgnore: true */ BUN_SQLITE);
4042
}
4143

44+
/**
45+
* Load the Node built-in SQLite driver.
46+
*
47+
* The plain dynamic import is what works under Node and Bun. Test runners such
48+
* as vitest intercept dynamic imports and try to resolve `node:sqlite` as a
49+
* project file, which makes the driver undetectable and silently turns every
50+
* session test into an early return. `createRequire` bypasses the bundler and
51+
* loads the real builtin, so those tests actually run.
52+
*/
53+
async function importNodeSqlite(): Promise<any> {
54+
try {
55+
return await import("node:sqlite");
56+
} catch {
57+
return createRequire(import.meta.url)("node:sqlite");
58+
}
59+
}
60+
4261
let cachedDriver: "bun" | "node" | "none" | undefined;
4362

4463
async function detectDriver(): Promise<"bun" | "node" | "none"> {
@@ -53,7 +72,7 @@ async function detectDriver(): Promise<"bun" | "node" | "none"> {
5372
}
5473
}
5574
try {
56-
const mod: any = await import("node:sqlite");
75+
const mod: any = await importNodeSqlite();
5776
if (mod?.DatabaseSync) {
5877
cachedDriver = "node";
5978
return cachedDriver;
@@ -102,7 +121,7 @@ export async function openDatabase(
102121
}
103122

104123
if (driver === "node") {
105-
const { DatabaseSync } = (await import("node:sqlite")) as any;
124+
const { DatabaseSync } = (await importNodeSqlite()) as any;
106125
const db = new DatabaseSync(file, options.readOnly ? { readOnly: true } : {});
107126
return {
108127
prepare: (sql: string) => {

‎tests/sessions.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,14 @@ describe("export and import", () => {
305305

306306
await exportSessions(source, repo, { ...settings(), now }, silentReporter);
307307
const shard = path.join(repo, "_sessions", "ses_recent.json.gz");
308-
const before = fs.statSync(shard).mtimeMs;
308+
const before = fs.readFileSync(shard);
309309

310310
await new Promise((resolve) => setTimeout(resolve, 20));
311311
await exportSessions(source, repo, { ...settings(), now }, silentReporter);
312312

313-
// An unchanged session must not produce a new diff on every push.
314-
expect(fs.statSync(shard).mtimeMs).toBe(before);
313+
// An unchanged session must not produce a new diff on every push — same
314+
// bytes, same mtime, no diff for git to pick up.
315+
expect(fs.readFileSync(shard).equals(before)).toBe(true);
315316
});
316317
});
317318

0 commit comments

Comments
 (0)