chapterhouse 0.4.1 → 0.4.2

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.
@@ -23,9 +23,9 @@ import { withWikiWrite } from "../wiki/lock.js";
23
23
  import { listSkills, removeSkill } from "../copilot/skills.js";
24
24
  import { restartDaemon } from "../daemon.js";
25
25
  import { API_TOKEN_PATH } from "../paths.js";
26
- import { getDb, getSessionMessages, getTaskEvents } from "../store/db.js";
26
+ import { getCurrentRunId, getDb, getSessionMessages, getTaskEvents } from "../store/db.js";
27
27
  import { getTaskLogEvents, subscribeTaskLog } from "../copilot/task-event-log.js";
28
- import { subscribeSession, getSessionEventsFromDb, oldestSessionSeq, } from "../copilot/turn-event-log.js";
28
+ import { subscribeSession, getSessionEventsFromDb, getSessionMaxSeqFromDb, oldestSessionSeq, } from "../copilot/turn-event-log.js";
29
29
  import { getStatus, onStatusChange } from "../status.js";
30
30
  import { formatSseData, formatSseEvent } from "./sse.js";
31
31
  import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
@@ -728,6 +728,7 @@ if (config.chatSseEnabled) {
728
728
  const sessionKey = Array.isArray(req.params.key) ? req.params.key[0] : req.params.key;
729
729
  if (!sessionKey)
730
730
  throw new BadRequestError("Missing session key");
731
+ const includeHistorical = req.query.include === "all";
731
732
  res.setHeader("Content-Type", "text/event-stream");
732
733
  res.setHeader("Cache-Control", "no-cache");
733
734
  res.setHeader("Connection", "keep-alive");
@@ -738,6 +739,10 @@ if (config.chatSseEnabled) {
738
739
  const lastSeq = rawLastId && !Array.isArray(rawLastId) && /^\d+$/.test(rawLastId.trim())
739
740
  ? parseInt(rawLastId.trim(), 10)
740
741
  : undefined;
742
+ const maxCurrentSeq = getSessionMaxSeqFromDb(sessionKey, { includeHistorical });
743
+ const effectiveLastSeq = maxCurrentSeq !== undefined && lastSeq !== undefined && lastSeq > maxCurrentSeq
744
+ ? 0
745
+ : lastSeq;
741
746
  // Helper: send a named SSE event with an id: field
742
747
  const sendEvent = (event, seq) => {
743
748
  const payload = JSON.stringify(event);
@@ -745,13 +750,13 @@ if (config.chatSseEnabled) {
745
750
  };
746
751
  // If Last-Event-ID is present and the session ring buffer doesn't cover it,
747
752
  // fall back to SQLite for replay of completed turns.
748
- let replayHighSeq = lastSeq;
749
- if (lastSeq !== undefined) {
753
+ let replayHighSeq = effectiveLastSeq;
754
+ if (effectiveLastSeq !== undefined) {
750
755
  const oldestBuf = oldestSessionSeq(sessionKey);
751
- const bufferMissesRange = oldestBuf === undefined || oldestBuf > lastSeq + 1;
756
+ const bufferMissesRange = oldestBuf === undefined || oldestBuf > effectiveLastSeq + 1;
752
757
  if (bufferMissesRange) {
753
758
  // Replay from SQLite (completed turns)
754
- const dbEvents = getSessionEventsFromDb(sessionKey, lastSeq);
759
+ const dbEvents = getSessionEventsFromDb(sessionKey, effectiveLastSeq, { includeHistorical });
755
760
  for (const e of dbEvents) {
756
761
  sendEvent(e, e._seq);
757
762
  if (replayHighSeq === undefined || e._seq > replayHighSeq)
@@ -766,7 +771,7 @@ if (config.chatSseEnabled) {
766
771
  sendEvent(e, e._seq);
767
772
  }, replayHighSeq);
768
773
  // Send connected event
769
- res.write(`: connected session=${sessionKey}\n\n`);
774
+ res.write(`: connected session=${sessionKey} run=${getCurrentRunId()}\n\n`);
770
775
  // Keep-alive every 15 s
771
776
  const keepAlive = setInterval(() => {
772
777
  res.write(`: keep-alive\n\n`);
@@ -1063,7 +1068,8 @@ app.get("/api/session/:sessionKey/messages", (req, res) => {
1063
1068
  if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
1064
1069
  throw new BadRequestError("'limit' must be a positive integer");
1065
1070
  }
1066
- const messages = getSessionMessages(sessionKey, limit);
1071
+ const includeHistorical = req.query.include === "all";
1072
+ const messages = getSessionMessages(sessionKey, limit, { includeHistorical });
1067
1073
  res.json({ sessionKey, messages });
1068
1074
  });
1069
1075
  app.use(apiNotFoundHandler);
@@ -371,6 +371,36 @@ test("server worker detail returns the stored dispatched prompt", async () => {
371
371
  assert.equal(body.completedAt, null);
372
372
  });
373
373
  });
374
+ test("server session message hydration returns current run by default and include=all returns history", async () => {
375
+ await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
376
+ const db = new Database(join(testRoot, ".chapterhouse", "chapterhouse.db"));
377
+ try {
378
+ const currentRun = db.prepare(`SELECT run_id FROM daemon_runs LIMIT 1`).get();
379
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, run_id)
380
+ VALUES ('user', ?, 'web', 'hydration-session', ?)`).run("previous run", "previous-run");
381
+ db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, run_id)
382
+ VALUES ('assistant', ?, 'web', 'hydration-session', ?)`).run("current run", currentRun.run_id);
383
+ }
384
+ finally {
385
+ db.close();
386
+ }
387
+ const currentOnly = await fetch(`${baseUrl}/api/session/hydration-session/messages`, {
388
+ headers: { authorization: authHeader },
389
+ });
390
+ assert.equal(currentOnly.status, 200);
391
+ assert.deepEqual((await currentOnly.json()).messages.map((message) => message.content), [
392
+ "current run",
393
+ ]);
394
+ const allRuns = await fetch(`${baseUrl}/api/session/hydration-session/messages?include=all`, {
395
+ headers: { authorization: authHeader },
396
+ });
397
+ assert.equal(allRuns.status, 200);
398
+ assert.deepEqual((await allRuns.json()).messages.map((message) => message.content), [
399
+ "previous run",
400
+ "current run",
401
+ ]);
402
+ });
403
+ });
374
404
  test("server wiki route synthesizes a welcome page when pages/index.md is missing", async () => {
375
405
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
376
406
  rmSync(join(testRoot, ".chapterhouse", "wiki", "pages", "index.md"), { force: true });
@@ -1104,11 +1104,11 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
1104
1104
  }
1105
1105
  catch { /* best-effort */ }
1106
1106
  try {
1107
- logConversation(logRole, prompt, sourceLabel, sessionKey);
1107
+ logConversation(logRole, prompt, sourceLabel, sessionKey, turnId);
1108
1108
  }
1109
1109
  catch { /* best-effort */ }
1110
1110
  try {
1111
- logConversation("assistant", finalContent, sourceLabel, sessionKey);
1111
+ logConversation("assistant", finalContent, sourceLabel, sessionKey, turnId);
1112
1112
  }
1113
1113
  catch { /* best-effort */ }
1114
1114
  scheduleCheckpointExtraction(sessionKey, prompt, finalContent, source);
@@ -1205,11 +1205,11 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
1205
1205
  }
1206
1206
  catch { /* best-effort */ }
1207
1207
  try {
1208
- logConversation("user", newPrompt, sourceLabel, sessionKey);
1208
+ logConversation("user", newPrompt, sourceLabel, sessionKey, turnId);
1209
1209
  }
1210
1210
  catch { /* best-effort */ }
1211
1211
  try {
1212
- logConversation("assistant", finalContent, sourceLabel, sessionKey);
1212
+ logConversation("assistant", finalContent, sourceLabel, sessionKey, turnId);
1213
1213
  }
1214
1214
  catch { /* best-effort */ }
1215
1215
  scheduleCheckpointExtraction(sessionKey, newPrompt, finalContent, source);
@@ -20,7 +20,7 @@
20
20
  * @module copilot/turn-event-log
21
21
  */
22
22
  import { childLogger } from "../util/logger.js";
23
- import { getDb } from "../store/db.js";
23
+ import { getCurrentRunId, getDb } from "../store/db.js";
24
24
  import { config } from "../config.js";
25
25
  import { RingBuffer } from "./ring-buffer.js";
26
26
  const log = childLogger("turn-event-log");
@@ -192,9 +192,9 @@ export function subscribeSession(sessionKey, listener, afterSeq) {
192
192
  function persistIndexedTurnEvent(sessionKey, event) {
193
193
  try {
194
194
  const db = getDb();
195
- const stmt = db.prepare(`INSERT INTO turn_events (turn_id, session_key, seq, ts, event_type, payload)
196
- VALUES (?, ?, ?, ?, ?, ?)`);
197
- stmt.run(event.turnId, sessionKey, event._seq, event._ts, event.type, JSON.stringify(event));
195
+ const stmt = db.prepare(`INSERT INTO turn_events (turn_id, session_key, run_id, seq, ts, event_type, payload)
196
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
197
+ stmt.run(event.turnId, sessionKey, getCurrentRunId(), event._seq, event._ts, event.type, JSON.stringify(event));
198
198
  }
199
199
  catch (err) {
200
200
  log.warn({ err: err instanceof Error ? err.message : err, turnId: event.turnId }, "turn-event-log: SQLite persist failed");
@@ -260,19 +260,24 @@ export function getTurnEventsFromDb(turnId, afterSeq = 0) {
260
260
  return [];
261
261
  }
262
262
  }
263
- /**
264
- * Return persisted events for a session from SQLite, after a given sequence number.
265
- * Used as SSE replay fallback when the session buffer doesn't cover the requested range.
266
- */
267
- export function getSessionEventsFromDb(sessionKey, afterSeq = 0) {
263
+ export function getSessionEventsFromDb(sessionKey, afterSeq = 0, options = {}) {
268
264
  try {
269
265
  const db = getDb();
270
- const rows = db
271
- .prepare(`SELECT payload FROM turn_events
272
- WHERE session_key = ? AND seq > ?
273
- ORDER BY seq ASC
274
- LIMIT ?`)
275
- .all(sessionKey, afterSeq, SESSION_REPLAY_LIMIT);
266
+ const includeHistorical = options.includeHistorical ?? false;
267
+ const runId = options.runId ?? getCurrentRunId();
268
+ const rows = includeHistorical
269
+ ? db
270
+ .prepare(`SELECT payload FROM turn_events
271
+ WHERE session_key = ? AND seq > ?
272
+ ORDER BY id ASC
273
+ LIMIT ?`)
274
+ .all(sessionKey, afterSeq, SESSION_REPLAY_LIMIT)
275
+ : db
276
+ .prepare(`SELECT payload FROM turn_events
277
+ WHERE session_key = ? AND run_id = ? AND seq > ?
278
+ ORDER BY seq ASC
279
+ LIMIT ?`)
280
+ .all(sessionKey, runId, afterSeq, SESSION_REPLAY_LIMIT);
276
281
  return rows.map((r) => JSON.parse(r.payload));
277
282
  }
278
283
  catch (err) {
@@ -280,6 +285,21 @@ export function getSessionEventsFromDb(sessionKey, afterSeq = 0) {
280
285
  return [];
281
286
  }
282
287
  }
288
+ export function getSessionMaxSeqFromDb(sessionKey, options = {}) {
289
+ try {
290
+ const db = getDb();
291
+ const includeHistorical = options.includeHistorical ?? false;
292
+ const runId = options.runId ?? getCurrentRunId();
293
+ const row = includeHistorical
294
+ ? db.prepare(`SELECT MAX(seq) AS max_seq FROM turn_events WHERE session_key = ?`).get(sessionKey)
295
+ : db.prepare(`SELECT MAX(seq) AS max_seq FROM turn_events WHERE session_key = ? AND run_id = ?`).get(sessionKey, runId);
296
+ return row?.max_seq ?? undefined;
297
+ }
298
+ catch (err) {
299
+ log.warn({ err: err instanceof Error ? err.message : err, sessionKey }, "turn-event-log: SQLite session max-seq read failed");
300
+ return undefined;
301
+ }
302
+ }
283
303
  // ---------------------------------------------------------------------------
284
304
  // Diagnostics
285
305
  // ---------------------------------------------------------------------------
@@ -119,6 +119,37 @@ describe("turn-event-log", () => {
119
119
  assert.equal(persisted[0].type, "turn:started");
120
120
  assert.equal(persisted[0].turnId, turnId);
121
121
  });
122
+ it("replays persisted session events from the current daemon run by default and can include historical runs", () => {
123
+ const session = freshSessionKey();
124
+ const db = getDb();
125
+ const columns = db.prepare("PRAGMA table_info(turn_events)").all();
126
+ if (!columns.some((column) => column.name === "run_id")) {
127
+ db.exec("ALTER TABLE turn_events ADD COLUMN run_id TEXT");
128
+ }
129
+ const insert = db.prepare(`INSERT INTO turn_events (turn_id, session_key, seq, ts, event_type, payload, run_id)
130
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
131
+ const previous = {
132
+ type: "turn:complete",
133
+ turnId: "previous-turn",
134
+ sessionKey: session,
135
+ finalMessage: "previous",
136
+ _seq: 10,
137
+ _ts: 10,
138
+ };
139
+ const current = {
140
+ type: "turn:complete",
141
+ turnId: "current-turn",
142
+ sessionKey: session,
143
+ finalMessage: "current",
144
+ _seq: 11,
145
+ _ts: 11,
146
+ };
147
+ insert.run(previous.turnId, session, previous._seq, previous._ts, previous.type, JSON.stringify(previous), "previous-run");
148
+ insert.run(current.turnId, session, current._seq, current._ts, current.type, JSON.stringify(current), "current-run");
149
+ const getEvents = getSessionEventsFromDb;
150
+ assert.deepEqual(getEvents(session, 0, { runId: "current-run" }).map((event) => event.turnId), ["current-turn"]);
151
+ assert.deepEqual(getEvents(session, 0, { runId: "current-run", includeHistorical: true }).map((event) => event.turnId), ["previous-turn", "current-turn"]);
152
+ });
122
153
  });
123
154
  describe("subscribeTurn", () => {
124
155
  it("replays existing buffered events immediately on subscribe", () => {
@@ -65,7 +65,7 @@ test("runEndOfTaskMemoryHook does nothing when CHAPTERHOUSE_MEMORY_EOT_HOOK_ENAB
65
65
  },
66
66
  });
67
67
  assert.equal(llmCalls, 0);
68
- assert.equal(listObservations({ scope_id: chapterhouse.id }).length, 0);
68
+ assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Disabled hooks must not persist memory."), false);
69
69
  const row = db.prepare(`SELECT status FROM mem_inbox WHERE id = ?`).get(Number(inserted.lastInsertRowid));
70
70
  assert.equal(row.status, "pending");
71
71
  });
@@ -185,7 +185,7 @@ test("runEndOfTaskMemoryHook leaves reviewed proposals pending when CHAPTERHOUSE
185
185
  implicit_memories: [],
186
186
  }),
