fix(pi): parse session age timestamps as UTC (#488)

Co-authored-by: s010mn <s010mn@users.noreply.github.com>
This commit is contained in:
s010mn
2026-05-09 12:11:44 +03:00
committed by GitHub
co-authored by s010mn
parent 990586a2d6
commit 228c34d595
2 changed files with 79 additions and 3 deletions
+21 -3
View File
@@ -151,6 +151,22 @@ function deriveSessionId(ctx: Record<string, unknown>): string {
return `pi-${Date.now()}`;
}
/**
* Parse SessionDB timestamps as UTC. SQLite datetime('now') returns
* "YYYY-MM-DD HH:MM:SS" in UTC without a timezone suffix; JavaScript parses
* that shape as local time, which skews ages by the local UTC offset.
*/
function parseSessionTimestampMs(value: string): number {
const trimmed = value.trim();
const sqliteUtc = trimmed.match(
/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)?$/,
);
const normalized = sqliteUtc
? `${sqliteUtc[1]}T${sqliteUtc[2]}${sqliteUtc[3] ?? ""}Z`
: trimmed;
return Date.parse(normalized);
}
/** Build stats text for the /ctx-stats command. */
function buildStatsText(db: SessionDB, sessionId: string): string {
try {
@@ -179,9 +195,11 @@ function buildStatsText(db: SessionDB, sessionId: string): string {
// Session age
if (stats?.started_at) {
const startedMs = new Date(stats.started_at).getTime();
const ageMinutes = Math.round((Date.now() - startedMs) / 60_000);
lines.push(`- Session age: ${ageMinutes}m`);
const startedMs = parseSessionTimestampMs(stats.started_at);
if (Number.isFinite(startedMs)) {
const ageMinutes = Math.round((Date.now() - startedMs) / 60_000);
lines.push(`- Session age: ${ageMinutes}m`);
}
}
return lines.join("\n");
+58
View File
@@ -21,6 +21,7 @@ import { createHash } from "node:crypto";
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { SessionDB } from "../src/session/db.js";
// ── Mock Pi API ──────────────────────────────────────────────
@@ -534,6 +535,63 @@ describe("Pi Extension", () => {
const result = await cmd!.handler!({});
expect(result).toBeDefined();
});
it("/ctx-stats treats SQLite started_at as UTC", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-09T12:30:00Z"));
const originalTZ = process.env.TZ;
process.env.TZ = "America/Los_Angeles";
const sessionFile = join(tempDir, "stats-utc-session.jsonl");
const sessionId = createHash("sha256")
.update(sessionFile)
.digest("hex")
.slice(0, 16);
try {
await registerPiExtension(api);
await api._trigger(
"session_start",
{ type: "session_start", reason: "startup" },
{ sessionManager: { getSessionFile: () => sessionFile } },
);
const dbPath = join(
process.env.HOME!,
".pi",
"context-mode",
"sessions",
"context-mode.db",
);
const db = new SessionDB({ dbPath });
try {
// SQLite datetime('now') stores UTC as "YYYY-MM-DD HH:MM:SS".
// If parsed as local time in America/Los_Angeles, this would be
// 2026-05-09T19:00:00Z and the age would not be 30 minutes.
db.db
.prepare(
"UPDATE session_meta SET started_at = ? WHERE session_id = ?",
)
.run("2026-05-09 12:00:00", sessionId);
} finally {
db.close();
}
const result = await api._getCommand("ctx-stats")!.handler!({});
expect((result as { text: string }).text).toContain(
"- Session age: 30m",
);
} finally {
if (originalTZ === undefined) {
delete process.env.TZ;
} else {
process.env.TZ = originalTZ;
}
vi.useRealTimers();
}
});
});
// ═══════════════════════════════════════════════════════════