Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit 3685102

Browse files
committed
FEA-1564: Resolve backfill timestamp review gaps
- Keep sessions.updated_at as an ingest-time sync cursor while preserving source dates for analytics. - Touch existing sessions only when new backfill events are appended or a row is reactivated. - Cover future source timestamps and appended no-timestamp event imports. Testing: Focused collector import tests, desktop typecheck, desktop lint, and full desktop test suite passed. Risks: Token usage updated_at remains source-dated because token analytics use token_usage.created_at and session sync is driven by sessions.updated_at.
1 parent d738387 commit 3685102

2 files changed

Lines changed: 149 additions & 14 deletions

File tree

apps/desktop/src/main/collectors/import-session.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { Harness, NormalizedSession, NormalizedToolUse } from "./types.js";
1111
*
1212
* Idempotency (FEA-1503 AC): re-import adds nothing new.
1313
* - session row: COALESCE-fill on conflict, never clobbers a live row.
14+
* `updated_at` stays an ingest-time mutation cursor for cloud sync; source
15+
* dates live on `started_at`, `ended_at`, events, and token usage analytics.
1416
* - events: per-(session, event_type) high-water-mark on `created_at` — only
1517
* events with a source timestamp strictly greater than the stored max are
1618
* inserted. Backfill never stamps events with importer runtime `now`; when an
@@ -74,10 +76,12 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
7476
model = COALESCE(model, ?),
7577
cwd = COALESCE(cwd, ?),
7678
harness = CASE WHEN COALESCE(harness, '') = '' THEN ? ELSE harness END,
77-
billing_mode = CASE WHEN COALESCE(billing_mode, '') IN ('', 'unknown') THEN ? ELSE billing_mode END,
78-
updated_at = CASE WHEN updated_at IS NULL OR updated_at < ? THEN ? ELSE updated_at END
79+
billing_mode = CASE WHEN COALESCE(billing_mode, '') IN ('', 'unknown') THEN ? ELSE billing_mode END
7980
WHERE id = ?
8081
`);
82+
const touchSessionStmt = db.prepare(
83+
"UPDATE sessions SET updated_at = CASE WHEN updated_at IS NULL OR updated_at < ? THEN ? ELSE updated_at END WHERE id = ?",
84+
);
8185
const reactivateSessionStmt = db.prepare(
8286
"UPDATE sessions SET status = 'active', ended_at = NULL, updated_at = ? WHERE id = ?",
8387
);
@@ -228,8 +232,8 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
228232
session.cwd ?? null,
229233
session.model ?? null,
230234
startedAt,
231-
sourceUpdatedAt,
232-
status === "completed" ? session.endedAt ?? null : null,
235+
now,
236+
status === "completed" ? sourceUpdatedAt : null,
233237
harness,
234238
billingMode,
235239
buildMetadata(session, harness),
@@ -244,7 +248,7 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
244248
null,
245249
null,
246250
startedAt,
247-
sourceUpdatedAt,
251+
now,
248252
status === "completed" ? sourceUpdatedAt : null,
249253
null,
250254
null,
@@ -257,15 +261,13 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
257261
session.cwd ?? null,
258262
harness,
259263
billingMode,
260-
sourceUpdatedAt,
261-
sourceUpdatedAt,
262264
session.sessionId,
263265
);
264266
const isLive = existing.status === "active" && existing.ended_at == null;
265267
if (recentlyActive && !isLive) {
266-
reactivateSessionStmt.run(sourceUpdatedAt, session.sessionId);
268+
reactivateSessionStmt.run(now, session.sessionId);
267269
if (getAgentStmt.get(mainId)) {
268-
reactivateMainAgentStmt.run(sourceUpdatedAt, mainId);
270+
reactivateMainAgentStmt.run(now, mainId);
269271
}
270272
reactivated = true;
271273
}
@@ -327,7 +329,7 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
327329
strOf(input.subagent_type) ?? null,
328330
prompt ? prompt.slice(0, 500) : null,
329331
tu.timestamp ?? startedAt,
330-
tu.timestamp ?? sourceUpdatedAt,
332+
tu.timestamp ?? now,
331333
tu.timestamp ?? sourceUpdatedAt,
332334
mainId,
333335
);
@@ -352,6 +354,10 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
352354
deps.tokenUsage.replace(session.sessionId, model, counts, sourceUpdatedAt);
353355
}
354356

357+
if (existing != null && inserted > 0 && !reactivated) {
358+
touchSessionStmt.run(now, now, session.sessionId);
359+
}
360+
355361
db.exec("COMMIT");
356362

357363
const skipped = existing != null && inserted === 0 && !reactivated;

apps/desktop/test/collectors-import.test.ts

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ test("a new event with a later timestamp backfills without duplicating prior eve
148148
}
149149
});
150150

151-
test("backfill writes source timestamps instead of importer runtime", () => {
151+
test("backfill source-dates analytics while preserving the sync mutation cursor", () => {
152152
const { db, cleanup } = openTempDb();
153153
try {
154154
const importer = createImporter(db.connection, {
@@ -177,10 +177,12 @@ test("backfill writes source timestamps instead of importer runtime", () => {
177177
const session = db.sessions.getById("old-session");
178178
assert.ok(session);
179179
assert.equal(session.status, "completed");
180-
assert.equal(session.updatedAt, "2026-04-01T10:05:00.000Z");
180+
assert.equal(session.startedAt, "2026-04-01T10:00:00.000Z");
181+
assert.equal(session.endedAt, "2026-04-01T10:05:00.000Z");
182+
assert.equal(session.updatedAt, "2026-06-06T12:00:00.000Z");
181183

182184
const agent = db.agents.getBySession("old-session")[0];
183-
assert.equal(agent.updatedAt, "2026-04-01T10:05:00.000Z");
185+
assert.equal(agent.updatedAt, "2026-06-06T12:00:00.000Z");
184186

185187
const events = db.events.getBySession("old-session");
186188
assert.equal(events.length, 3);
@@ -238,7 +240,134 @@ test("backfill falls back missing event timestamps to the source session date",
238240

239241
const events = db.events.getBySession("missing-event-ts");
240242
assert.equal(events.length, 1);
241-
assert.equal(events[0].createdAt, "2026-04-02T10:00:00.000Z");
243+
assert.equal(events[0].createdAt, "2026-04-02T10:05:00.000Z");
244+
} finally {
245+
cleanup();
246+
}
247+
});
248+
249+
test("backfill appends new missing-timestamp events without high-water collision", () => {
250+
const { db, cleanup } = openTempDb();
251+
try {
252+
const importer = createImporter(db.connection, {
253+
tokenUsage: db.tokenUsage,
254+
detectBillingMode: () => "api",
255+
now: () => "2026-06-06T12:00:00.000Z",
256+
});
257+
258+
importer.importSession(
259+
makeSession({
260+
sessionId: "missing-event-append",
261+
startedAt: "2026-04-02T10:00:00.000Z",
262+
endedAt: "2026-04-02T10:05:00.000Z",
263+
messageTimestamps: [],
264+
toolUses: [{ name: "Read", timestamp: null, input: { file: "a" } }],
265+
}),
266+
"codex",
267+
);
268+
269+
importer.importSession(
270+
makeSession({
271+
sessionId: "missing-event-append",
272+
startedAt: "2026-04-02T10:00:00.000Z",
273+
endedAt: "2026-04-02T10:05:00.000Z",
274+
messageTimestamps: [],
275+
toolUses: [
276+
{ name: "Read", timestamp: null, input: { file: "a" } },
277+
{ name: "Read", timestamp: null, input: { file: "b" } },
278+
],
279+
}),
280+
"codex",
281+
);
282+
283+
const events = db.events.getBySession("missing-event-append");
284+
assert.equal(events.length, 2);
285+
assert.deepEqual(
286+
events.map((event) => event.createdAt),
287+
[
288+
"2026-04-02T10:05:00.000Z",
289+
"2026-04-02T10:05:00.001Z",
290+
],
291+
);
292+
} finally {
293+
cleanup();
294+
}
295+
});
296+
297+
test("historical backfill imported after sync cursor remains visible to incremental sync", () => {
298+
const { db, cleanup } = openTempDb();
299+
try {
300+
db.connection.prepare(`
301+
INSERT INTO sessions (id, name, status, started_at, updated_at)
302+
VALUES (?, ?, ?, ?, ?)
303+
`).run(
304+
"cursor-sentinel",
305+
"Cursor sentinel",
306+
"completed",
307+
"2026-06-06T11:59:00.000Z",
308+
"2026-06-06T12:00:00.000Z",
309+
);
310+
311+
const importer = createImporter(db.connection, {
312+
tokenUsage: db.tokenUsage,
313+
detectBillingMode: () => "api",
314+
now: () => "2026-06-06T12:01:00.000Z",
315+
});
316+
317+
importer.importSession(
318+
makeSession({
319+
sessionId: "old-after-cursor",
320+
startedAt: "2026-04-01T10:00:00.000Z",
321+
endedAt: "2026-04-01T10:05:00.000Z",
322+
messageTimestamps: [],
323+
toolUses: [],
324+
}),
325+
"codex",
326+
);
327+
328+
const rows = db.connection.prepare(`
329+
SELECT id
330+
FROM sessions
331+
WHERE updated_at >= ?
332+
ORDER BY updated_at DESC, id DESC
333+
`).all("2026-06-06T12:00:00.000Z") as Array<{ id: string }>;
334+
assert.ok(
335+
rows.some((row) => row.id === "old-after-cursor"),
336+
"new historical imports must remain visible to updated_at cursor sync",
337+
);
338+
const session = db.sessions.getById("old-after-cursor");
339+
assert.equal(session?.startedAt, "2026-04-01T10:00:00.000Z");
340+
assert.equal(session?.updatedAt, "2026-06-06T12:01:00.000Z");
341+
} finally {
342+
cleanup();
343+
}
344+
});
345+
346+
test("future-dated source activity does not mark a backfilled session active", () => {
347+
const { db, cleanup } = openTempDb();
348+
try {
349+
const importer = createImporter(db.connection, {
350+
tokenUsage: db.tokenUsage,
351+
detectBillingMode: () => "api",
352+
now: () => "2026-06-06T12:00:00.000Z",
353+
});
354+
355+
importer.importSession(
356+
makeSession({
357+
sessionId: "future-source",
358+
startedAt: "2026-06-06T12:30:00.000Z",
359+
endedAt: "2026-06-06T12:35:00.000Z",
360+
fileModifiedAt: Date.parse("2026-06-06T11:59:00.000Z"),
361+
messageTimestamps: [],
362+
toolUses: [],
363+
}),
364+
"codex",
365+
);
366+
367+
const session = db.sessions.getById("future-source");
368+
assert.ok(session);
369+
assert.equal(session.status, "completed");
370+
assert.equal(session.updatedAt, "2026-06-06T12:00:00.000Z");
242371
} finally {
243372
cleanup();
244373
}

0 commit comments

Comments
 (0)