chapterhouse 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@ import { withWikiWrite } from "../wiki/lock.js";
19
19
  import { listSkills, removeSkill } from "../copilot/skills.js";
20
20
  import { restartDaemon } from "../daemon.js";
21
21
  import { API_TOKEN_PATH, resolveWikiRelativePath } from "../paths.js";
22
- import { getDb, getSessionMessages, getTaskEvents } from "../store/db.js";
22
+ import { getDb, getSessionMessages, getTaskEvents, normalizeSqliteTsToIso } from "../store/db.js";
23
23
  import { getStatus, onStatusChange } from "../status.js";
24
24
  import { formatSseData, formatSseEvent } from "./sse.js";
25
25
  import { syncDecisionsFileToWiki } from "../squad/mirror.js";
@@ -614,7 +614,7 @@ app.get("/api/projects", (_req, res) => {
614
614
  squadDir: r.squad_dir,
615
615
  // Count from live filesystem — authoritative per Squad SDK rule: repo files win over cache.
616
616
  agentCount: countAgentsOnDisk(r.project_root),
617
- loadedAt: r.loaded_at,
617
+ loadedAt: normalizeSqliteTsToIso(r.loaded_at),
618
618
  lastUsedAt: r.last_used_at != null ? new Date(r.last_used_at).toISOString() : undefined,
619
619
  })));
620
620
  });
package/dist/store/db.js CHANGED
@@ -307,6 +307,28 @@ export function getRecentConversation(limit, sessionKey) {
307
307
  }
308
308
  const MAX_SESSION_MESSAGES_LIMIT = 500;
309
309
  const DEFAULT_SESSION_MESSAGES_LIMIT = 100;
310
+ /**
311
+ * Normalize a SQLite CURRENT_TIMESTAMP string to a proper ISO-8601 UTC string.
312
+ *
313
+ * SQLite stores CURRENT_TIMESTAMP as "YYYY-MM-DD HH:MM:SS" — no timezone
314
+ * marker, space separator. Browsers that receive this bare string may parse it
315
+ * as *local* time instead of UTC, shifting every displayed timestamp by the
316
+ * user's UTC offset. This helper appends the `Z` suffix (and replaces the space
317
+ * separator with `T`) so that `new Date(ts)` always parses as UTC.
318
+ *
319
+ * - Already-ISO strings (containing `T`) are returned unchanged (idempotent).
320
+ * - Strings with a `T` but no `Z` are left as-is; they are already ISO format
321
+ * and the caller is responsible for any timezone semantics (we do not blindly
322
+ * append `Z` and risk double-shifting a value that might already be local).
323
+ * - Falsy / empty input is returned as an empty string rather than throwing.
324
+ */
325
+ export function normalizeSqliteTsToIso(ts) {
326
+ if (!ts)
327
+ return "";
328
+ if (ts.includes("T"))
329
+ return ts;
330
+ return ts.replace(" ", "T") + "Z";
331
+ }
310
332
  /**
311
333
  * Return conversation_log rows for a specific session as structured JSON,
312
334
  * suitable for seeding the frontend Zustand store on mount.
@@ -328,7 +350,7 @@ export function getSessionMessages(sessionKey, limit) {
328
350
  return rows.map((r) => ({
329
351
  role: r.role,
330
352
  content: r.content,
331
- ts: r.ts,
353
+ ts: normalizeSqliteTsToIso(r.ts),
332
354
  }));
333
355
  }
334
356
  /**
@@ -280,4 +280,47 @@ test("#86: agent_task_events table exists in schema after getDb()", async () =>
280
280
  dbModule.closeDb();
281
281
  }
282
282
  });
283
+ // ---------------------------------------------------------------------------
284
+ // normalizeSqliteTsToIso — unit tests
285
+ // ---------------------------------------------------------------------------
286
+ test("normalizeSqliteTsToIso converts SQLite space-separated UTC string to ISO-8601", async () => {
287
+ const dbModule = await loadDbModule();
288
+ assert.equal(dbModule.normalizeSqliteTsToIso("2026-05-09 00:54:31"), "2026-05-09T00:54:31Z", "space-separated string should become T-separated with Z suffix");
289
+ });
290
+ test("normalizeSqliteTsToIso is idempotent for already-ISO strings ending with Z", async () => {
291
+ const dbModule = await loadDbModule();
292
+ assert.equal(dbModule.normalizeSqliteTsToIso("2026-05-09T00:54:31Z"), "2026-05-09T00:54:31Z", "already-ISO string should be returned unchanged");
293
+ });
294
+ test("normalizeSqliteTsToIso leaves T-but-no-Z strings untouched", async () => {
295
+ // Strings that already contain T are considered already in ISO format.
296
+ // We do NOT append Z because the timezone semantics are unknown — the caller
297
+ // is responsible. Appending Z blindly could double-shift a local-time value.
298
+ const dbModule = await loadDbModule();
299
+ assert.equal(dbModule.normalizeSqliteTsToIso("2026-05-09T00:54:31"), "2026-05-09T00:54:31", "T-but-no-Z string should be left as-is");
300
+ });
301
+ test("normalizeSqliteTsToIso handles empty and nullish input gracefully", async () => {
302
+ const dbModule = await loadDbModule();
303
+ assert.equal(dbModule.normalizeSqliteTsToIso(""), "", "empty string → empty string");
304
+ assert.equal(dbModule.normalizeSqliteTsToIso(null), "", "null → empty string");
305
+ assert.equal(dbModule.normalizeSqliteTsToIso(undefined), "", "undefined → empty string");
306
+ });
307
+ // ---------------------------------------------------------------------------
308
+ // getSessionMessages — integration: ts field is normalized to ISO UTC
309
+ // ---------------------------------------------------------------------------
310
+ test("getSessionMessages returns ts values normalized to ISO-8601 UTC (ends with Z)", async () => {
311
+ const dbModule = await loadDbModule();
312
+ try {
313
+ const db = dbModule.getDb();
314
+ // Insert a row with a bare SQLite-format timestamp (no T, no Z)
315
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, ts)
316
+ VALUES ('user', 'timezone test', 'web', 'tz-session', '2026-05-09 01:23:45')`).run();
317
+ const messages = dbModule.getSessionMessages("tz-session");
318
+ assert.equal(messages.length, 1, "should return the inserted row");
319
+ assert.equal(messages[0].ts, "2026-05-09T01:23:45Z", "ts must be normalized to ISO-8601 UTC");
320
+ assert.ok(messages[0].ts.endsWith("Z"), "ts must end with Z");
321
+ }
322
+ finally {
323
+ dbModule.closeDb();
324
+ }
325
+ });
283
326
  //# sourceMappingURL=db.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Chapterhouse — a team-level AI assistant for engineering teams, built on the GitHub Copilot SDK. Web UI only.",
5
5
  "bin": {
6
6
  "chapterhouse": "dist/cli.js"