chapterhouse 0.3.25 → 0.4.0

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.
Files changed (57) hide show
  1. package/dist/api/server-runtime.js +1 -1
  2. package/dist/api/server.js +13 -1
  3. package/dist/api/server.test.js +68 -54
  4. package/dist/api/sse.integration.test.js +4 -46
  5. package/dist/api/turn-sse.integration.test.js +20 -47
  6. package/dist/config.js +81 -1
  7. package/dist/config.test.js +123 -0
  8. package/dist/copilot/agents.js +27 -4
  9. package/dist/copilot/agents.test.js +7 -0
  10. package/dist/copilot/oneshot.js +54 -0
  11. package/dist/copilot/orchestrator.js +228 -4
  12. package/dist/copilot/orchestrator.test.js +373 -1
  13. package/dist/copilot/system-message.js +4 -0
  14. package/dist/copilot/system-message.test.js +24 -0
  15. package/dist/copilot/tools.agent.test.js +23 -0
  16. package/dist/copilot/tools.js +350 -4
  17. package/dist/copilot/tools.memory.test.js +248 -0
  18. package/dist/copilot/turn-event-log-env.test.js +19 -0
  19. package/dist/copilot/turn-event-log.js +22 -23
  20. package/dist/copilot/turn-event-log.test.js +61 -2
  21. package/dist/memory/active-scope.js +69 -0
  22. package/dist/memory/active-scope.test.js +76 -0
  23. package/dist/memory/checkpoint-prompt.js +71 -0
  24. package/dist/memory/checkpoint.js +257 -0
  25. package/dist/memory/checkpoint.test.js +255 -0
  26. package/dist/memory/decisions.js +53 -0
  27. package/dist/memory/decisions.test.js +92 -0
  28. package/dist/memory/entities.js +59 -0
  29. package/dist/memory/entities.test.js +65 -0
  30. package/dist/memory/eot.js +219 -0
  31. package/dist/memory/eot.test.js +263 -0
  32. package/dist/memory/hot-tier.js +187 -0
  33. package/dist/memory/hot-tier.test.js +197 -0
  34. package/dist/memory/housekeeping.js +352 -0
  35. package/dist/memory/housekeeping.test.js +280 -0
  36. package/dist/memory/inbox.js +73 -0
  37. package/dist/memory/index.js +11 -0
  38. package/dist/memory/observations.js +46 -0
  39. package/dist/memory/observations.test.js +86 -0
  40. package/dist/memory/recall.js +197 -0
  41. package/dist/memory/recall.test.js +196 -0
  42. package/dist/memory/scopes.js +89 -0
  43. package/dist/memory/scopes.test.js +201 -0
  44. package/dist/memory/tiering.js +193 -0
  45. package/dist/memory/types.js +2 -0
  46. package/dist/paths.js +7 -1
  47. package/dist/store/db.js +423 -17
  48. package/dist/store/db.test.js +94 -7
  49. package/dist/test/api-server.js +50 -0
  50. package/dist/test/api-server.test.js +57 -0
  51. package/dist/test/setup-env.js +25 -0
  52. package/dist/test/setup-env.test.js +38 -0
  53. package/package.json +1 -1
  54. package/web/dist/assets/{index-BRPJa1DK.js → index-DmYLALt0.js} +70 -70
  55. package/web/dist/assets/index-DmYLALt0.js.map +1 -0
  56. package/web/dist/index.html +1 -1
  57. package/web/dist/assets/index-BRPJa1DK.js.map +0 -1