187
187
  });
188
- assert.equal(listObservations({ scope_id: chapterhouse.id }).length, 0);
188
+ assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Review can approve this, but auto-accept is disabled."), false);
189
189
  const row = db.prepare(`SELECT status FROM mem_inbox WHERE id = ?`).get(Number(inserted.lastInsertRowid));
190
190
  assert.equal(row.status, "pending");
191
191
  });
@@ -214,7 +214,7 @@ test("runEndOfTaskMemoryHook rejects pending same-task proposals omitted by the
214
214
  implicit_memories: [],
215
215
  }),
216
216
  });
217
- assert.equal(listObservations({ scope_id: chapterhouse.id }).length, 0);
217
+ assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Reviewer omissions should not silently leave proposals pending."), false);
218
218
  const row = db.prepare(`
219
219
  SELECT status, resolution_reason
220
220
  FROM mem_inbox
@@ -45,30 +45,30 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
45
45
  const team = getScope("team");
46
46
  assert.ok(chapterhouse && team);
47
47
  const first = recordObservation({
48
- scope_id: chapterhouse.id,
48
+ scope_id: team.id,
49
49
  content: "The worker event stream uses server sent events for live task output.",
50
50
  source: "test",
51
51
  confidence: 0.4,
52
52
  });
53
53
  const keeper = recordObservation({
54
- scope_id: chapterhouse.id,
54
+ scope_id: team.id,
55
55
  content: "Worker event streams use server sent events for live task output.",
56
56
  source: "test",
57
57
  confidence: 0.9,
58
58
  });
59
59
  const third = recordObservation({
60
- scope_id: chapterhouse.id,
60
+ scope_id: team.id,
61
61
  content: "The worker event stream uses server sent events for live task output today.",
62
62
  source: "test",
63
63
  confidence: 0.9,
64
64
  });
65
65
  const otherScope = recordObservation({
66
- scope_id: team.id,
66
+ scope_id: chapterhouse.id,
67
67
  content: "Worker event streams use server sent events for live task output.",
68
68
  source: "test",
69
69
  confidence: 0.1,
70
70
  });
71
- const summary = housekeepingModule.dedupObservationsPass(chapterhouse.id);
71
+ const summary = housekeepingModule.dedupObservationsPass(team.id);
72
72
  assert.equal(summary.pass, "dedupObservationsPass");
73
73
  assert.equal(summary.examined, 3);
74
74
  assert.equal(summary.modified, 2);
@@ -79,7 +79,7 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
79
79
  { id: third.id, superseded_by: keeper.id },
80
80
  ]);
81
81
  assert.equal(db.prepare(`SELECT superseded_by FROM mem_observations WHERE id = ?`).get(otherScope.id).superseded_by, null);
82
- const second = housekeepingModule.dedupObservationsPass(chapterhouse.id);
82
+ const second = housekeepingModule.dedupObservationsPass(team.id);
83
83
  assert.equal(second.modified, 0);
84
84
  });
85
85
  test("dedupDecisionsPass supersedes similar active decisions within scope and keeps the latest decision", async () => {
@@ -88,39 +88,39 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
88
88
  const getScope = getFunction(memoryModule, "getScope");
89
89
  const upsertEntity = getFunction(memoryModule, "upsertEntity");
90
90
  const recordDecision = getFunction(memoryModule, "recordDecision");
91
- const chapterhouse = getScope("chapterhouse");
92
91
  const team = getScope("team");
93
- assert.ok(chapterhouse && team);
94
- const api = upsertEntity({ scope_id: chapterhouse.id, kind: "component", name: "api" });
95
- const web = upsertEntity({ scope_id: chapterhouse.id, kind: "component", name: "web" });
92
+ const infra = getScope("infra");
93
+ assert.ok(team && infra);
94
+ const api = upsertEntity({ scope_id: team.id, kind: "component", name: "api" });
95
+ const web = upsertEntity({ scope_id: team.id, kind: "component", name: "web" });
96
96
  const oldDecision = recordDecision({
97
- scope_id: chapterhouse.id,
97
+ scope_id: team.id,
98
98
  entity_id: api.id,
99
99
  title: "Use SQLite FTS5 for memory recall",
100
100
  rationale: "Initial choice.",
101
101
  decided_at: "2026-05-11",
102
102
  });
103
103
  const keeper = recordDecision({
104
- scope_id: chapterhouse.id,
104
+ scope_id: team.id,
105
105
  entity_id: api.id,
106
106
  title: "Use SQLite FTS5 for scoped memory recall",
107
107
  rationale: "Latest choice.",
108
108
  decided_at: "2026-05-13",
109
109
  });
110
110
  const otherEntity = recordDecision({
111
- scope_id: chapterhouse.id,
111
+ scope_id: team.id,
112
112
  entity_id: web.id,
113
113
  title: "Use SQLite FTS5 for memory recall",
114
114
  rationale: "Same title, different entity context, but newer scope-level decision.",
115
115
  decided_at: "2026-05-14",
116
116
  });
117
117
  const otherScope = recordDecision({
118
- scope_id: team.id,
118
+ scope_id: infra.id,
119
119
  title: "Use SQLite FTS5 for memory recall",
120
120
  rationale: "Same title, different scope.",
121
121
  decided_at: "2026-05-14",
122
122
  });
123
- const summary = housekeepingModule.dedupDecisionsPass(chapterhouse.id);
123
+ const summary = housekeepingModule.dedupDecisionsPass(team.id);
124
124
  assert.equal(summary.pass, "dedupDecisionsPass");
125
125
  assert.equal(summary.examined, 3);
126
126
  assert.equal(summary.modified, 2);
@@ -129,7 +129,7 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
129
129
  assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(keeper.id).superseded_by, otherEntity.id);
130
130
  assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherEntity.id).superseded_by, null);
131
131
  assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherScope.id).superseded_by, null);
132
- const second = housekeepingModule.dedupDecisionsPass(chapterhouse.id);
132
+ const second = housekeepingModule.dedupDecisionsPass(team.id);
133
133
  assert.equal(second.modified, 0);
134
134
  });
135
135
  test("orphanCleanupPass clears missing observation entity references without touching valid or out-of-scope rows", async () => {
@@ -225,18 +225,18 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
225
225
  const upsertEntity = getFunction(memoryModule, "upsertEntity");
226
226
  const recordObservation = getFunction(memoryModule, "recordObservation");
227
227
  const recordDecision = getFunction(memoryModule, "recordDecision");
228
- const chapterhouse = getScope("chapterhouse");
229
- assert.ok(chapterhouse);
230
- const entity = upsertEntity({ scope_id: chapterhouse.id, kind: "component", name: "memory", tier: "warm" });
228
+ const team = getScope("team");
229
+ assert.ok(team);
230
+ const entity = upsertEntity({ scope_id: team.id, kind: "component", name: "memory", tier: "warm" });
231
231
  const referencedObservation = recordObservation({
232
- scope_id: chapterhouse.id,
232
+ scope_id: team.id,
233
233
  entity_id: entity.id,
234
234
  content: "Referenced by a recent decision through its entity.",
235
235
  source: "test",
236
236
  tier: "warm",
237
237
  });
238
238
  const recentDecision = recordDecision({
239
- scope_id: chapterhouse.id,
239
+ scope_id: team.id,
240
240
  entity_id: entity.id,
241
241
  title: "Restate memory tiering decision",
242
242
  rationale: "Recent entity-linked decisions keep related observations hot.",
@@ -244,20 +244,20 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
244
244
  tier: "warm",
245
245
  });
246
246
  const oldHot = recordObservation({
247
- scope_id: chapterhouse.id,
247
+ scope_id: team.id,
248
248
  content: "Old hot row with no recall activity should cool down.",
249
249
  source: "test",
250
250
  tier: "hot",
251
251
  });
252
252
  const staleLowConfidence = recordObservation({
253
- scope_id: chapterhouse.id,
253
+ scope_id: team.id,
254
254
  content: "Low confidence stale row should go cold.",
255
255
  source: "test",
256
256
  tier: "warm",
257
257
  confidence: 0.2,
258
258
  });
259
259
  const archived = recordObservation({
260
- scope_id: chapterhouse.id,
260
+ scope_id: team.id,
261
261
  content: "Archived row should always be cold.",
262
262
  source: "test",
263
263
  tier: "hot",
@@ -265,7 +265,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
265
265
  db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-45 days') WHERE id = ?`).run(oldHot.id);
266
266
  db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-61 days') WHERE id = ?`).run(staleLowConfidence.id);
267
267
  db.prepare(`UPDATE mem_observations SET archived_at = CURRENT_TIMESTAMP WHERE id = ?`).run(archived.id);
268
- const summary = housekeepingModule.tieringPass(chapterhouse.id);
268
+ const summary = housekeepingModule.tieringPass(team.id);
269
269
  assert.equal(summary.pass, "tieringPass");
270
270
  assert.equal(summary.modified, 5);
271
271
  assert.deepEqual(summary.errors, []);
@@ -274,7 +274,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
274
274
  assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(oldHot.id).tier, "warm");
275
275
  assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(staleLowConfidence.id).tier, "cold");
276
276
  assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(archived.id).tier, "cold");
277
- const second = housekeepingModule.tieringPass(chapterhouse.id);
277
+ const second = housekeepingModule.tieringPass(team.id);
278
278
  assert.equal(second.modified, 0);
279
279
  });
280
280
  //# sourceMappingURL=housekeeping.test.js.map