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.
Files changed (132) 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 +13 -10
  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 +92 -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/memory-assets/domain-skill.md +38 -0
  60. package/memory-assets/seed/cog-meta/improvements.md +8 -0
  61. package/memory-assets/seed/cog-meta/patterns.md +5 -0
  62. package/memory-assets/seed/cog-meta/reflect-cursor.md +4 -0
  63. package/memory-assets/seed/cog-meta/scenario-calibration.md +14 -0
  64. package/memory-assets/seed/cog-meta/self-observations.md +4 -0
  65. package/memory-assets/seed/domains.yml +19 -0
  66. package/memory-assets/seed/glacier/index.md +6 -0
  67. package/memory-assets/seed/hot-memory.md +5 -0
  68. package/memory-assets/seed/link-index.md +6 -0
  69. package/memory-assets/system-instructions.md +214 -0
  70. package/memory-assets/templates/action-items.md +8 -0
  71. package/memory-assets/templates/entities.md +4 -0
  72. package/memory-assets/templates/generic.md +2 -0
  73. package/memory-assets/templates/hot-memory.md +4 -0
  74. package/memory-assets/templates/observations.md +4 -0
  75. package/package.json +2 -1
  76. package/skills/system/evolve/SKILL.md +131 -0
  77. package/skills/system/foresight/SKILL.md +116 -0
  78. package/skills/system/history/SKILL.md +58 -0
  79. package/skills/system/housekeeping/SKILL.md +185 -0
  80. package/skills/system/reflect/SKILL.md +214 -0
  81. package/skills/system/scenario/SKILL.md +198 -0
  82. package/skills/system/setup/SKILL.md +113 -0
  83. package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
  84. package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
  85. package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
  86. package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
  87. package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
  88. package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
  89. package/web/dist/index.html +1 -1
  90. package/dist/api/routes/memory.js +0 -475
  91. package/dist/api/routes/memory.test.js +0 -108
  92. package/dist/copilot/tools/memory.js +0 -678
  93. package/dist/copilot/tools.memory.test.js +0 -590
  94. package/dist/memory/action-items.js +0 -100
  95. package/dist/memory/action-items.test.js +0 -83
  96. package/dist/memory/active-scope.js +0 -78
  97. package/dist/memory/active-scope.test.js +0 -80
  98. package/dist/memory/checkpoint-prompt.js +0 -71
  99. package/dist/memory/checkpoint.js +0 -274
  100. package/dist/memory/checkpoint.test.js +0 -275
  101. package/dist/memory/decisions.js +0 -54
  102. package/dist/memory/decisions.test.js +0 -92
  103. package/dist/memory/entities.js +0 -70
  104. package/dist/memory/entities.test.js +0 -65
  105. package/dist/memory/eot.js +0 -459
  106. package/dist/memory/eot.test.js +0 -949
  107. package/dist/memory/hooks.js +0 -149
  108. package/dist/memory/hooks.test.js +0 -325
  109. package/dist/memory/hot-tier.js +0 -283
  110. package/dist/memory/hot-tier.test.js +0 -275
  111. package/dist/memory/housekeeping-scheduler.js +0 -187
  112. package/dist/memory/housekeeping-scheduler.test.js +0 -236
  113. package/dist/memory/housekeeping.js +0 -497
  114. package/dist/memory/housekeeping.test.js +0 -410
  115. package/dist/memory/inbox.js +0 -83
  116. package/dist/memory/inbox.test.js +0 -178
  117. package/dist/memory/migration.js +0 -244
  118. package/dist/memory/migration.test.js +0 -108
  119. package/dist/memory/observations.js +0 -46
  120. package/dist/memory/observations.test.js +0 -86
  121. package/dist/memory/recall.js +0 -269
  122. package/dist/memory/recall.test.js +0 -265
  123. package/dist/memory/reflect.js +0 -273
  124. package/dist/memory/reflect.test.js +0 -256
  125. package/dist/memory/scope-lock.js +0 -26
  126. package/dist/memory/scope-lock.test.js +0 -118
  127. package/dist/memory/scopes.js +0 -89
  128. package/dist/memory/scopes.test.js +0 -176
  129. package/dist/memory/tiering.js +0 -223
  130. package/dist/memory/tiering.test.js +0 -323
  131. package/dist/memory/types.js +0 -2
  132. package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
@@ -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