chapterhouse 0.13.0 → 0.14.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 (118) hide show
  1. package/dist/api/route-coverage.test.js +1 -3
  2. package/dist/api/server.js +0 -2
  3. package/dist/api/server.test.js +0 -281
  4. package/dist/config.js +3 -85
  5. package/dist/config.test.js +5 -123
  6. package/dist/copilot/agents.js +25 -13
  7. package/dist/copilot/agents.test.js +10 -11
  8. package/dist/copilot/memory-coordinator.js +12 -227
  9. package/dist/copilot/memory-coordinator.test.js +31 -250
  10. package/dist/copilot/orchestrator.js +8 -66
  11. package/dist/copilot/orchestrator.test.js +9 -467
  12. package/dist/copilot/skills.js +15 -1
  13. package/dist/copilot/system-message.js +9 -15
  14. package/dist/copilot/system-message.test.js +9 -22
  15. package/dist/copilot/tools/index.js +3 -3
  16. package/dist/copilot/tools-deps.js +1 -1
  17. package/dist/copilot/tools.agent.test.js +6 -0
  18. package/dist/copilot/tools.inventory.test.js +1 -14
  19. package/dist/daemon.js +7 -9
  20. package/dist/memory/assets.js +33 -0
  21. package/dist/memory/domains.js +58 -0
  22. package/dist/memory/domains.test.js +47 -0
  23. package/dist/memory/git.js +66 -0
  24. package/dist/memory/git.test.js +32 -0
  25. package/dist/memory/history.js +19 -0
  26. package/dist/memory/hottier.js +32 -0
  27. package/dist/memory/hottier.test.js +33 -0
  28. package/dist/memory/index.js +5 -13
  29. package/dist/memory/instructions.js +17 -0
  30. package/dist/memory/manager.js +84 -0
  31. package/dist/memory/markdown.js +78 -0
  32. package/dist/memory/markdown.test.js +42 -0
  33. package/dist/memory/mutex.js +18 -0
  34. package/dist/memory/path-guard.js +26 -0
  35. package/dist/memory/path-guard.test.js +27 -0
  36. package/dist/memory/paths.js +12 -0
  37. package/dist/memory/reconcile.js +75 -0
  38. package/dist/memory/reconcile.test.js +50 -0
  39. package/dist/memory/scaffold.js +37 -0
  40. package/dist/memory/scaffold.test.js +52 -0
  41. package/dist/memory/tools/commit-wrapper.js +32 -0
  42. package/dist/memory/tools/domains.js +73 -0
  43. package/dist/memory/tools/domains.test.js +66 -0
  44. package/dist/memory/tools/git.js +52 -0
  45. package/dist/memory/tools/index.js +25 -0
  46. package/dist/memory/tools/read.js +101 -0
  47. package/dist/memory/tools/read.test.js +69 -0
  48. package/dist/memory/tools/search.js +103 -0
  49. package/dist/memory/tools/search.test.js +63 -0
  50. package/dist/memory/tools/sessions.js +45 -0
  51. package/dist/memory/tools/sessions.test.js +74 -0
  52. package/dist/memory/tools/shared.js +7 -0
  53. package/dist/memory/tools/write.js +116 -0
  54. package/dist/memory/tools/write.test.js +107 -0
  55. package/dist/memory/walk.js +39 -0
  56. package/dist/store/repositories/sessions.js +40 -0
  57. package/dist/wiki/consolidation.js +3 -31
  58. package/dist/wiki/consolidation.test.js +0 -19
  59. package/dist/wiki/frontmatter.js +18 -6
  60. package/dist/wiki/frontmatter.test.js +40 -0
  61. package/package.json +1 -1
  62. package/skills/system/evolve/SKILL.md +131 -0
  63. package/skills/system/foresight/SKILL.md +116 -0
  64. package/skills/system/history/SKILL.md +58 -0
  65. package/skills/system/housekeeping/SKILL.md +185 -0
  66. package/skills/system/reflect/SKILL.md +214 -0
  67. package/skills/system/scenario/SKILL.md +198 -0
  68. package/skills/system/setup/SKILL.md +113 -0
  69. package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
  70. package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
  71. package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
  72. package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
  73. package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
  74. package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
  75. package/web/dist/index.html +1 -1
  76. package/dist/api/routes/memory.js +0 -475
  77. package/dist/api/routes/memory.test.js +0 -108
  78. package/dist/copilot/tools/memory.js +0 -678
  79. package/dist/copilot/tools.memory.test.js +0 -590
  80. package/dist/memory/action-items.js +0 -100
  81. package/dist/memory/action-items.test.js +0 -83
  82. package/dist/memory/active-scope.js +0 -78
  83. package/dist/memory/active-scope.test.js +0 -80
  84. package/dist/memory/checkpoint-prompt.js +0 -71
  85. package/dist/memory/checkpoint.js +0 -274
  86. package/dist/memory/checkpoint.test.js +0 -275
  87. package/dist/memory/decisions.js +0 -54
  88. package/dist/memory/decisions.test.js +0 -92
  89. package/dist/memory/entities.js +0 -70
  90. package/dist/memory/entities.test.js +0 -65
  91. package/dist/memory/eot.js +0 -459
  92. package/dist/memory/eot.test.js +0 -949
  93. package/dist/memory/hooks.js +0 -149
  94. package/dist/memory/hooks.test.js +0 -325
  95. package/dist/memory/hot-tier.js +0 -283
  96. package/dist/memory/hot-tier.test.js +0 -275
  97. package/dist/memory/housekeeping-scheduler.js +0 -187
  98. package/dist/memory/housekeeping-scheduler.test.js +0 -236
  99. package/dist/memory/housekeeping.js +0 -497
  100. package/dist/memory/housekeeping.test.js +0 -410
  101. package/dist/memory/inbox.js +0 -83
  102. package/dist/memory/inbox.test.js +0 -178
  103. package/dist/memory/migration.js +0 -244
  104. package/dist/memory/migration.test.js +0 -108
  105. package/dist/memory/observations.js +0 -46
  106. package/dist/memory/observations.test.js +0 -86
  107. package/dist/memory/recall.js +0 -269
  108. package/dist/memory/recall.test.js +0 -265
  109. package/dist/memory/reflect.js +0 -273
  110. package/dist/memory/reflect.test.js +0 -256
  111. package/dist/memory/scope-lock.js +0 -26
  112. package/dist/memory/scope-lock.test.js +0 -118
  113. package/dist/memory/scopes.js +0 -89
  114. package/dist/memory/scopes.test.js +0 -176
  115. package/dist/memory/tiering.js +0 -223
  116. package/dist/memory/tiering.test.js +0 -323
  117. package/dist/memory/types.js +0 -2
  118. package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
