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/reflect.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { config } from "../config.js";
|
|
2
|
-
import { getClient } from "../copilot/client.js";
|
|
3
|
-
import { runOneShotPrompt } from "../copilot/oneshot.js";
|
|
4
|
-
import { childLogger } from "../util/logger.js";
|
|
5
|
-
import { getScope, listScopes } from "./scopes.js";
|
|
6
|
-
const log = childLogger("memory.reflect");
|
|
7
|
-
const MIN_GROUP_SIZE = 3;
|
|
8
|
-
const SIMILARITY_THRESHOLD = 0.35;
|
|
9
|
-
const MAX_GROUP_OBSERVATIONS = 8;
|
|
10
|
-
const GLOBAL_SCOPE_SLUG = "global";
|
|
11
|
-
function normalizeText(value) {
|
|
12
|
-
return value
|
|
13
|
-
.toLowerCase()
|
|
14
|
-
.replace(/[^a-z0-9\s]/g, " ")
|
|
15
|
-
.replace(/\s+/g, " ")
|
|
16
|
-
.trim();
|
|
17
|
-
}
|
|
18
|
-
function tokenize(value) {
|
|
19
|
-
return new Set(normalizeText(value)
|
|
20
|
-
.split(" ")
|
|
21
|
-
.map((token) => token.replace(/s$/u, ""))
|
|
22
|
-
.filter((token) => token.length > 2));
|
|
23
|
-
}
|
|
24
|
-
function overlapCount(left, right) {
|
|
25
|
-
let overlap = 0;
|
|
26
|
-
for (const token of left) {
|
|
27
|
-
if (right.has(token)) {
|
|
28
|
-
overlap++;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return overlap;
|
|
32
|
-
}
|
|
33
|
-
function jaccard(left, right) {
|
|
34
|
-
if (left.size === 0 || right.size === 0) {
|
|
35
|
-
return 0;
|
|
36
|
-
}
|
|
37
|
-
const overlap = overlapCount(left, right);
|
|
38
|
-
const union = left.size + right.size - overlap;
|
|
39
|
-
return union === 0 ? 0 : overlap / union;
|
|
40
|
-
}
|
|
41
|
-
function compareObservation(left, right) {
|
|
42
|
-
if (left.created_at !== right.created_at) {
|
|
43
|
-
return left.created_at.localeCompare(right.created_at);
|
|
44
|
-
}
|
|
45
|
-
return left.id - right.id;
|
|
46
|
-
}
|
|
47
|
-
function canonicalObservationIds(observations) {
|
|
48
|
-
return observations
|
|
49
|
-
.map((observation) => observation.id)
|
|
50
|
-
.sort((left, right) => left - right)
|
|
51
|
-
.join(",");
|
|
52
|
-
}
|
|
53
|
-
function buildEntityGroups(observations) {
|
|
54
|
-
const grouped = new Map();
|
|
55
|
-
for (const observation of observations) {
|
|
56
|
-
if (!observation.entity_id) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
const existing = grouped.get(observation.entity_id) ?? [];
|
|
60
|
-
existing.push(observation);
|
|
61
|
-
grouped.set(observation.entity_id, existing);
|
|
62
|
-
}
|
|
63
|
-
return [...grouped.entries()]
|
|
64
|
-
.map(([entityId, rows]) => ({
|
|
65
|
-
kind: "entity",
|
|
66
|
-
key: `entity:${entityId}`,
|
|
67
|
-
observations: rows.sort(compareObservation),
|
|
68
|
-
}))
|
|
69
|
-
.filter((group) => group.observations.length >= MIN_GROUP_SIZE);
|
|
70
|
-
}
|
|
71
|
-
function buildTopicGroups(observations) {
|
|
72
|
-
const tokensByObservation = new Map();
|
|
73
|
-
for (const observation of observations) {
|
|
74
|
-
tokensByObservation.set(observation.id, tokenize(observation.content));
|
|
75
|
-
}
|
|
76
|
-
const groups = [];
|
|
77
|
-
const seenKeys = new Set();
|
|
78
|
-
const ordered = [...observations].sort(compareObservation);
|
|
79
|
-
for (const seed of ordered) {
|
|
80
|
-
const seedTokens = tokensByObservation.get(seed.id) ?? new Set();
|
|
81
|
-
const cluster = ordered.filter((candidate) => {
|
|
82
|
-
const candidateTokens = tokensByObservation.get(candidate.id) ?? new Set();
|
|
83
|
-
return overlapCount(seedTokens, candidateTokens) >= 3
|
|
84
|
-
&& jaccard(seedTokens, candidateTokens) >= SIMILARITY_THRESHOLD;
|
|
85
|
-
});
|
|
86
|
-
if (cluster.length < MIN_GROUP_SIZE) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const key = `topic:${canonicalObservationIds(cluster)}`;
|
|
90
|
-
if (seenKeys.has(key)) {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
seenKeys.add(key);
|
|
94
|
-
groups.push({ kind: "topic", key, observations: cluster.sort(compareObservation) });
|
|
95
|
-
}
|
|
96
|
-
return groups;
|
|
97
|
-
}
|
|
98
|
-
function buildGroups(observations) {
|
|
99
|
-
const groups = [...buildEntityGroups(observations), ...buildTopicGroups(observations)];
|
|
100
|
-
const deduped = new Map();
|
|
101
|
-
for (const group of groups) {
|
|
102
|
-
const key = canonicalObservationIds(group.observations);
|
|
103
|
-
if (!deduped.has(key)) {
|
|
104
|
-
deduped.set(key, group);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return [...deduped.values()];
|
|
108
|
-
}
|
|
109
|
-
function buildFallbackPattern(group) {
|
|
110
|
-
const [first] = group.observations;
|
|
111
|
-
const entityTitle = first?.entity_name ? `${first.entity_name} pattern` : "Observed pattern";
|
|
112
|
-
return {
|
|
113
|
-
title: entityTitle,
|
|
114
|
-
summary: group.observations.slice(0, 3).map((observation) => observation.content).join(" "),
|
|
115
|
-
confidence: Math.max(0.5, Math.min(0.95, Number((group.observations.reduce((sum, observation) => sum + observation.confidence, 0) / group.observations.length).toFixed(2)))),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function parseSynthesizedPattern(raw, group) {
|
|
119
|
-
try {
|
|
120
|
-
const parsed = JSON.parse(raw);
|
|
121
|
-
if (typeof parsed.title === "string" && typeof parsed.summary === "string") {
|
|
122
|
-
return {
|
|
123
|
-
title: parsed.title.trim() || buildFallbackPattern(group).title,
|
|
124
|
-
summary: parsed.summary.trim() || buildFallbackPattern(group).summary,
|
|
125
|
-
confidence: typeof parsed.confidence === "number"
|
|
126
|
-
? Math.max(0, Math.min(1, parsed.confidence))
|
|
127
|
-
: buildFallbackPattern(group).confidence,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// Fall through to the heuristic fallback.
|
|
133
|
-
}
|
|
134
|
-
return buildFallbackPattern(group);
|
|
135
|
-
}
|
|
136
|
-
async function synthesizePattern(scopeSlug, group) {
|
|
137
|
-
const client = await getClient();
|
|
138
|
-
const system = [
|
|
139
|
-
"You synthesize durable memory patterns from repeated observations.",
|
|
140
|
-
"Return JSON only with keys: title, summary, confidence.",
|
|
141
|
-
"title should be short and specific.",
|
|
142
|
-
"summary should describe the stable pattern in 1-2 sentences.",
|
|
143
|
-
"confidence must be a number between 0 and 1.",
|
|
144
|
-
].join("\n");
|
|
145
|
-
const user = JSON.stringify({
|
|
146
|
-
scope: scopeSlug,
|
|
147
|
-
group_kind: group.kind,
|
|
148
|
-
observations: group.observations.slice(0, MAX_GROUP_OBSERVATIONS).map((observation) => ({
|
|
149
|
-
id: observation.id,
|
|
150
|
-
scope: observation.scope_slug,
|
|
151
|
-
entity: observation.entity_name,
|
|
152
|
-
content: observation.content,
|
|
153
|
-
source: observation.source,
|
|
154
|
-
confidence: observation.confidence,
|
|
155
|
-
created_at: observation.created_at,
|
|
156
|
-
})),
|
|
157
|
-
}, null, 2);
|
|
158
|
-
const response = await runOneShotPrompt({
|
|
159
|
-
client,
|
|
160
|
-
model: config.copilotModel,
|
|
161
|
-
system,
|
|
162
|
-
user,
|
|
163
|
-
expectJson: true,
|
|
164
|
-
});
|
|
165
|
-
return parseSynthesizedPattern(response.content, group);
|
|
166
|
-
}
|
|
167
|
-
function containsContradictionSignal(value) {
|
|
168
|
-
return /\b(?:no longer|changed to|now uses|now use|moved to|switched to|was .+ now .+)\b/i.test(value);
|
|
169
|
-
}
|
|
170
|
-
function countContradictions(groups) {
|
|
171
|
-
let contradictions = 0;
|
|
172
|
-
for (const group of groups) {
|
|
173
|
-
if (group.kind !== "entity") {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (group.observations.some((observation) => containsContradictionSignal(observation.content))) {
|
|
177
|
-
contradictions++;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return contradictions;
|
|
181
|
-
}
|
|
182
|
-
function loadObservations(scopeSlug, db) {
|
|
183
|
-
const scope = getScope(scopeSlug);
|
|
184
|
-
if (!scope) {
|
|
185
|
-
throw new Error(`Unknown memory scope '${scopeSlug}'.`);
|
|
186
|
-
}
|
|
187
|
-
const globalScope = scopeSlug === GLOBAL_SCOPE_SLUG ? null : getScope(GLOBAL_SCOPE_SLUG);
|
|
188
|
-
const scopeIds = globalScope ? [scope.id, globalScope.id] : [scope.id];
|
|
189
|
-
const placeholders = scopeIds.map(() => "?").join(", ");
|
|
190
|
-
return db.prepare(`
|
|
191
|
-
SELECT
|
|
192
|
-
o.id,
|
|
193
|
-
o.scope_id,
|
|
194
|
-
s.slug AS scope_slug,
|
|
195
|
-
o.entity_id,
|
|
196
|
-
e.name AS entity_name,
|
|
197
|
-
e.kind AS entity_kind,
|
|
198
|
-
o.content,
|
|
199
|
-
o.source,
|
|
200
|
-
o.tier,
|
|
201
|
-
o.confidence,
|
|
202
|
-
o.created_at
|
|
203
|
-
FROM mem_observations o
|
|
204
|
-
JOIN mem_scopes s ON s.id = o.scope_id
|
|
205
|
-
LEFT JOIN mem_entities e ON e.id = o.entity_id
|
|
206
|
-
WHERE o.scope_id IN (${placeholders})
|
|
207
|
-
AND o.tier IN ('hot', 'warm')
|
|
208
|
-
AND o.superseded_by IS NULL
|
|
209
|
-
AND o.archived_at IS NULL
|
|
210
|
-
ORDER BY o.created_at ASC, o.id ASC
|
|
211
|
-
`).all(...scopeIds);
|
|
212
|
-
}
|
|
213
|
-
function upsertPattern(db, scopeId, synthesized, observations) {
|
|
214
|
-
const sourceObservationIds = JSON.stringify(observations.map((observation) => observation.id));
|
|
215
|
-
const existing = db.prepare(`
|
|
216
|
-
SELECT id, title, source_observation_ids
|
|
217
|
-
FROM mem_patterns
|
|
218
|
-
WHERE scope_id = ? AND title = ?
|
|
219
|
-
ORDER BY id DESC
|
|
220
|
-
LIMIT 1
|
|
221
|
-
`).get(scopeId, synthesized.title);
|
|
222
|
-
if (existing) {
|
|
223
|
-
db.prepare(`
|
|
224
|
-
UPDATE mem_patterns
|
|
225
|
-
SET summary = ?,
|
|
226
|
-
source_observation_ids = ?,
|
|
227
|
-
confidence = ?,
|
|
228
|
-
tier = 'warm',
|
|
229
|
-
last_updated = CURRENT_TIMESTAMP
|
|
230
|
-
WHERE id = ?
|
|
231
|
-
`).run(synthesized.summary, sourceObservationIds, synthesized.confidence, existing.id);
|
|
232
|
-
return "updated";
|
|
233
|
-
}
|
|
234
|
-
db.prepare(`
|
|
235
|
-
INSERT INTO mem_patterns (
|
|
236
|
-
scope_id, title, summary, source_observation_ids, confidence, tier, created_at, last_updated
|
|
237
|
-
)
|
|
238
|
-
VALUES (?, ?, ?, ?, ?, 'warm', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
239
|
-
`).run(scopeId, synthesized.title, synthesized.summary, sourceObservationIds, synthesized.confidence);
|
|
240
|
-
return "created";
|
|
241
|
-
}
|
|
242
|
-
export async function reflectOnScope(scopeSlug, db) {
|
|
243
|
-
const scope = getScope(scopeSlug);
|
|
244
|
-
if (!scope) {
|
|
245
|
-
throw new Error(`Unknown memory scope '${scopeSlug}'.`);
|
|
246
|
-
}
|
|
247
|
-
const observations = loadObservations(scopeSlug, db);
|
|
248
|
-
const groups = buildGroups(observations);
|
|
249
|
-
const contradictionsFound = countContradictions(groups);
|
|
250
|
-
let patternsCreated = 0;
|
|
251
|
-
let patternsUpdated = 0;
|
|
252
|
-
for (const group of groups) {
|
|
253
|
-
const synthesized = await synthesizePattern(scopeSlug, group);
|
|
254
|
-
const outcome = upsertPattern(db, scope.id, synthesized, group.observations);
|
|
255
|
-
if (outcome === "created") {
|
|
256
|
-
patternsCreated++;
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
patternsUpdated++;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const result = { patternsCreated, patternsUpdated, contradictionsFound };
|
|
263
|
-
log.info({ scope: scopeSlug, ...result }, "memory.reflect.scope.complete");
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
export async function reflectAllScopes(db) {
|
|
267
|
-
const results = {};
|
|
268
|
-
for (const scope of listScopes().filter((entry) => entry.active)) {
|
|
269
|
-
results[scope.slug] = await reflectOnScope(scope.slug, db);
|
|
270
|
-
}
|
|
271
|
-
return results;
|
|
272
|
-
}
|
|
273
|
-
//# sourceMappingURL=reflect.js.map
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import test from "node:test";
|
|
5
|
-
const repoRoot = process.cwd();
|
|
6
|
-
const testWorkRoot = join(repoRoot, ".test-work");
|
|
7
|
-
let sandboxRoot = "";
|
|
8
|
-
function resetSandbox() {
|
|
9
|
-
mkdirSync(testWorkRoot, { recursive: true });
|
|
10
|
-
sandboxRoot = mkdtempSync(join(testWorkRoot, "memory-reflect-"));
|
|
11
|
-
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
12
|
-
}
|
|
13
|
-
async function loadBaseModules() {
|
|
14
|
-
const nonce = `${Date.now()}-${Math.random()}`;
|
|
15
|
-
const dbModule = await import(new URL(`../store/db.js?case=${nonce}`, import.meta.url).href);
|
|
16
|
-
const memoryModule = await import(new URL(`./index.js?case=${nonce}`, import.meta.url).href);
|
|
17
|
-
return { dbModule, memoryModule };
|
|
18
|
-
}
|
|
19
|
-
async function loadReflectModule(t, llmResponse) {
|
|
20
|
-
t.mock.module("../copilot/oneshot.js", {
|
|
21
|
-
namedExports: {
|
|
22
|
-
runOneShotPrompt: async () => ({ content: llmResponse, model: "mock-model", attempts: 1 }),
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
t.mock.module("../copilot/client.js", {
|
|
26
|
-
namedExports: {
|
|
27
|
-
getClient: async () => ({}),
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
t.mock.module("../util/logger.js", {
|
|
31
|
-
namedExports: {
|
|
32
|
-
logger: { info: () => { }, warn: () => { }, error: () => { } },
|
|
33
|
-
childLogger: () => ({ info: () => { }, warn: () => { }, error: () => { } }),
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
return await import(new URL(`./reflect.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
37
|
-
}
|
|
38
|
-
async function loadToolsModule(t) {
|
|
39
|
-
t.mock.module("../copilot/orchestrator.js", {
|
|
40
|
-
namedExports: {
|
|
41
|
-
getCurrentSourceChannel: () => "web",
|
|
42
|
-
getCurrentActivityCallback: () => undefined,
|
|
43
|
-
getCurrentActiveProjectRules: () => null,
|
|
44
|
-
getCurrentAuthenticatedUser: () => undefined,
|
|
45
|
-
getLastAuthenticatedUser: () => undefined,
|
|
46
|
-
getCurrentAuthorizationHeader: () => undefined,
|
|
47
|
-
getCurrentSessionKey: () => "session-reflect-test",
|
|
48
|
-
sendToAgentSession: async () => "",
|
|
49
|
-
invalidateOrchestratorSession: () => { },
|
|
50
|
-
maybeScheduleScopeChangeCheckpoint: () => { },
|
|
51
|
-
resetCheckpointSessionState: () => { },
|
|
52
|
-
switchSessionModel: async () => { },
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
t.mock.module("../memory/reflect.js", {
|
|
56
|
-
namedExports: {
|
|
57
|
-
reflectOnScope: async () => ({ patternsCreated: 1, patternsUpdated: 0, contradictionsFound: 0 }),
|
|
58
|
-
reflectAllScopes: async () => ({
|
|
59
|
-
chapterhouse: { patternsCreated: 1, patternsUpdated: 0, contradictionsFound: 0 },
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
t.mock.module("../util/logger.js", {
|
|
64
|
-
namedExports: {
|
|
65
|
-
logger: { info: () => { }, warn: () => { }, error: () => { } },
|
|
66
|
-
childLogger: () => ({ info: () => { }, warn: () => { }, error: () => { } }),
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
const nonce = `${Date.now()}-${Math.random()}`;
|
|
70
|
-
const toolsModule = await import(new URL(`../copilot/tools.js?case=${nonce}`, import.meta.url).href);
|
|
71
|
-
const agentsModule = await import(new URL(`../copilot/agents.js?case=${nonce}`, import.meta.url).href);
|
|
72
|
-
const dbModule = await import(new URL(`../store/db.js?case=${nonce}`, import.meta.url).href);
|
|
73
|
-
return { toolsModule, agentsModule, dbModule };
|
|
74
|
-
}
|
|
75
|
-
function getFunction(module, name) {
|
|
76
|
-
const value = module[name];
|
|
77
|
-
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
function findTool(tools, name) {
|
|
81
|
-
const tool = tools.find((entry) => entry.name === name);
|
|
82
|
-
assert.ok(tool, `${name} tool should be registered`);
|
|
83
|
-
return tool;
|
|
84
|
-
}
|
|
85
|
-
test.beforeEach(async () => {
|
|
86
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
87
|
-
dbModule.closeDb();
|
|
88
|
-
resetSandbox();
|
|
89
|
-
});
|
|
90
|
-
test.afterEach(async () => {
|
|
91
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
92
|
-
dbModule.closeDb();
|
|
93
|
-
if (sandboxRoot) {
|
|
94
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
test("reflectOnScope creates a pattern when three similar observations accumulate for one entity", async (t) => {
|
|
98
|
-
const { dbModule, memoryModule } = await loadBaseModules();
|
|
99
|
-
const db = dbModule.getDb();
|
|
100
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
101
|
-
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
102
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
103
|
-
const chapterhouse = getScope("chapterhouse");
|
|
104
|
-
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
105
|
-
const workerQueue = upsertEntity({ scope_id: chapterhouse.id, kind: "subsystem", name: "worker-queue" });
|
|
106
|
-
const first = recordObservation({
|
|
107
|
-
scope_id: chapterhouse.id,
|
|
108
|
-
entity_id: workerQueue.id,
|
|
109
|
-
content: "The worker queue serializes task execution through SQLite state.",
|
|
110
|
-
source: "test",
|
|
111
|
-
tier: "hot",
|
|
112
|
-
});
|
|
113
|
-
const second = recordObservation({
|
|
114
|
-
scope_id: chapterhouse.id,
|
|
115
|
-
entity_id: workerQueue.id,
|
|
116
|
-
content: "Worker queue execution is serialized using SQLite-backed state.",
|
|
117
|
-
source: "test",
|
|
118
|
-
tier: "warm",
|
|
119
|
-
});
|
|
120
|
-
const third = recordObservation({
|
|
121
|
-
scope_id: chapterhouse.id,
|
|
122
|
-
entity_id: workerQueue.id,
|
|
123
|
-
content: "SQLite keeps worker queue execution serialized across turns.",
|
|
124
|
-
source: "test",
|
|
125
|
-
tier: "warm",
|
|
126
|
-
});
|
|
127
|
-
const reflectModule = await loadReflectModule(t, JSON.stringify({
|
|
128
|
-
title: "Queue execution pattern",
|
|
129
|
-
summary: "Worker queue execution stays serialized through SQLite-backed coordination.",
|
|
130
|
-
confidence: 0.88,
|
|
131
|
-
}));
|
|
132
|
-
const result = await reflectModule.reflectOnScope("chapterhouse", db);
|
|
133
|
-
assert.equal(result.patternsCreated >= 1, true);
|
|
134
|
-
assert.equal(result.patternsCreated + result.patternsUpdated >= 1, true);
|
|
135
|
-
assert.equal(result.contradictionsFound, 0);
|
|
136
|
-
const pattern = db.prepare(`
|
|
137
|
-
SELECT title, summary, source_observation_ids, confidence, tier
|
|
138
|
-
FROM mem_patterns
|
|
139
|
-
ORDER BY id DESC
|
|
140
|
-
LIMIT 1
|
|
141
|
-
`).get();
|
|
142
|
-
assert.ok(pattern, "reflectOnScope should persist a pattern");
|
|
143
|
-
assert.equal(pattern.title, "Queue execution pattern");
|
|
144
|
-
assert.match(pattern.summary, /serialized/i);
|
|
145
|
-
assert.deepEqual(JSON.parse(pattern.source_observation_ids), [first.id, second.id, third.id]);
|
|
146
|
-
assert.equal(pattern.confidence, 0.88);
|
|
147
|
-
assert.equal(pattern.tier, "warm");
|
|
148
|
-
});
|
|
149
|
-
test("reflectOnScope counts contradictions inside the same entity group", async (t) => {
|
|
150
|
-
const { dbModule, memoryModule } = await loadBaseModules();
|
|
151
|
-
const db = dbModule.getDb();
|
|
152
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
153
|
-
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
154
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
155
|
-
const chapterhouse = getScope("chapterhouse");
|
|
156
|
-
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
157
|
-
const auth = upsertEntity({ scope_id: chapterhouse.id, kind: "subsystem", name: "auth" });
|
|
158
|
-
recordObservation({ scope_id: chapterhouse.id, entity_id: auth.id, content: "Auth used GitHub login for sign-in.", source: "test" });
|
|
159
|
-
recordObservation({ scope_id: chapterhouse.id, entity_id: auth.id, content: "Auth changed to Entra ID for sign-in.", source: "test" });
|
|
160
|
-
recordObservation({ scope_id: chapterhouse.id, entity_id: auth.id, content: "Auth no longer uses GitHub login.", source: "test" });
|
|
161
|
-
const reflectModule = await loadReflectModule(t, JSON.stringify({
|
|
162
|
-
title: "Auth provider transition",
|
|
163
|
-
summary: "Authentication moved away from GitHub login toward Entra ID.",
|
|
164
|
-
confidence: 0.82,
|
|
165
|
-
}));
|
|
166
|
-
const result = await reflectModule.reflectOnScope("chapterhouse", db);
|
|
167
|
-
assert.equal(result.contradictionsFound, 1);
|
|
168
|
-
});
|
|
169
|
-
test("reflectOnScope folds global observations into project-scope reflection", async (t) => {
|
|
170
|
-
const { dbModule, memoryModule } = await loadBaseModules();
|
|
171
|
-
const db = dbModule.getDb();
|
|
172
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
173
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
174
|
-
const chapterhouse = getScope("chapterhouse");
|
|
175
|
-
const global = getScope("global");
|
|
176
|
-
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
177
|
-
assert.ok(global, "global scope should be seeded");
|
|
178
|
-
const globalObservation = recordObservation({
|
|
179
|
-
scope_id: global.id,
|
|
180
|
-
content: "SQLite WAL keeps Chapterhouse memory writes fast.",
|
|
181
|
-
source: "test",
|
|
182
|
-
tier: "warm",
|
|
183
|
-
});
|
|
184
|
-
const scopedOne = recordObservation({
|
|
185
|
-
scope_id: chapterhouse.id,
|
|
186
|
-
content: "Chapterhouse memory writes stay fast because SQLite uses WAL mode.",
|
|
187
|
-
source: "test",
|
|
188
|
-
tier: "hot",
|
|
189
|
-
});
|
|
190
|
-
const scopedTwo = recordObservation({
|
|
191
|
-
scope_id: chapterhouse.id,
|
|
192
|
-
content: "WAL mode keeps Chapterhouse memory writes quick under concurrency.",
|
|
193
|
-
source: "test",
|
|
194
|
-
tier: "warm",
|
|
195
|
-
});
|
|
196
|
-
const reflectModule = await loadReflectModule(t, JSON.stringify({
|
|
197
|
-
title: "SQLite WAL performance pattern",
|
|
198
|
-
summary: "Across scopes, Chapterhouse relies on SQLite WAL mode for fast memory writes.",
|
|
199
|
-
confidence: 0.91,
|
|
200
|
-
}));
|
|
201
|
-
const result = await reflectModule.reflectOnScope("chapterhouse", db);
|
|
202
|
-
assert.equal(result.patternsCreated, 1);
|
|
203
|
-
const pattern = db.prepare(`
|
|
204
|
-
SELECT source_observation_ids
|
|
205
|
-
FROM mem_patterns
|
|
206
|
-
ORDER BY id DESC
|
|
207
|
-
LIMIT 1
|
|
208
|
-
`).get();
|
|
209
|
-
assert.ok(pattern, "cross-scope reflection should persist a pattern");
|
|
210
|
-
assert.deepEqual(JSON.parse(pattern.source_observation_ids), [globalObservation.id, scopedOne.id, scopedTwo.id]);
|
|
211
|
-
});
|
|
212
|
-
test("memory_reflect runs end-to-end for chapterhouse and is only bound to orchestrator tools", async (t) => {
|
|
213
|
-
const { toolsModule, agentsModule, dbModule } = await loadToolsModule(t);
|
|
214
|
-
const db = dbModule.getDb();
|
|
215
|
-
const tools = toolsModule.createTools({
|
|
216
|
-
client: { async listModels() { return []; } },
|
|
217
|
-
onAgentTaskComplete: () => { },
|
|
218
|
-
});
|
|
219
|
-
const bindToolsToAgent = agentsModule.bindToolsToAgent;
|
|
220
|
-
const filterToolsForAgent = agentsModule.filterToolsForAgent;
|
|
221
|
-
assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
|
|
222
|
-
assert.equal(typeof filterToolsForAgent, "function", "filterToolsForAgent should be exported");
|
|
223
|
-
const chapterhouseVisibleTools = filterToolsForAgent({
|
|
224
|
-
slug: "chapterhouse",
|
|
225
|
-
name: "Chapterhouse",
|
|
226
|
-
description: "Orchestrator",
|
|
227
|
-
model: "auto",
|
|
228
|
-
systemMessage: "test",
|
|
229
|
-
}, tools);
|
|
230
|
-
const coderVisibleTools = filterToolsForAgent({
|
|
231
|
-
slug: "coder",
|
|
232
|
-
name: "Coder",
|
|
233
|
-
description: "Software engineer",
|
|
234
|
-
model: "gpt-5.4",
|
|
235
|
-
systemMessage: "test",
|
|
236
|
-
}, tools);
|
|
237
|
-
const chapterhouseTools = bindToolsToAgent("chapterhouse", chapterhouseVisibleTools);
|
|
238
|
-
const coderTools = bindToolsToAgent("coder", coderVisibleTools);
|
|
239
|
-
assert.equal(chapterhouseTools.some((tool) => tool.name === "memory_reflect"), true);
|
|
240
|
-
assert.equal(coderTools.some((tool) => tool.name === "memory_reflect"), false);
|
|
241
|
-
const scope = db.prepare(`SELECT id FROM mem_scopes WHERE slug = 'chapterhouse'`).get();
|
|
242
|
-
db.prepare(`
|
|
243
|
-
INSERT INTO mem_observations (scope_id, content, source, tier)
|
|
244
|
-
VALUES (?, ?, 'test', 'hot'), (?, ?, 'test', 'warm'), (?, ?, 'test', 'warm')
|
|
245
|
-
`).run(scope.id, "The worker queue serializes task execution through SQLite state.", scope.id, "Worker queue execution is serialized using SQLite-backed state.", scope.id, "SQLite keeps worker queue execution serialized across turns.");
|
|
246
|
-
const memoryReflect = findTool(chapterhouseTools, "memory_reflect");
|
|
247
|
-
const result = await memoryReflect.handler({ scope: "chapterhouse" }, {});
|
|
248
|
-
assert.deepEqual(result, {
|
|
249
|
-
ok: true,
|
|
250
|
-
scope: "chapterhouse",
|
|
251
|
-
patterns_created: 1,
|
|
252
|
-
patterns_updated: 0,
|
|
253
|
-
contradictions_found: 0,
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
//# sourceMappingURL=reflect.test.js.map
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const inFlightScopeWrites = new Set();
|
|
2
|
-
function uniqueScopeIds(scopeIds) {
|
|
3
|
-
return [...new Set(scopeIds)].sort((left, right) => left - right);
|
|
4
|
-
}
|
|
5
|
-
export function tryAcquireScopeWriteLocks(scopeIds) {
|
|
6
|
-
const uniqueScopeIdsToAcquire = uniqueScopeIds(scopeIds);
|
|
7
|
-
if (uniqueScopeIdsToAcquire.some((scopeId) => inFlightScopeWrites.has(scopeId))) {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
for (const scopeId of uniqueScopeIdsToAcquire) {
|
|
11
|
-
inFlightScopeWrites.add(scopeId);
|
|
12
|
-
}
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
export function releaseScopeWriteLocks(scopeIds) {
|
|
16
|
-
for (const scopeId of uniqueScopeIds(scopeIds)) {
|
|
17
|
-
inFlightScopeWrites.delete(scopeId);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
export function isScopeWriteLocked(scopeIds) {
|
|
21
|
-
if (!scopeIds || scopeIds.length === 0) {
|
|
22
|
-
return inFlightScopeWrites.size > 0;
|
|
23
|
-
}
|
|
24
|
-
return uniqueScopeIds(scopeIds).some((scopeId) => inFlightScopeWrites.has(scopeId));
|
|
25
|
-
}
|
|
26
|
-
//# sourceMappingURL=scope-lock.js.map
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
const sharedScope = {
|
|
4
|
-
id: 11,
|
|
5
|
-
slug: "shared",
|
|
6
|
-
title: "Shared",
|
|
7
|
-
description: "Shared scope for lock tests.",
|
|
8
|
-
keywords: ["shared"],
|
|
9
|
-
active: true,
|
|
10
|
-
createdAt: new Date().toISOString(),
|
|
11
|
-
updatedAt: new Date().toISOString(),
|
|
12
|
-
};
|
|
13
|
-
async function loadModules(t, tieringPass) {
|
|
14
|
-
t.mock.module("../config.js", {
|
|
15
|
-
namedExports: {
|
|
16
|
-
config: {
|
|
17
|
-
copilotModel: "test-model",
|
|
18
|
-
memoryDecayDays: 30,
|
|
19
|
-
memoryInboxRetentionDays: 7,
|
|
20
|
-
memoryCheckpointTurns: 5,
|
|
21
|
-
memoryCheckpointEnabled: true,
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
t.mock.module("../copilot/oneshot.js", {
|
|
26
|
-
namedExports: {
|
|
27
|
-
runOneShotPrompt: async () => ({ content: JSON.stringify({ proposals: [] }) }),
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
t.mock.module("../store/db.js", {
|
|
31
|
-
namedExports: {
|
|
32
|
-
getDb: () => {
|
|
33
|
-
throw new Error("getDb should not be called in this test");
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
t.mock.module("../util/logger.js", {
|
|
38
|
-
namedExports: {
|
|
39
|
-
childLogger: () => ({
|
|
40
|
-
info: () => { },
|
|
41
|
-
warn: () => { },
|
|
42
|
-
error: () => { },
|
|
43
|
-
}),
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
t.mock.module("./active-scope.js", {
|
|
47
|
-
namedExports: {
|
|
48
|
-
getActiveScope: () => sharedScope,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
t.mock.module("./scopes.js", {
|
|
52
|
-
namedExports: {
|
|
53
|
-
getScope: (slug) => (slug === sharedScope.slug ? sharedScope : null),
|
|
54
|
-
listScopes: () => [{ id: sharedScope.id, active: true }],
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
t.mock.module("./entities.js", {
|
|
58
|
-
namedExports: {
|
|
59
|
-
listEntities: () => [],
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
t.mock.module("./decisions.js", {
|
|
63
|
-
namedExports: {
|
|
64
|
-
listDecisions: () => [],
|
|
65
|
-
recordDecision: () => ({ id: 1 }),
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
t.mock.module("./observations.js", {
|
|
69
|
-
namedExports: {
|
|
70
|
-
listObservations: () => [],
|
|
71
|
-
recordObservation: () => ({ id: 1 }),
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
t.mock.module("./checkpoint-prompt.js", {
|
|
75
|
-
namedExports: {
|
|
76
|
-
buildCheckpointSystemPrompt: () => "system",
|
|
77
|
-
buildCheckpointUserPrompt: () => "user",
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
t.mock.module("./tiering.js", {
|
|
81
|
-
namedExports: {
|
|
82
|
-
tieringPass,
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
const checkpointModule = await import(new URL(`./checkpoint.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
86
|
-
const housekeepingModule = await import(new URL(`./housekeeping.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
87
|
-
return { checkpointModule, housekeepingModule };
|
|
88
|
-
}
|
|
89
|
-
test("runCheckpointExtraction skips writes while housekeeping holds the same scope lock", async (t) => {
|
|
90
|
-
let releaseHousekeeping;
|
|
91
|
-
const { checkpointModule, housekeepingModule } = await loadModules(t, async (scopeId) => {
|
|
92
|
-
assert.equal(scopeId, sharedScope.id);
|
|
93
|
-
await new Promise((resolve) => {
|
|
94
|
-
releaseHousekeeping = resolve;
|
|
95
|
-
});
|
|
96
|
-
return { pass: `tieringPass:${scopeId}`, examined: 1, modified: 1, errors: [] };
|
|
97
|
-
});
|
|
98
|
-
const housekeepingRun = housekeepingModule.runHousekeeping({ scopeIds: [sharedScope.id], passes: ["tiering"] });
|
|
99
|
-
await Promise.resolve();
|
|
100
|
-
const checkpointResult = await checkpointModule.runCheckpointExtraction({
|
|
101
|
-
turns: [{ user: "Remember this.", assistant: "Okay." }],
|
|
102
|
-
activeScope: sharedScope,
|
|
103
|
-
copilotClient: {},
|
|
104
|
-
callLLM: async () => JSON.stringify({
|
|
105
|
-
proposals: [
|
|
106
|
-
{
|
|
107
|
-
kind: "observation",
|
|
108
|
-
content: "Shared scope writes should not overlap housekeeping.",
|
|
109
|
-
confidence: 0.95,
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
assert.deepEqual(checkpointResult, { written: 0, skipped: 0, errors: [] });
|
|
115
|
-
releaseHousekeeping?.();
|
|
116
|
-
await housekeepingRun;
|
|
117
|
-
});
|
|
118
|
-
//# sourceMappingURL=scope-lock.test.js.map
|