@@ -0,0 +1,280 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ const repoRoot = process.cwd();
6
+ const sandboxRoot = join(repoRoot, ".test-work", `memory-housekeeping-${process.pid}`);
7
+ const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
8
+ process.env.CHAPTERHOUSE_HOME = sandboxRoot;
9
+ process.env.CHAPTERHOUSE_MEMORY_DECAY_DAYS = "30";
10
+ process.env.CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS = "7";
11
+ function resetSandbox() {
12
+ mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
13
+ rmSync(sandboxRoot, { recursive: true, force: true });
14
+ mkdirSync(chapterhouseHome, { recursive: true });
15
+ }
16
+ async function loadModules() {
17
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
18
+ const memoryModule = await import(new URL("./index.js", import.meta.url).href);
19
+ const housekeepingModule = await import(new URL("./housekeeping.js", import.meta.url).href);
20
+ return { dbModule, memoryModule, housekeepingModule };
21
+ }
22
+ function getFunction(module, name) {
23
+ const value = module[name];
24
+ assert.equal(typeof value, "function", `expected ${name} to be exported`);
25
+ return value;
26
+ }
27
+ test.beforeEach(async () => {
28
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
29
+ dbModule.closeDb();
30
+ resetSandbox();
31
+ });
32
+ test.after(async () => {
33
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
34
+ dbModule.closeDb();
35
+ rmSync(sandboxRoot, { recursive: true, force: true });
36
+ delete process.env.CHAPTERHOUSE_MEMORY_DECAY_DAYS;
37
+ delete process.env.CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS;
38
+ });
39
+ test("dedupObservationsPass supersedes similar observations in scope deterministically and is idempotent", async () => {
40
+ const { dbModule, memoryModule, housekeepingModule } = await loadModules();
41
+ const db = dbModule.getDb();
42
+ const getScope = getFunction(memoryModule, "getScope");
43
+ const recordObservation = getFunction(memoryModule, "recordObservation");
44
+ const chapterhouse = getScope("chapterhouse");
45
+ const team = getScope("team");
46
+ assert.ok(chapterhouse && team);
47
+ const first = recordObservation({
48
+ scope_id: chapterhouse.id,
49
+ content: "The worker event stream uses server sent events for live task output.",
50
+ source: "test",
51
+ confidence: 0.4,
52
+ });
53
+ const keeper = recordObservation({
54
+ scope_id: chapterhouse.id,
55
+ content: "Worker event streams use server sent events for live task output.",
56
+ source: "test",
57
+ confidence: 0.9,
58
+ });
59
+ const third = recordObservation({
60
+ scope_id: chapterhouse.id,
61
+ content: "The worker event stream uses server sent events for live task output today.",
62
+ source: "test",
63
+ confidence: 0.9,
64
+ });
65
+ const otherScope = recordObservation({
66
+ scope_id: team.id,
67
+ content: "Worker event streams use server sent events for live task output.",
68
+ source: "test",
69
+ confidence: 0.1,
70
+ });
71
+ const summary = housekeepingModule.dedupObservationsPass(chapterhouse.id);
72
+ assert.equal(summary.pass, "dedupObservationsPass");
73
+ assert.equal(summary.examined, 3);
74
+ assert.equal(summary.modified, 2);
75
+ assert.deepEqual(summary.errors, []);
76
+ assert.deepEqual(db.prepare(`SELECT id, superseded_by FROM mem_observations WHERE id IN (?, ?, ?) ORDER BY id`).all(first.id, keeper.id, third.id), [
77
+ { id: first.id, superseded_by: keeper.id },
78
+ { id: keeper.id, superseded_by: null },
79
+ { id: third.id, superseded_by: keeper.id },
80
+ ]);
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);
83
+ assert.equal(second.modified, 0);
84
+ });
85
+ test("dedupDecisionsPass supersedes similar active decisions within scope and keeps the latest decision", async () => {
86
+ const { dbModule, memoryModule, housekeepingModule } = await loadModules();
87
+ const db = dbModule.getDb();
88
+ const getScope = getFunction(memoryModule, "getScope");
89
+ const upsertEntity = getFunction(memoryModule, "upsertEntity");
90
+ const recordDecision = getFunction(memoryModule, "recordDecision");
91
+ const chapterhouse = getScope("chapterhouse");
92
+ 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" });
96
+ const oldDecision = recordDecision({
97
+ scope_id: chapterhouse.id,
98
+ entity_id: api.id,
99
+ title: "Use SQLite FTS5 for memory recall",
100
+ rationale: "Initial choice.",
101
+ decided_at: "2026-05-11",
102
+ });
103
+ const keeper = recordDecision({
104
+ scope_id: chapterhouse.id,
105
+ entity_id: api.id,
106
+ title: "Use SQLite FTS5 for scoped memory recall",
107
+ rationale: "Latest choice.",
108
+ decided_at: "2026-05-13",
109
+ });
110
+ const otherEntity = recordDecision({
111
+ scope_id: chapterhouse.id,
112
+ entity_id: web.id,
113
+ title: "Use SQLite FTS5 for memory recall",
114
+ rationale: "Same title, different entity context, but newer scope-level decision.",
115
+ decided_at: "2026-05-14",
116
+ });
117
+ const otherScope = recordDecision({
118
+ scope_id: team.id,
119
+ title: "Use SQLite FTS5 for memory recall",
120
+ rationale: "Same title, different scope.",
121
+ decided_at: "2026-05-14",
122
+ });
123
+ const summary = housekeepingModule.dedupDecisionsPass(chapterhouse.id);
124
+ assert.equal(summary.pass, "dedupDecisionsPass");
125
+ assert.equal(summary.examined, 3);
126
+ assert.equal(summary.modified, 2);
127
+ assert.deepEqual(summary.errors, []);
128
+ assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(oldDecision.id).superseded_by, otherEntity.id);
129
+ assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(keeper.id).superseded_by, otherEntity.id);
130
+ assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherEntity.id).superseded_by, null);
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);
133
+ assert.equal(second.modified, 0);
134
+ });
135
+ test("orphanCleanupPass clears missing observation entity references without touching valid or out-of-scope rows", async () => {
136
+ const { dbModule, memoryModule, housekeepingModule } = await loadModules();
137
+ const db = dbModule.getDb();
138
+ const getScope = getFunction(memoryModule, "getScope");
139
+ const upsertEntity = getFunction(memoryModule, "upsertEntity");
140
+ const recordObservation = getFunction(memoryModule, "recordObservation");
141
+ const chapterhouse = getScope("chapterhouse");
142
+ const team = getScope("team");
143
+ assert.ok(chapterhouse && team);
144
+ const entity = upsertEntity({ scope_id: chapterhouse.id, kind: "tool", name: "sqlite" });
145
+ const valid = recordObservation({ scope_id: chapterhouse.id, entity_id: entity.id, content: "Valid entity reference", source: "test" });
146
+ const orphan = recordObservation({ scope_id: chapterhouse.id, content: "Will become orphaned", source: "test" });
147
+ const otherScope = recordObservation({ scope_id: team.id, content: "Other scope orphan", source: "test" });
148
+ db.pragma("foreign_keys = OFF");
149
+ db.prepare(`UPDATE mem_observations SET entity_id = 987654 WHERE id IN (?, ?)`).run(orphan.id, otherScope.id);
150
+ db.pragma("foreign_keys = ON");
151
+ const summary = housekeepingModule.orphanCleanupPass(chapterhouse.id);
152
+ assert.equal(summary.pass, "orphanCleanupPass");
153
+ assert.equal(summary.examined, 1);
154
+ assert.equal(summary.modified, 1);
155
+ assert.equal(db.prepare(`SELECT entity_id FROM mem_observations WHERE id = ?`).get(orphan.id).entity_id, null);
156
+ assert.equal(db.prepare(`SELECT entity_id FROM mem_observations WHERE id = ?`).get(valid.id).entity_id, entity.id);
157
+ assert.equal(db.prepare(`SELECT entity_id FROM mem_observations WHERE id = ?`).get(otherScope.id).entity_id, 987654);
158
+ const second = housekeepingModule.orphanCleanupPass(chapterhouse.id);
159
+ assert.equal(second.modified, 0);
160
+ });
161
+ test("decayPass archives old low-confidence observations only in scope and compactInboxPass removes resolved inbox rows after retention", async () => {
162
+ const { dbModule, memoryModule, housekeepingModule } = await loadModules();
163
+ const db = dbModule.getDb();
164
+ const getScope = getFunction(memoryModule, "getScope");
165
+ const recordObservation = getFunction(memoryModule, "recordObservation");
166
+ const chapterhouse = getScope("chapterhouse");
167
+ const team = getScope("team");
168
+ assert.ok(chapterhouse && team);
169
+ const archiveMe = recordObservation({ scope_id: chapterhouse.id, content: "Old low confidence", source: "test", confidence: 0.2 });
170
+ const highConfidence = recordObservation({ scope_id: chapterhouse.id, content: "Old high confidence", source: "test", confidence: 0.9 });
171
+ const fresh = recordObservation({ scope_id: chapterhouse.id, content: "Fresh low confidence", source: "test", confidence: 0.2 });
172
+ const otherScope = recordObservation({ scope_id: team.id, content: "Other scope old low confidence", source: "test", confidence: 0.2 });
173
+ db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-31 days') WHERE id IN (?, ?, ?)`).run(archiveMe.id, highConfidence.id, otherScope.id);
174
+ const decay = housekeepingModule.decayPass(chapterhouse.id);
175
+ assert.equal(decay.pass, "decayPass");
176
+ assert.equal(decay.examined, 1);
177
+ assert.equal(decay.modified, 1);
178
+ assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(archiveMe.id).archived_at);
179
+ assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(highConfidence.id).archived_at, null);
180
+ assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(fresh.id).archived_at, null);
181
+ assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(otherScope.id).archived_at, null);
182
+ assert.equal(housekeepingModule.decayPass(chapterhouse.id).modified, 0);
183
+ db.prepare(`
184
+ INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, status, created_at, resolved_at)
185
+ VALUES
186
+ (?, 'memory_proposal', '{}', 'test', 'accepted', datetime('now', '-20 days'), datetime('now', '-8 days')),
187
+ (?, 'memory_proposal', '{}', 'test', 'rejected', datetime('now', '-20 days'), datetime('now', '-6 days')),
188
+ (?, 'memory_proposal', '{}', 'test', 'pending', datetime('now', '-20 days'), NULL)
189
+ `).run(chapterhouse.id, chapterhouse.id, chapterhouse.id);
190
+ const compact = housekeepingModule.compactInboxPass();
191
+ assert.equal(compact.pass, "compactInboxPass");
192
+ assert.equal(compact.examined, 1);
193
+ assert.equal(compact.modified, 1);
194
+ assert.deepEqual(db.prepare(`SELECT status FROM mem_inbox ORDER BY id`).all(), [{ status: "rejected" }, { status: "pending" }]);
195
+ assert.equal(housekeepingModule.compactInboxPass().modified, 0);
196
+ });
197
+ test("runHousekeeping defaults to the active scope and can target all active scopes", async () => {
198
+ const { dbModule, memoryModule, housekeepingModule } = await loadModules();
199
+ const db = dbModule.getDb();
200
+ const getScope = getFunction(memoryModule, "getScope");
201
+ const setActiveScope = getFunction(memoryModule, "setActiveScope");
202
+ const recordObservation = getFunction(memoryModule, "recordObservation");
203
+ const chapterhouse = getScope("chapterhouse");
204
+ const team = getScope("team");
205
+ assert.ok(chapterhouse && team);
206
+ const chapterhouseOld = recordObservation({ scope_id: chapterhouse.id, content: "Chapterhouse old low", source: "test", confidence: 0.1 });
207
+ const teamOld = recordObservation({ scope_id: team.id, content: "Team old low", source: "test", confidence: 0.1 });
208
+ db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-31 days') WHERE id IN (?, ?)`).run(chapterhouseOld.id, teamOld.id);
209
+ setActiveScope("chapterhouse");
210
+ const activeOnly = housekeepingModule.runHousekeeping({ passes: ["decay"] });
211
+ assert.deepEqual(activeOnly.scopeIds, [chapterhouse.id]);
212
+ assert.equal(activeOnly.summaries.length, 1);
213
+ assert.equal(activeOnly.summaries[0]?.modified, 1);
214
+ assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(chapterhouseOld.id).archived_at);
215
+ assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(teamOld.id).archived_at, null);
216
+ const allScopes = housekeepingModule.runHousekeeping({ allScopes: true, passes: ["decay"] });
217
+ assert.ok(allScopes.scopeIds.includes(team.id));
218
+ assert.equal(allScopes.summaries.some((summary) => summary.modified === 1), true);
219
+ assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(teamOld.id).archived_at);
220
+ });
221
+ test("tieringPass promotes and demotes rows from lifecycle signals and is idempotent", async () => {
222
+ const { dbModule, memoryModule, housekeepingModule } = await loadModules();
223
+ const db = dbModule.getDb();
224
+ const getScope = getFunction(memoryModule, "getScope");
225
+ const upsertEntity = getFunction(memoryModule, "upsertEntity");
226
+ const recordObservation = getFunction(memoryModule, "recordObservation");
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" });
231
+ const referencedObservation = recordObservation({
232
+ scope_id: chapterhouse.id,
233
+ entity_id: entity.id,
234
+ content: "Referenced by a recent decision through its entity.",
235
+ source: "test",
236
+ tier: "warm",
237
+ });
238
+ const recentDecision = recordDecision({
239
+ scope_id: chapterhouse.id,
240
+ entity_id: entity.id,
241
+ title: "Restate memory tiering decision",
242
+ rationale: "Recent entity-linked decisions keep related observations hot.",
243
+ decided_at: new Date().toISOString(),
244
+ tier: "warm",
245
+ });
246
+ const oldHot = recordObservation({
247
+ scope_id: chapterhouse.id,
248
+ content: "Old hot row with no recall activity should cool down.",
249
+ source: "test",
250
+ tier: "hot",
251
+ });
252
+ const staleLowConfidence = recordObservation({
253
+ scope_id: chapterhouse.id,
254
+ content: "Low confidence stale row should go cold.",
255
+ source: "test",
256
+ tier: "warm",
257
+ confidence: 0.2,
258
+ });
259
+ const archived = recordObservation({
260
+ scope_id: chapterhouse.id,
261
+ content: "Archived row should always be cold.",
262
+ source: "test",
263
+ tier: "hot",
264
+ });
265
+ db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-45 days') WHERE id = ?`).run(oldHot.id);
266
+ db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-61 days') WHERE id = ?`).run(staleLowConfidence.id);
267
+ db.prepare(`UPDATE mem_observations SET archived_at = CURRENT_TIMESTAMP WHERE id = ?`).run(archived.id);
268
+ const summary = housekeepingModule.tieringPass(chapterhouse.id);
269
+ assert.equal(summary.pass, "tieringPass");
270
+ assert.equal(summary.modified, 5);
271
+ assert.deepEqual(summary.errors, []);
272
+ assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(referencedObservation.id).tier, "hot");
273
+ assert.equal(db.prepare(`SELECT tier FROM mem_decisions WHERE id = ?`).get(recentDecision.id).tier, "hot");
274
+ assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(oldHot.id).tier, "warm");
275
+ assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(staleLowConfidence.id).tier, "cold");
276
+ assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(archived.id).tier, "cold");
277
+ const second = housekeepingModule.tieringPass(chapterhouse.id);
278
+ assert.equal(second.modified, 0);
279
+ });
280
+ //# sourceMappingURL=housekeeping.test.js.map
@@ -0,0 +1,73 @@
1
+ import { getDb } from "../store/db.js";
2
+ import { getActiveScope } from "./active-scope.js";
3
+ import { getScope } from "./scopes.js";
4
+ function toInboxItem(row) {
5
+ return {
6
+ id: row.id,
7
+ scopeId: row.scope_id ?? undefined,
8
+ kind: row.kind,
9
+ payload: row.payload,
10
+ sourceAgent: row.source_agent,
11
+ sourceTaskId: row.source_task_id ?? undefined,
12
+ status: row.status,
13
+ createdAt: row.created_at,
14
+ resolvedAt: row.resolved_at ?? undefined,
15
+ resolutionReason: row.resolution_reason ?? undefined,
16
+ };
17
+ }
18
+ export function resolveProposalScope(scopeSlug) {
19
+ if (scopeSlug) {
20
+ const explicit = getScope(scopeSlug);
21
+ if (!explicit) {
22
+ throw new Error(`Unknown memory scope '${scopeSlug}'.`);
23
+ }
24
+ return { scopeId: explicit.id, scopeSlug: explicit.slug };
25
+ }
26
+ const activeScope = getActiveScope();
27
+ if (!activeScope) {
28
+ throw new Error("No active memory scope is set. Use memory_set_scope or pass scope_slug explicitly.");
29
+ }
30
+ return { scopeId: activeScope.id, scopeSlug: activeScope.slug };
31
+ }
32
+ export function queueMemoryProposal(input) {
33
+ const { scopeId, scopeSlug } = resolveProposalScope(input.scopeSlug);
34
+ const envelope = {
35
+ kind: input.kind,
36
+ scope_slug: scopeSlug,
37
+ confidence: input.confidence ?? 0.5,
38
+ reason: input.reason,
39
+ payload: input.payload,
40
+ };
41
+ const result = getDb().prepare(`
42
+ INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status, created_at)
43
+ VALUES (?, 'memory_proposal', ?, ?, ?, 'pending', CURRENT_TIMESTAMP)
44
+ `).run(scopeId, JSON.stringify(envelope), input.sourceAgent, input.sourceTaskId ?? null);
45
+ return getInboxItem(Number(result.lastInsertRowid));
46
+ }
47
+ export function getInboxItem(id) {
48
+ const row = getDb().prepare(`
49
+ SELECT id, scope_id, kind, payload, source_agent, source_task_id, status, created_at, resolved_at, resolution_reason
50
+ FROM mem_inbox
51
+ WHERE id = ?
52
+ `).get(id);
53
+ return row ? toInboxItem(row) : undefined;
54
+ }
55
+ export function listPendingMemoryProposalsForTask(taskId) {
56
+ const rows = getDb().prepare(`
57
+ SELECT id, scope_id, kind, payload, source_agent, source_task_id, status, created_at, resolved_at, resolution_reason
58
+ FROM mem_inbox
59
+ WHERE source_task_id = ?
60
+ AND status = 'pending'
61
+ AND kind = 'memory_proposal'
62
+ ORDER BY id ASC
63
+ `).all(taskId);
64
+ return rows.map(toInboxItem);
65
+ }
66
+ export function resolveInboxItem(id, status, reason) {
67
+ getDb().prepare(`
68
+ UPDATE mem_inbox
69
+ SET status = ?, resolution_reason = ?, resolved_at = CURRENT_TIMESTAMP
70
+ WHERE id = ?
71
+ `).run(status, reason, id);
72
+ }
73
+ //# sourceMappingURL=inbox.js.map
@@ -0,0 +1,11 @@
1
+ export { getActiveScope, inferScopeFromText, setActiveScope } from "./active-scope.js";
2
+ export { recordDecision, getDecision, listDecisions, supersedeDecision } from "./decisions.js";
3
+ export { getEntity, findEntityByName, listEntities, upsertEntity } from "./entities.js";
4
+ export { getHotTierEntries, renderHotTierForActiveScope, renderHotTierXML } from "./hot-tier.js";
5
+ export { compactInboxPass, decayPass, dedupDecisionsPass, dedupObservationsPass, isHousekeepingInFlight, orphanCleanupPass, runHousekeeping, } from "./housekeeping.js";
6
+ export { getInboxItem, listPendingMemoryProposalsForTask, queueMemoryProposal, resolveInboxItem, resolveProposalScope } from "./inbox.js";
7
+ export { recordObservation, getObservation, listObservations, deleteObservation } from "./observations.js";
8
+ export { recall } from "./recall.js";
9
+ export { createScope, deactivateScope, getScope, listScopes, updateScope } from "./scopes.js";
10
+ export { demoteToCold, demoteToWarm, inferTierFromSignals, promoteToHot, tieringPass } from "./tiering.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,46 @@
1
+ import { getDb } from "../store/db.js";
2
+ function toObservation(row) {
3
+ return {
4
+ id: row.id,
5
+ scopeId: row.scope_id,
6
+ entityId: row.entity_id ?? undefined,
7
+ content: row.content,
8
+ source: row.source,
9
+ tier: row.tier,
10
+ confidence: row.confidence,
11
+ embedding: row.embedding ?? undefined,
12
+ supersededBy: row.superseded_by ?? undefined,
13
+ archivedAt: row.archived_at ?? undefined,
14
+ createdAt: row.created_at,
15
+ };
16
+ }
17
+ export function recordObservation(input) {
18
+ const result = getDb().prepare(`
19
+ INSERT INTO mem_observations (scope_id, entity_id, content, source, tier, confidence, created_at)
20
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
21
+ `).run(input.scope_id, input.entity_id ?? null, input.content, input.source, input.tier ?? "warm", input.confidence ?? 1.0);
22
+ return getObservation(Number(result.lastInsertRowid));
23
+ }
24
+ export function getObservation(id) {
25
+ const row = getDb().prepare(`
26
+ SELECT id, scope_id, entity_id, content, source, tier, confidence, embedding, superseded_by, archived_at, created_at
27
+ FROM mem_observations
28
+ WHERE id = ?
29
+ `).get(id);
30
+ return row ? toObservation(row) : undefined;
31
+ }
32
+ export function listObservations(input = {}) {
33
+ const rows = getDb().prepare(`
34
+ SELECT id, scope_id, entity_id, content, source, tier, confidence, embedding, superseded_by, archived_at, created_at
35
+ FROM mem_observations
36
+ WHERE (? IS NULL OR scope_id = ?)
37
+ AND (? IS NULL OR entity_id = ?)
38
+ ORDER BY id DESC
39
+ LIMIT ? OFFSET ?
40
+ `).all(input.scope_id ?? null, input.scope_id ?? null, input.entity_id ?? null, input.entity_id ?? null, input.limit ?? 50, input.offset ?? 0);
41
+ return rows.map(toObservation);
42
+ }
43
+ export function deleteObservation(id) {
44
+ getDb().prepare(`DELETE FROM mem_observations WHERE id = ?`).run(id);
45
+ }
46
+ //# sourceMappingURL=observations.js.map
@@ -0,0 +1,86 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ const repoRoot = process.cwd();
6
+ const sandboxRoot = join(repoRoot, ".test-work", `memory-observations-${process.pid}`);
7
+ const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
8
+ process.env.CHAPTERHOUSE_HOME = sandboxRoot;
9
+ function resetSandbox() {
10
+ mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
11
+ rmSync(sandboxRoot, { recursive: true, force: true });
12
+ mkdirSync(chapterhouseHome, { recursive: true });
13
+ }
14
+ async function loadModules() {
15
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
16
+ const memoryModule = await import(new URL("./index.js", import.meta.url).href);
17
+ return { dbModule, memoryModule };
18
+ }
19
+ function getFunction(module, name) {
20
+ const value = module[name];
21
+ assert.equal(typeof value, "function", `expected ${name} to be exported`);
22
+ return value;
23
+ }
24
+ test.beforeEach(async () => {
25
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
26
+ dbModule.closeDb();
27
+ resetSandbox();
28
+ });
29
+ test.after(async () => {
30
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
31
+ dbModule.closeDb();
32
+ rmSync(sandboxRoot, { recursive: true, force: true });
33
+ });
34
+ test("recordObservation supports CRUD and keeps the observation FTS index in sync", async () => {
35
+ const { dbModule, memoryModule } = await loadModules();
36
+ const db = dbModule.getDb();
37
+ const getScope = getFunction(memoryModule, "getScope");
38
+ const recordObservation = getFunction(memoryModule, "recordObservation");
39
+ const getObservation = getFunction(memoryModule, "getObservation");
40
+ const listObservations = getFunction(memoryModule, "listObservations");
41
+ const deleteObservation = getFunction(memoryModule, "deleteObservation");
42
+ const chapterhouse = getScope("chapterhouse");
43
+ assert.ok(chapterhouse, "chapterhouse scope should be seeded");
44
+ const created = recordObservation({
45
+ scope_id: chapterhouse.id,
46
+ content: "SQLite WAL mode keeps Chapterhouse memory writes fast.",
47
+ source: "user",
48
+ tier: "hot",
49
+ confidence: 0.9,
50
+ });
51
+ assert.equal(created.content, "SQLite WAL mode keeps Chapterhouse memory writes fast.");
52
+ assert.deepEqual(getObservation(created.id)?.content, created.content);
53
+ assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.id === created.id), true);
54
+ const initialHits = db.prepare(`
55
+ SELECT rowid
56
+ FROM mem_observations_fts
57
+ WHERE mem_observations_fts MATCH 'WAL'
58
+ `).all();
59
+ assert.deepEqual(initialHits, [{ rowid: created.id }]);
60
+ db.prepare(`
61
+ UPDATE mem_observations
62
+ SET content = ?
63
+ WHERE id = ?
64
+ `).run("FTS rebuild test for hot-tier prompts and scoped recall.", created.id);
65
+ const walHits = db.prepare(`
66
+ SELECT rowid
67
+ FROM mem_observations_fts
68
+ WHERE mem_observations_fts MATCH 'WAL'
69
+ `).all();
70
+ const rebuildHits = db.prepare(`
71
+ SELECT rowid
72
+ FROM mem_observations_fts
73
+ WHERE mem_observations_fts MATCH 'rebuild'
74
+ `).all();
75
+ assert.deepEqual(walHits, []);
76
+ assert.deepEqual(rebuildHits, [{ rowid: created.id }]);
77
+ deleteObservation(created.id);
78
+ assert.equal(getObservation(created.id), undefined);
79
+ const postDeleteHits = db.prepare(`
80
+ SELECT rowid
81
+ FROM mem_observations_fts
82
+ WHERE mem_observations_fts MATCH 'rebuild'
83
+ `).all();
84
+ assert.deepEqual(postDeleteHits, []);
85
+ });
86
+ //# sourceMappingURL=observations.test.js.map