@@ -1,269 +0,0 @@
1
- import { config } from "../config.js";
2
- import { getDb, isFts5Available } from "../store/db.js";
3
- import { getActiveScope } from "./active-scope.js";
4
- function quoteFts5QueryTerms(query) {
5
- return query
6
- .trim()
7
- .split(/\s+/)
8
- .filter((term) => term.length > 0)
9
- .map((term) => {
10
- const unquoted = term.replace(/^["']|["']$/g, "");
11
- return `"${unquoted.replace(/"/g, "\"\"")}"`;
12
- })
13
- .join(" ");
14
- }
15
- function recallHotTier(scopeId, options = {}) {
16
- const rows = getDb().prepare(`
17
- SELECT 'observation' AS kind, id, content
18
- FROM mem_observations
19
- WHERE scope_id = ? AND tier = 'hot'
20
- AND (? = 1 OR superseded_by IS NULL)
21
- AND (? = 1 OR archived_at IS NULL)
22
- UNION ALL
23
- SELECT 'decision' AS kind, id, title || ' — ' || rationale AS content
24
- FROM mem_decisions
25
- WHERE scope_id = ? AND tier = 'hot'
26
- AND (? = 1 OR superseded_by IS NULL)
27
- AND (? = 1 OR archived_at IS NULL)
28
- UNION ALL
29
- SELECT 'entity' AS kind, id, name || COALESCE(' — ' || summary, '') AS content
30
- FROM mem_entities
31
- WHERE scope_id = ? AND tier = 'hot'
32
- ORDER BY id DESC
33
- LIMIT 10
34
- `).all(scopeId, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0, scopeId, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0, scopeId);
35
- return rows;
36
- }
37
- function recallObservationHits(query, scopeId, options = {}) {
38
- if (isFts5Available()) {
39
- const ftsQuery = quoteFts5QueryTerms(query);
40
- const rows = getDb().prepare(`
41
- SELECT
42
- o.id,
43
- o.scope_id,
44
- s.slug AS scope,
45
- o.content,
46
- o.tier,
47
- -bm25(mem_observations_fts) * CASE WHEN o.tier = 'hot' THEN ? ELSE 1 END AS score,
48
- snippet(mem_observations_fts, 0, '[', ']', '…', 12) AS snippet
49
- FROM mem_observations o
50
- JOIN mem_scopes s ON s.id = o.scope_id
51
- JOIN mem_observations_fts ON mem_observations_fts.rowid = o.id
52
- WHERE mem_observations_fts MATCH ?
53
- AND (? IS NULL OR o.scope_id = ?)
54
- AND (? = 1 OR o.tier != 'cold')
55
- AND (? = 1 OR o.superseded_by IS NULL)
56
- AND (? = 1 OR o.archived_at IS NULL)
57
- ORDER BY score DESC, o.id DESC
58
- `).all(config.memoryHotRecallBoost, ftsQuery, scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0);
59
- return rows.map((row) => ({
60
- kind: "observation",
61
- id: row.id,
62
- scopeId: row.scope_id,
63
- scope: row.scope,
64
- content: row.content,
65
- score: row.score,
66
- snippet: row.snippet ?? row.content,
67
- }));
68
- }
69
- const pattern = `%${query}%`;
70
- const rows = getDb().prepare(`
71
- SELECT o.id, o.scope_id, s.slug AS scope, o.content, o.tier
72
- FROM mem_observations o
73
- JOIN mem_scopes s ON s.id = o.scope_id
74
- WHERE (? IS NULL OR o.scope_id = ?)
75
- AND (? = 1 OR o.tier != 'cold')
76
- AND (? = 1 OR o.superseded_by IS NULL)
77
- AND (? = 1 OR o.archived_at IS NULL)
78
- AND o.content LIKE ?
79
- ORDER BY o.id DESC
80
- `).all(scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0, pattern);
81
- return rows.map((row) => ({
82
- kind: "observation",
83
- id: row.id,
84
- scopeId: row.scope_id,
85
- scope: row.scope,
86
- content: row.content,
87
- score: row.tier === "hot" ? config.memoryHotRecallBoost : 1,
88
- snippet: row.content,
89
- }));
90
- }
91
- function recallDecisionHits(query, scopeId, options = {}) {
92
- if (isFts5Available()) {
93
- const ftsQuery = quoteFts5QueryTerms(query);
94
- const rows = getDb().prepare(`
95
- SELECT
96
- d.id,
97
- d.scope_id,
98
- s.slug AS scope,
99
- d.title,
100
- d.rationale,
101
- d.decided_at,
102
- d.tier,
103
- -bm25(mem_decisions_fts) * CASE WHEN d.tier = 'hot' THEN ? ELSE 1 END AS score,
104
- snippet(mem_decisions_fts, 0, '[', ']', '…', 8) || ' — ' ||
105
- snippet(mem_decisions_fts, 1, '[', ']', '…', 12) AS snippet
106
- FROM mem_decisions d
107
- JOIN mem_scopes s ON s.id = d.scope_id
108
- JOIN mem_decisions_fts ON mem_decisions_fts.rowid = d.id
109
- WHERE mem_decisions_fts MATCH ?
110
- AND (? IS NULL OR d.scope_id = ?)
111
- AND (? = 1 OR d.tier != 'cold')
112
- AND (? = 1 OR d.superseded_by IS NULL)
113
- AND (? = 1 OR d.archived_at IS NULL)
114
- ORDER BY score DESC, d.id DESC
115
- `).all(config.memoryHotRecallBoost, ftsQuery, scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0);
116
- return rows.map((row) => ({
117
- kind: "decision",
118
- id: row.id,
119
- scopeId: row.scope_id,
120
- scope: row.scope,
121
- content: `${row.title} — ${row.rationale}`,
122
- decidedAt: row.decided_at,
123
- score: row.score,
124
- snippet: row.snippet ?? `${row.title} — ${row.rationale}`,
125
- }));
126
- }
127
- const pattern = `%${query}%`;
128
- const rows = getDb().prepare(`
129
- SELECT d.id, d.scope_id, s.slug AS scope, d.title, d.rationale, d.decided_at, d.tier
130
- FROM mem_decisions d
131
- JOIN mem_scopes s ON s.id = d.scope_id
132
- WHERE (? IS NULL OR d.scope_id = ?)
133
- AND (? = 1 OR d.tier != 'cold')
134
- AND (? = 1 OR d.superseded_by IS NULL)
135
- AND (? = 1 OR d.archived_at IS NULL)
136
- AND (d.title LIKE ? OR d.rationale LIKE ?)
137
- ORDER BY d.decided_at DESC, d.id DESC
138
- `).all(scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0, pattern, pattern);
139
- return rows.map((row) => ({
140
- kind: "decision",
141
- id: row.id,
142
- scopeId: row.scope_id,
143
- scope: row.scope,
144
- content: `${row.title} — ${row.rationale}`,
145
- decidedAt: row.decided_at,
146
- score: row.tier === "hot" ? config.memoryHotRecallBoost : 1,
147
- snippet: `${row.title} — ${row.rationale}`,
148
- }));
149
- }
150
- function recallEntityHits(query, scopeId, options = {}) {
151
- const pattern = `%${query}%`;
152
- const rows = getDb().prepare(`
153
- SELECT e.id, e.scope_id, s.slug AS scope, e.name, e.summary, e.tier
154
- FROM mem_entities e
155
- JOIN mem_scopes s ON s.id = e.scope_id
156
- WHERE (? IS NULL OR e.scope_id = ?)
157
- AND (e.name LIKE ? OR COALESCE(e.summary, '') LIKE ?)
158
- AND (? = 1 OR e.tier != 'cold')
159
- ORDER BY e.updated_at DESC, e.id DESC
160
- `).all(scopeId ?? null, scopeId ?? null, pattern, pattern, options.includeCold ? 1 : 0);
161
- return rows.map((row) => ({
162
- kind: "entity",
163
- id: row.id,
164
- scopeId: row.scope_id,
165
- scope: row.scope,
166
- content: `${row.name}${row.summary ? ` — ${row.summary}` : ""}`,
167
- score: (row.name.toLowerCase().includes(query.toLowerCase()) ? 2 : 1)
168
- * (row.tier === "hot" ? config.memoryHotRecallBoost : 1),
169
- snippet: `${row.name}${row.summary ? ` — ${row.summary}` : ""}`,
170
- }));
171
- }
172
- function recallActionItemHits(query, scopeId, options = {}) {
173
- if (isFts5Available()) {
174
- const ftsQuery = quoteFts5QueryTerms(query);
175
- const rows = getDb().prepare(`
176
- SELECT
177
- a.id,
178
- a.scope_id,
179
- s.slug AS scope,
180
- a.title,
181
- a.detail,
182
- a.tier,
183
- -bm25(mem_action_items_fts) * CASE WHEN a.tier = 'hot' THEN ? ELSE 1 END AS score,
184
- snippet(mem_action_items_fts, 0, '[', ']', '…', 8) || COALESCE(' — ' ||
185
- snippet(mem_action_items_fts, 1, '[', ']', '…', 12), '') AS snippet
186
- FROM mem_action_items a
187
- JOIN mem_scopes s ON s.id = a.scope_id
188
- JOIN mem_action_items_fts ON mem_action_items_fts.rowid = a.id
189
- WHERE mem_action_items_fts MATCH ?
190
- AND (? IS NULL OR a.scope_id = ?)
191
- AND (? = 1 OR a.tier != 'cold')
192
- AND a.status IN ('open', 'snoozed')
193
- ORDER BY score DESC, a.id DESC
194
- `).all(config.memoryHotRecallBoost, ftsQuery, scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0);
195
- return rows.map((row) => ({
196
- kind: "action_item",
197
- id: row.id,
198
- scopeId: row.scope_id,
199
- scope: row.scope,
200
- content: `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
201
- score: row.score,
202
- snippet: row.snippet ?? `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
203
- }));
204
- }
205
- const pattern = `%${query}%`;
206
- const rows = getDb().prepare(`
207
- SELECT a.id, a.scope_id, s.slug AS scope, a.title, a.detail, a.tier
208
- FROM mem_action_items a
209
- JOIN mem_scopes s ON s.id = a.scope_id
210
- WHERE (? IS NULL OR a.scope_id = ?)
211
- AND (? = 1 OR a.tier != 'cold')
212
- AND a.status IN ('open', 'snoozed')
213
- AND (a.title LIKE ? OR COALESCE(a.detail, '') LIKE ?)
214
- ORDER BY
215
- CASE WHEN a.due_at IS NULL THEN 1 ELSE 0 END ASC,
216
- datetime(a.due_at) ASC,
217
- a.id DESC
218
- `).all(scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, pattern, pattern);
219
- return rows.map((row) => ({
220
- kind: "action_item",
221
- id: row.id,
222
- scopeId: row.scope_id,
223
- scope: row.scope,
224
- content: `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
225
- score: row.tier === "hot" ? config.memoryHotRecallBoost : 1,
226
- snippet: `${row.title}${row.detail ? ` — ${row.detail}` : ""}`,
227
- }));
228
- }
229
- export function recall(input) {
230
- const activeScope = getActiveScope();
231
- const effectiveScopeId = input.scope_id ?? activeScope?.id;
232
- const requestedKinds = new Set(input.kinds ?? ["observation", "decision", "entity"]);
233
- const hotTier = activeScope && (!effectiveScopeId || activeScope.id === effectiveScopeId)
234
- ? recallHotTier(activeScope.id, input)
235
- : [];
236
- const hits = [
237
- ...(requestedKinds.has("observation") ? recallObservationHits(input.query, effectiveScopeId, input) : []),
238
- ...(requestedKinds.has("decision") ? recallDecisionHits(input.query, effectiveScopeId, input) : []),
239
- ...(requestedKinds.has("entity") ? recallEntityHits(input.query, effectiveScopeId, input) : []),
240
- ...(requestedKinds.has("action_item") ? recallActionItemHits(input.query, effectiveScopeId, input) : []),
241
- ]
242
- .sort((a, b) => {
243
- if (b.score !== a.score)
244
- return b.score - a.score;
245
- return b.id - a.id;
246
- })
247
- .slice(0, input.limit ?? 10);
248
- if (hits.length > 0) {
249
- const db = getDb();
250
- const tables = {
251
- observation: "mem_observations",
252
- decision: "mem_decisions",
253
- entity: "mem_entities",
254
- action_item: "mem_action_items",
255
- };
256
- for (const kind of Object.keys(tables)) {
257
- const ids = hits.filter((hit) => hit.kind === kind).map((hit) => hit.id);
258
- if (ids.length > 0) {
259
- db.prepare(`UPDATE ${tables[kind]} SET last_recalled_at = CURRENT_TIMESTAMP WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
260
- }
261
- }
262
- }
263
- return {
264
- activeScope,
265
- hotTier,
266
- hits,
267
- };
268
- }
269
- //# sourceMappingURL=recall.js.map
@@ -1,265 +0,0 @@
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-recall-${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("recall returns active-scope hot-tier entries before ranked FTS hits and respects filters", async () => {
35
- const { dbModule, memoryModule } = await loadModules();
36
- dbModule.getDb();
37
- const getScope = getFunction(memoryModule, "getScope");
38
- const setActiveScope = getFunction(memoryModule, "setActiveScope");
39
- const recordObservation = getFunction(memoryModule, "recordObservation");
40
- const recordDecision = getFunction(memoryModule, "recordDecision");
41
- const upsertEntity = getFunction(memoryModule, "upsertEntity");
42
- const recall = getFunction(memoryModule, "recall");
43
- const chapterhouse = getScope("chapterhouse");
44
- const global = getScope("global");
45
- assert.ok(chapterhouse && global, "seeded scopes should be available");
46
- setActiveScope("chapterhouse");
47
- const hotObservation = recordObservation({
48
- scope_id: chapterhouse.id,
49
- content: "Hot memory: build with npm run build before releasing scoped memory changes.",
50
- source: "user",
51
- tier: "hot",
52
- });
53
- const bestDecision = recordDecision({
54
- scope_id: chapterhouse.id,
55
- title: "Use SQLite FTS5 for scoped recall",
56
- rationale: "SQLite FTS5 gives ranked scoped recall and snippet generation for agent memory.",
57
- decided_at: "2026-05-13",
58
- });
59
- recordObservation({
60
- scope_id: chapterhouse.id,
61
- content: "SQLite recall fallback exists, but FTS5 is preferred for ranked snippet output.",
62
- source: "user",
63
- tier: "warm",
64
- });
65
- recordObservation({
66
- scope_id: global.id,
67
- content: "Global memory should not leak into chapterhouse-scoped recall.",
68
- source: "user",
69
- tier: "hot",
70
- });
71
- upsertEntity({
72
- scope_id: chapterhouse.id,
73
- kind: "tool",
74
- name: "better-sqlite3",
75
- summary: "SQLite driver backing FTS5 recall and scope settings.",
76
- tier: "warm",
77
- confidence: 0.9,
78
- });
79
- const full = recall({ query: "SQLite FTS5 scoped recall", scope_id: chapterhouse.id, limit: 10 });
80
- assert.equal(full.activeScope?.slug, "chapterhouse");
81
- assert.equal(full.hotTier.some((entry) => entry.id === hotObservation.id), true);
82
- assert.equal(full.hits.some((entry) => entry.id === bestDecision.id && entry.kind === "decision"), true);
83
- assert.equal(full.hits.every((entry) => entry.snippet.toLowerCase().includes("sqlite")), true);
84
- assert.equal(full.hits[0]?.id, bestDecision.id);
85
- const decisionOnly = recall({
86
- query: "SQLite FTS5 scoped recall",
87
- scope_id: chapterhouse.id,
88
- kinds: ["decision"],
89
- limit: 10,
90
- });
91
- assert.equal(decisionOnly.hits.every((entry) => entry.kind === "decision"), true);
92
- const limited = recall({
93
- query: "SQLite FTS5 scoped recall",
94
- scope_id: chapterhouse.id,
95
- limit: 1,
96
- });
97
- assert.equal(limited.hits.length, 1);
98
- });
99
- test("recall can opt into action item hits without adding them to default kind searches", async () => {
100
- const { dbModule, memoryModule } = await loadModules();
101
- dbModule.getDb();
102
- const getScope = getFunction(memoryModule, "getScope");
103
- const recordActionItem = getFunction(memoryModule, "recordActionItem");
104
- const recall = getFunction(memoryModule, "recall");
105
- const chapterhouse = getScope("chapterhouse");
106
- assert.ok(chapterhouse);
107
- const actionItem = recordActionItem({
108
- scope_id: chapterhouse.id,
109
- title: "Bellonda disk alert",
110
- detail: "Next time disk usage exceeds 85 percent, notify infra.",
111
- source: "test",
112
- });
113
- const defaults = recall({ query: "Bellonda disk alert", scope_id: chapterhouse.id, limit: 10 });
114
- assert.equal(defaults.hits.some((hit) => hit.kind === "action_item" && hit.id === actionItem.id), false);
115
- const actionOnly = recall({
116
- query: "Bellonda disk alert",
117
- scope_id: chapterhouse.id,
118
- kinds: ["action_item"],
119
- limit: 10,
120
- });
121
- assert.equal(actionOnly.hits.length, 1);
122
- assert.equal(actionOnly.hits[0]?.kind, "action_item");
123
- assert.equal(actionOnly.hits[0]?.id, actionItem.id);
124
- assert.match(actionOnly.hits[0]?.content ?? "", /Bellonda disk alert/);
125
- });
126
- test("recall excludes superseded and archived rows by default with opt-in inclusion", async () => {
127
- const { dbModule, memoryModule } = await loadModules();
128
- const db = dbModule.getDb();
129
- const getScope = getFunction(memoryModule, "getScope");
130
- const recordObservation = getFunction(memoryModule, "recordObservation");
131
- const recordDecision = getFunction(memoryModule, "recordDecision");
132
- const recall = getFunction(memoryModule, "recall");
133
- const chapterhouse = getScope("chapterhouse");
134
- assert.ok(chapterhouse);
135
- const liveObservation = recordObservation({
136
- scope_id: chapterhouse.id,
137
- content: "retention sentinel live observation",
138
- source: "test",
139
- });
140
- const supersededObservation = recordObservation({
141
- scope_id: chapterhouse.id,
142
- content: "retention sentinel superseded observation",
143
- source: "test",
144
- });
145
- const archivedObservation = recordObservation({
146
- scope_id: chapterhouse.id,
147
- content: "retention sentinel archived observation",
148
- source: "test",
149
- });
150
- const liveDecision = recordDecision({
151
- scope_id: chapterhouse.id,
152
- title: "retention sentinel live decision",
153
- rationale: "visible decision",
154
- });
155
- const supersededDecision = recordDecision({
156
- scope_id: chapterhouse.id,
157
- title: "retention sentinel superseded decision",
158
- rationale: "hidden decision",
159
- decided_at: "2026-05-12",
160
- });
161
- const archivedDecision = recordDecision({
162
- scope_id: chapterhouse.id,
163
- title: "retention sentinel archived decision",
164
- rationale: "hidden decision",
165
- decided_at: "2026-05-11",
166
- });
167
- db.prepare(`UPDATE mem_observations SET superseded_by = ? WHERE id = ?`).run(liveObservation.id, supersededObservation.id);
168
- db.prepare(`UPDATE mem_observations SET archived_at = CURRENT_TIMESTAMP WHERE id = ?`).run(archivedObservation.id);
169
- db.prepare(`UPDATE mem_decisions SET superseded_by = ? WHERE id = ?`).run(liveDecision.id, supersededDecision.id);
170
- db.prepare(`UPDATE mem_decisions SET archived_at = CURRENT_TIMESTAMP WHERE id = ?`).run(archivedDecision.id);
171
- const defaults = recall({ query: "retention sentinel", scope_id: chapterhouse.id, limit: 20 });
172
- assert.equal(defaults.hits.some((hit) => hit.id === liveObservation.id && hit.kind === "observation"), true);
173
- assert.equal(defaults.hits.some((hit) => hit.id === liveDecision.id && hit.kind === "decision"), true);
174
- assert.equal(defaults.hits.some((hit) => hit.id === supersededObservation.id && hit.kind === "observation"), false);
175
- assert.equal(defaults.hits.some((hit) => hit.id === archivedObservation.id && hit.kind === "observation"), false);
176
- assert.equal(defaults.hits.some((hit) => hit.id === supersededDecision.id && hit.kind === "decision"), false);
177
- assert.equal(defaults.hits.some((hit) => hit.id === archivedDecision.id && hit.kind === "decision"), false);
178
- const included = recall({
179
- query: "retention sentinel",
180
- scope_id: chapterhouse.id,
181
- limit: 20,
182
- includeSuperseded: true,
183
- includeArchived: true,
184
- });
185
- assert.equal(included.hits.some((hit) => hit.id === supersededObservation.id && hit.kind === "observation"), true);
186
- assert.equal(included.hits.some((hit) => hit.id === archivedObservation.id && hit.kind === "observation"), true);
187
- assert.equal(included.hits.some((hit) => hit.id === supersededDecision.id && hit.kind === "decision"), true);
188
- assert.equal(included.hits.some((hit) => hit.id === archivedDecision.id && hit.kind === "decision"), true);
189
- });
190
- test("recall boosts hot rows and excludes cold rows unless includeCold is set", async () => {
191
- const { dbModule, memoryModule } = await loadModules();
192
- dbModule.getDb();
193
- const getScope = getFunction(memoryModule, "getScope");
194
- const recordObservation = getFunction(memoryModule, "recordObservation");
195
- const recall = getFunction(memoryModule, "recall");
196
- const chapterhouse = getScope("chapterhouse");
197
- assert.ok(chapterhouse);
198
- const warm = recordObservation({
199
- scope_id: chapterhouse.id,
200
- content: "tierboost sentinel equal lexical match",
201
- source: "test",
202
- tier: "warm",
203
- });
204
- const hot = recordObservation({
205
- scope_id: chapterhouse.id,
206
- content: "tierboost sentinel equal lexical match",
207
- source: "test",
208
- tier: "hot",
209
- });
210
- const cold = recordObservation({
211
- scope_id: chapterhouse.id,
212
- content: "tierboost sentinel cold lexical match",
213
- source: "test",
214
- tier: "cold",
215
- });
216
- const defaults = recall({ query: "tierboost sentinel", scope_id: chapterhouse.id, kinds: ["observation"], limit: 10 });
217
- assert.equal(defaults.hits[0]?.id, hot.id);
218
- assert.equal(defaults.hits.some((hit) => hit.id === warm.id), true);
219
- assert.equal(defaults.hits.some((hit) => hit.id === cold.id), false);
220
- const withCold = recall({ query: "tierboost sentinel", scope_id: chapterhouse.id, kinds: ["observation"], limit: 10, includeCold: true });
221
- assert.equal(withCold.hits.some((hit) => hit.id === cold.id), true);
222
- });
223
- test("recall matches multi-word observation queries with exact tokens", async () => {
224
- const { dbModule, memoryModule } = await loadModules();
225
- dbModule.getDb();
226
- const getScope = getFunction(memoryModule, "getScope");
227
- const recordObservation = getFunction(memoryModule, "recordObservation");
228
- const recall = getFunction(memoryModule, "recall");
229
- const chapterhouse = getScope("chapterhouse");
230
- assert.ok(chapterhouse);
231
- const observation = recordObservation({
232
- scope_id: chapterhouse.id,
233
- content: "Chapterhouse memory P1 shipped on 2026-05-13",
234
- source: "test",
235
- });
236
- const result = recall({
237
- query: "memory P1 shipped",
238
- scope_id: chapterhouse.id,
239
- kinds: ["observation"],
240
- limit: 10,
241
- });
242
- assert.equal(result.hits.some((hit) => hit.id === observation.id), true);
243
- });
244
- test("recall treats hyphenated observation query terms literally", async () => {
245
- const { dbModule, memoryModule } = await loadModules();
246
- dbModule.getDb();
247
- const getScope = getFunction(memoryModule, "getScope");
248
- const recordObservation = getFunction(memoryModule, "recordObservation");
249
- const recall = getFunction(memoryModule, "recall");
250
- const chapterhouse = getScope("chapterhouse");
251
- assert.ok(chapterhouse);
252
- const observation = recordObservation({
253
- scope_id: chapterhouse.id,
254
- content: "Chapterhouse agent-memory recall shipped safely",
255
- source: "test",
256
- });
257
- const result = recall({
258
- query: "agent-memory",
259
- scope_id: chapterhouse.id,
260
- kinds: ["observation"],
261
- limit: 10,
262
- });
263
- assert.equal(result.hits.some((hit) => hit.id === observation.id), true);
264
- });
265
- //# sourceMappingURL=recall.test.js.map