chapterhouse 0.13.1 → 0.14.1
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.
- package/dist/api/route-coverage.test.js +1 -3
- package/dist/api/server.js +0 -2
- package/dist/api/server.test.js +0 -281
- package/dist/config.js +3 -85
- package/dist/config.test.js +5 -123
- package/dist/copilot/agents.js +13 -10
- package/dist/copilot/agents.test.js +10 -11
- package/dist/copilot/memory-coordinator.js +12 -227
- package/dist/copilot/memory-coordinator.test.js +31 -250
- package/dist/copilot/orchestrator.js +8 -66
- package/dist/copilot/orchestrator.test.js +9 -467
- package/dist/copilot/skills.js +15 -1
- package/dist/copilot/system-message.js +9 -15
- package/dist/copilot/system-message.test.js +9 -22
- package/dist/copilot/tools/index.js +3 -3
- package/dist/copilot/tools-deps.js +1 -1
- package/dist/copilot/tools.agent.test.js +6 -0
- package/dist/copilot/tools.inventory.test.js +1 -14
- package/dist/daemon.js +7 -9
- package/dist/memory/assets.js +33 -0
- package/dist/memory/domains.js +58 -0
- package/dist/memory/domains.test.js +47 -0
- package/dist/memory/git.js +66 -0
- package/dist/memory/git.test.js +32 -0
- package/dist/memory/history.js +19 -0
- package/dist/memory/hottier.js +32 -0
- package/dist/memory/hottier.test.js +33 -0
- package/dist/memory/index.js +5 -13
- package/dist/memory/instructions.js +17 -0
- package/dist/memory/manager.js +92 -0
- package/dist/memory/markdown.js +78 -0
- package/dist/memory/markdown.test.js +42 -0
- package/dist/memory/mutex.js +18 -0
- package/dist/memory/path-guard.js +26 -0
- package/dist/memory/path-guard.test.js +27 -0
- package/dist/memory/paths.js +12 -0
- package/dist/memory/reconcile.js +75 -0
- package/dist/memory/reconcile.test.js +50 -0
- package/dist/memory/scaffold.js +37 -0
- package/dist/memory/scaffold.test.js +52 -0
- package/dist/memory/tools/commit-wrapper.js +32 -0
- package/dist/memory/tools/domains.js +73 -0
- package/dist/memory/tools/domains.test.js +66 -0
- package/dist/memory/tools/git.js +52 -0
- package/dist/memory/tools/index.js +25 -0
- package/dist/memory/tools/read.js +101 -0
- package/dist/memory/tools/read.test.js +69 -0
- package/dist/memory/tools/search.js +103 -0
- package/dist/memory/tools/search.test.js +63 -0
- package/dist/memory/tools/sessions.js +45 -0
- package/dist/memory/tools/sessions.test.js +74 -0
- package/dist/memory/tools/shared.js +7 -0
- package/dist/memory/tools/write.js +116 -0
- package/dist/memory/tools/write.test.js +107 -0
- package/dist/memory/walk.js +39 -0
- package/dist/store/repositories/sessions.js +40 -0
- package/dist/wiki/consolidation.js +3 -31
- package/dist/wiki/consolidation.test.js +0 -19
- package/memory-assets/domain-skill.md +38 -0
- package/memory-assets/seed/cog-meta/improvements.md +8 -0
- package/memory-assets/seed/cog-meta/patterns.md +5 -0
- package/memory-assets/seed/cog-meta/reflect-cursor.md +4 -0
- package/memory-assets/seed/cog-meta/scenario-calibration.md +14 -0
- package/memory-assets/seed/cog-meta/self-observations.md +4 -0
- package/memory-assets/seed/domains.yml +19 -0
- package/memory-assets/seed/glacier/index.md +6 -0
- package/memory-assets/seed/hot-memory.md +5 -0
- package/memory-assets/seed/link-index.md +6 -0
- package/memory-assets/system-instructions.md +214 -0
- package/memory-assets/templates/action-items.md +8 -0
- package/memory-assets/templates/entities.md +4 -0
- package/memory-assets/templates/generic.md +2 -0
- package/memory-assets/templates/hot-memory.md +4 -0
- package/memory-assets/templates/observations.md +4 -0
- package/package.json +2 -1
- package/skills/system/evolve/SKILL.md +131 -0
- package/skills/system/foresight/SKILL.md +116 -0
- package/skills/system/history/SKILL.md +58 -0
- package/skills/system/housekeeping/SKILL.md +185 -0
- package/skills/system/reflect/SKILL.md +214 -0
- package/skills/system/scenario/SKILL.md +198 -0
- package/skills/system/setup/SKILL.md +113 -0
- package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
- package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
- package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
- package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/dist/api/routes/memory.js +0 -475
- package/dist/api/routes/memory.test.js +0 -108
- package/dist/copilot/tools/memory.js +0 -678
- package/dist/copilot/tools.memory.test.js +0 -590
- package/dist/memory/action-items.js +0 -100
- package/dist/memory/action-items.test.js +0 -83
- package/dist/memory/active-scope.js +0 -78
- package/dist/memory/active-scope.test.js +0 -80
- package/dist/memory/checkpoint-prompt.js +0 -71
- package/dist/memory/checkpoint.js +0 -274
- package/dist/memory/checkpoint.test.js +0 -275
- package/dist/memory/decisions.js +0 -54
- package/dist/memory/decisions.test.js +0 -92
- package/dist/memory/entities.js +0 -70
- package/dist/memory/entities.test.js +0 -65
- package/dist/memory/eot.js +0 -459
- package/dist/memory/eot.test.js +0 -949
- package/dist/memory/hooks.js +0 -149
- package/dist/memory/hooks.test.js +0 -325
- package/dist/memory/hot-tier.js +0 -283
- package/dist/memory/hot-tier.test.js +0 -275
- package/dist/memory/housekeeping-scheduler.js +0 -187
- package/dist/memory/housekeeping-scheduler.test.js +0 -236
- package/dist/memory/housekeeping.js +0 -497
- package/dist/memory/housekeeping.test.js +0 -410
- package/dist/memory/inbox.js +0 -83
- package/dist/memory/inbox.test.js +0 -178
- package/dist/memory/migration.js +0 -244
- package/dist/memory/migration.test.js +0 -108
- package/dist/memory/observations.js +0 -46
- package/dist/memory/observations.test.js +0 -86
- package/dist/memory/recall.js +0 -269
- package/dist/memory/recall.test.js +0 -265
- package/dist/memory/reflect.js +0 -273
- package/dist/memory/reflect.test.js +0 -256
- package/dist/memory/scope-lock.js +0 -26
- package/dist/memory/scope-lock.test.js +0 -118
- package/dist/memory/scopes.js +0 -89
- package/dist/memory/scopes.test.js +0 -176
- package/dist/memory/tiering.js +0 -223
- package/dist/memory/tiering.test.js +0 -323
- package/dist/memory/types.js +0 -2
- package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
package/dist/memory/recall.js
DELETED
|
@@ -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
|