chapterhouse 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +84 -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/package.json +1 -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
|
@@ -1,410 +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-housekeeping-${process.pid}`);
|
|
7
|
-
const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
|
|
8
|
-
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
9
|
-
process.env.CHAPTERHOUSE_MEMORY_DECAY_DAYS = "30";
|
|
10
|
-
process.env.CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS = "7";
|
|
11
|
-
function resetSandbox() {
|
|
12
|
-
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
13
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
14
|
-
mkdirSync(chapterhouseHome, { recursive: true });
|
|
15
|
-
}
|
|
16
|
-
async function loadModules() {
|
|
17
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
18
|
-
const memoryModule = await import(new URL("./index.js", import.meta.url).href);
|
|
19
|
-
const housekeepingModule = await import(new URL(`./housekeeping.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
20
|
-
return { dbModule, memoryModule, housekeepingModule };
|
|
21
|
-
}
|
|
22
|
-
async function loadMockedHousekeepingModule(t, options = {}) {
|
|
23
|
-
t.mock.module("../config.js", {
|
|
24
|
-
namedExports: {
|
|
25
|
-
config: {
|
|
26
|
-
memoryDecayDays: 30,
|
|
27
|
-
memoryInboxRetentionDays: 7,
|
|
28
|
-
memoryHousekeepingSimilarityThreshold: 0.8,
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
t.mock.module("../store/db.js", {
|
|
33
|
-
namedExports: {
|
|
34
|
-
getDb: () => {
|
|
35
|
-
throw new Error("getDb should not be called in this test");
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
t.mock.module("../util/logger.js", {
|
|
40
|
-
namedExports: {
|
|
41
|
-
childLogger: () => ({
|
|
42
|
-
info: () => { },
|
|
43
|
-
warn: () => { },
|
|
44
|
-
error: () => { },
|
|
45
|
-
}),
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
t.mock.module("./active-scope.js", {
|
|
49
|
-
namedExports: {
|
|
50
|
-
getActiveScope: () => (options.activeScopeId === undefined ? null : { id: options.activeScopeId }),
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
t.mock.module("./scopes.js", {
|
|
54
|
-
namedExports: {
|
|
55
|
-
listScopes: () => options.scopes ?? [],
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
t.mock.module("./tiering.js", {
|
|
59
|
-
namedExports: {
|
|
60
|
-
tieringPass: (scopeId) => options.tieringPass?.(scopeId) ?? { pass: "tieringPass", examined: scopeId, modified: 1, errors: [] },
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
return await import(new URL(`./housekeeping.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
64
|
-
}
|
|
65
|
-
function getFunction(module, name) {
|
|
66
|
-
const value = module[name];
|
|
67
|
-
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
function createTestScope(memoryModule, slug) {
|
|
71
|
-
const createScope = getFunction(memoryModule, "createScope");
|
|
72
|
-
return createScope({
|
|
73
|
-
slug,
|
|
74
|
-
title: slug.split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" "),
|
|
75
|
-
description: `${slug} test scope`,
|
|
76
|
-
keywords: [slug],
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
test.beforeEach(async () => {
|
|
80
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
81
|
-
dbModule.closeDb();
|
|
82
|
-
resetSandbox();
|
|
83
|
-
});
|
|
84
|
-
test.after(async () => {
|
|
85
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
86
|
-
dbModule.closeDb();
|
|
87
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
88
|
-
delete process.env.CHAPTERHOUSE_MEMORY_DECAY_DAYS;
|
|
89
|
-
delete process.env.CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS;
|
|
90
|
-
});
|
|
91
|
-
test("dedupObservationsPass supersedes similar observations in scope deterministically and is idempotent", async () => {
|
|
92
|
-
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
93
|
-
const db = dbModule.getDb();
|
|
94
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
95
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
96
|
-
const chapterhouse = getScope("chapterhouse");
|
|
97
|
-
const team = createTestScope(memoryModule, "team");
|
|
98
|
-
assert.ok(chapterhouse);
|
|
99
|
-
const first = recordObservation({
|
|
100
|
-
scope_id: team.id,
|
|
101
|
-
content: "The worker event stream uses server sent events for live task output.",
|
|
102
|
-
source: "test",
|
|
103
|
-
confidence: 0.4,
|
|
104
|
-
});
|
|
105
|
-
const keeper = recordObservation({
|
|
106
|
-
scope_id: team.id,
|
|
107
|
-
content: "Worker event streams use server sent events for live task output.",
|
|
108
|
-
source: "test",
|
|
109
|
-
confidence: 0.9,
|
|
110
|
-
});
|
|
111
|
-
const third = recordObservation({
|
|
112
|
-
scope_id: team.id,
|
|
113
|
-
content: "The worker event stream uses server sent events for live task output today.",
|
|
114
|
-
source: "test",
|
|
115
|
-
confidence: 0.9,
|
|
116
|
-
});
|
|
117
|
-
const otherScope = recordObservation({
|
|
118
|
-
scope_id: chapterhouse.id,
|
|
119
|
-
content: "Worker event streams use server sent events for live task output.",
|
|
120
|
-
source: "test",
|
|
121
|
-
confidence: 0.1,
|
|
122
|
-
});
|
|
123
|
-
const summary = housekeepingModule.dedupObservationsPass(team.id);
|
|
124
|
-
assert.equal(summary.pass, "dedupObservationsPass");
|
|
125
|
-
assert.equal(summary.examined, 3);
|
|
126
|
-
assert.equal(summary.modified, 2);
|
|
127
|
-
assert.deepEqual(summary.errors, []);
|
|
128
|
-
assert.deepEqual(db.prepare(`SELECT id, superseded_by FROM mem_observations WHERE id IN (?, ?, ?) ORDER BY id`).all(first.id, keeper.id, third.id), [
|
|
129
|
-
{ id: first.id, superseded_by: keeper.id },
|
|
130
|
-
{ id: keeper.id, superseded_by: null },
|
|
131
|
-
{ id: third.id, superseded_by: keeper.id },
|
|
132
|
-
]);
|
|
133
|
-
assert.equal(db.prepare(`SELECT superseded_by FROM mem_observations WHERE id = ?`).get(otherScope.id).superseded_by, null);
|
|
134
|
-
const second = housekeepingModule.dedupObservationsPass(team.id);
|
|
135
|
-
assert.equal(second.modified, 0);
|
|
136
|
-
});
|
|
137
|
-
test("dedupDecisionsPass supersedes similar active decisions within scope and keeps the latest decision", async () => {
|
|
138
|
-
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
139
|
-
const db = dbModule.getDb();
|
|
140
|
-
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
141
|
-
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
142
|
-
const team = createTestScope(memoryModule, "team");
|
|
143
|
-
const infra = createTestScope(memoryModule, "infra");
|
|
144
|
-
const api = upsertEntity({ scope_id: team.id, kind: "component", name: "api" });
|
|
145
|
-
const web = upsertEntity({ scope_id: team.id, kind: "component", name: "web" });
|
|
146
|
-
const oldDecision = recordDecision({
|
|
147
|
-
scope_id: team.id,
|
|
148
|
-
entity_id: api.id,
|
|
149
|
-
title: "Use SQLite FTS5 for memory recall",
|
|
150
|
-
rationale: "Initial choice.",
|
|
151
|
-
decided_at: "2026-05-11",
|
|
152
|
-
});
|
|
153
|
-
const keeper = recordDecision({
|
|
154
|
-
scope_id: team.id,
|
|
155
|
-
entity_id: api.id,
|
|
156
|
-
title: "Use SQLite FTS5 for scoped memory recall",
|
|
157
|
-
rationale: "Latest choice.",
|
|
158
|
-
decided_at: "2026-05-13",
|
|
159
|
-
});
|
|
160
|
-
const otherEntity = recordDecision({
|
|
161
|
-
scope_id: team.id,
|
|
162
|
-
entity_id: web.id,
|
|
163
|
-
title: "Use SQLite FTS5 for memory recall",
|
|
164
|
-
rationale: "Same title, different entity context, but newer scope-level decision.",
|
|
165
|
-
decided_at: "2026-05-14",
|
|
166
|
-
});
|
|
167
|
-
const otherScope = recordDecision({
|
|
168
|
-
scope_id: infra.id,
|
|
169
|
-
title: "Use SQLite FTS5 for memory recall",
|
|
170
|
-
rationale: "Same title, different scope.",
|
|
171
|
-
decided_at: "2026-05-14",
|
|
172
|
-
});
|
|
173
|
-
const summary = housekeepingModule.dedupDecisionsPass(team.id);
|
|
174
|
-
assert.equal(summary.pass, "dedupDecisionsPass");
|
|
175
|
-
assert.equal(summary.examined, 3);
|
|
176
|
-
assert.equal(summary.modified, 2);
|
|
177
|
-
assert.deepEqual(summary.errors, []);
|
|
178
|
-
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(oldDecision.id).superseded_by, otherEntity.id);
|
|
179
|
-
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(keeper.id).superseded_by, otherEntity.id);
|
|
180
|
-
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherEntity.id).superseded_by, null);
|
|
181
|
-
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherScope.id).superseded_by, null);
|
|
182
|
-
const second = housekeepingModule.dedupDecisionsPass(team.id);
|
|
183
|
-
assert.equal(second.modified, 0);
|
|
184
|
-
});
|
|
185
|
-
test("compactSupersedeChainsPass collapses long supersede chains without looping on cycles", async () => {
|
|
186
|
-
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
187
|
-
const db = dbModule.getDb();
|
|
188
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
189
|
-
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
190
|
-
const team = createTestScope(memoryModule, "team");
|
|
191
|
-
const firstObservation = recordObservation({ scope_id: team.id, content: "First observation", source: "test" });
|
|
192
|
-
const middleObservation = recordObservation({ scope_id: team.id, content: "Middle observation", source: "test" });
|
|
193
|
-
const keeperObservation = recordObservation({ scope_id: team.id, content: "Keeper observation", source: "test" });
|
|
194
|
-
const cycleLeft = recordObservation({ scope_id: team.id, content: "Cycle left", source: "test" });
|
|
195
|
-
const cycleRight = recordObservation({ scope_id: team.id, content: "Cycle right", source: "test" });
|
|
196
|
-
db.prepare(`UPDATE mem_observations SET superseded_by = ? WHERE id = ?`).run(middleObservation.id, firstObservation.id);
|
|
197
|
-
db.prepare(`UPDATE mem_observations SET superseded_by = ? WHERE id = ?`).run(keeperObservation.id, middleObservation.id);
|
|
198
|
-
db.prepare(`UPDATE mem_observations SET superseded_by = ? WHERE id = ?`).run(cycleRight.id, cycleLeft.id);
|
|
199
|
-
db.prepare(`UPDATE mem_observations SET superseded_by = ? WHERE id = ?`).run(cycleLeft.id, cycleRight.id);
|
|
200
|
-
const firstDecision = recordDecision({ scope_id: team.id, title: "Use local memory", rationale: "Initial.", decided_at: "2026-05-10" });
|
|
201
|
-
const middleDecision = recordDecision({ scope_id: team.id, title: "Use scoped memory", rationale: "Refined.", decided_at: "2026-05-11" });
|
|
202
|
-
const keeperDecision = recordDecision({ scope_id: team.id, title: "Use tiered scoped memory", rationale: "Final.", decided_at: "2026-05-12" });
|
|
203
|
-
db.prepare(`UPDATE mem_decisions SET superseded_by = ? WHERE id = ?`).run(middleDecision.id, firstDecision.id);
|
|
204
|
-
db.prepare(`UPDATE mem_decisions SET superseded_by = ? WHERE id = ?`).run(keeperDecision.id, middleDecision.id);
|
|
205
|
-
const summary = housekeepingModule.compactSupersedeChainsPass(team.id);
|
|
206
|
-
assert.equal(summary.pass, "compactSupersedeChainsPass");
|
|
207
|
-
assert.equal(summary.modified, 2);
|
|
208
|
-
assert.deepEqual(summary.errors, []);
|
|
209
|
-
assert.equal(db.prepare(`SELECT superseded_by FROM mem_observations WHERE id = ?`).get(firstObservation.id).superseded_by, keeperObservation.id);
|
|
210
|
-
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(firstDecision.id).superseded_by, keeperDecision.id);
|
|
211
|
-
assert.deepEqual(db.prepare(`SELECT id, superseded_by FROM mem_observations WHERE id IN (?, ?) ORDER BY id`).all(cycleLeft.id, cycleRight.id), [
|
|
212
|
-
{ id: cycleLeft.id, superseded_by: cycleRight.id },
|
|
213
|
-
{ id: cycleRight.id, superseded_by: cycleLeft.id },
|
|
214
|
-
]);
|
|
215
|
-
assert.equal(housekeepingModule.compactSupersedeChainsPass(team.id).modified, 0);
|
|
216
|
-
});
|
|
217
|
-
test("orphanCleanupPass clears missing observation entity references without touching valid or out-of-scope rows", async () => {
|
|
218
|
-
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
219
|
-
const db = dbModule.getDb();
|
|
220
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
221
|
-
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
222
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
223
|
-
const chapterhouse = getScope("chapterhouse");
|
|
224
|
-
const team = createTestScope(memoryModule, "team");
|
|
225
|
-
assert.ok(chapterhouse);
|
|
226
|
-
const entity = upsertEntity({ scope_id: chapterhouse.id, kind: "tool", name: "sqlite" });
|
|
227
|
-
const valid = recordObservation({ scope_id: chapterhouse.id, entity_id: entity.id, content: "Valid entity reference", source: "test" });
|
|
228
|
-
const orphan = recordObservation({ scope_id: chapterhouse.id, content: "Will become orphaned", source: "test" });
|
|
229
|
-
const otherScope = recordObservation({ scope_id: team.id, content: "Other scope orphan", source: "test" });
|
|
230
|
-
db.pragma("foreign_keys = OFF");
|
|
231
|
-
db.prepare(`UPDATE mem_observations SET entity_id = 987654 WHERE id IN (?, ?)`).run(orphan.id, otherScope.id);
|
|
232
|
-
db.pragma("foreign_keys = ON");
|
|
233
|
-
const summary = housekeepingModule.orphanCleanupPass(chapterhouse.id);
|
|
234
|
-
assert.equal(summary.pass, "orphanCleanupPass");
|
|
235
|
-
assert.equal(summary.examined, 1);
|
|
236
|
-
assert.equal(summary.modified, 1);
|
|
237
|
-
assert.equal(db.prepare(`SELECT entity_id FROM mem_observations WHERE id = ?`).get(orphan.id).entity_id, null);
|
|
238
|
-
assert.equal(db.prepare(`SELECT entity_id FROM mem_observations WHERE id = ?`).get(valid.id).entity_id, entity.id);
|
|
239
|
-
assert.equal(db.prepare(`SELECT entity_id FROM mem_observations WHERE id = ?`).get(otherScope.id).entity_id, 987654);
|
|
240
|
-
const second = housekeepingModule.orphanCleanupPass(chapterhouse.id);
|
|
241
|
-
assert.equal(second.modified, 0);
|
|
242
|
-
});
|
|
243
|
-
test("decayPass archives old low-confidence observations only in scope and compactInboxPass removes resolved inbox rows after retention", async () => {
|
|
244
|
-
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
245
|
-
const db = dbModule.getDb();
|
|
246
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
247
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
248
|
-
const chapterhouse = getScope("chapterhouse");
|
|
249
|
-
const team = createTestScope(memoryModule, "team");
|
|
250
|
-
assert.ok(chapterhouse);
|
|
251
|
-
const archiveMe = recordObservation({ scope_id: chapterhouse.id, content: "Old low confidence", source: "test", confidence: 0.2 });
|
|
252
|
-
const highConfidence = recordObservation({ scope_id: chapterhouse.id, content: "Old high confidence", source: "test", confidence: 0.9 });
|
|
253
|
-
const fresh = recordObservation({ scope_id: chapterhouse.id, content: "Fresh low confidence", source: "test", confidence: 0.2 });
|
|
254
|
-
const otherScope = recordObservation({ scope_id: team.id, content: "Other scope old low confidence", source: "test", confidence: 0.2 });
|
|
255
|
-
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-31 days') WHERE id IN (?, ?, ?)`).run(archiveMe.id, highConfidence.id, otherScope.id);
|
|
256
|
-
const decay = housekeepingModule.decayPass(chapterhouse.id);
|
|
257
|
-
assert.equal(decay.pass, "decayPass");
|
|
258
|
-
assert.equal(decay.examined, 1);
|
|
259
|
-
assert.equal(decay.modified, 1);
|
|
260
|
-
assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(archiveMe.id).archived_at);
|
|
261
|
-
assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(highConfidence.id).archived_at, null);
|
|
262
|
-
assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(fresh.id).archived_at, null);
|
|
263
|
-
assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(otherScope.id).archived_at, null);
|
|
264
|
-
assert.equal(housekeepingModule.decayPass(chapterhouse.id).modified, 0);
|
|
265
|
-
db.prepare(`
|
|
266
|
-
INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, status, created_at, resolved_at)
|
|
267
|
-
VALUES
|
|
268
|
-
(?, 'memory_proposal', '{}', 'test', 'accepted', datetime('now', '-20 days'), datetime('now', '-8 days')),
|
|
269
|
-
(?, 'memory_proposal', '{}', 'test', 'rejected', datetime('now', '-20 days'), datetime('now', '-6 days')),
|
|
270
|
-
(?, 'memory_proposal', '{}', 'test', 'pending', datetime('now', '-20 days'), NULL)
|
|
271
|
-
`).run(chapterhouse.id, chapterhouse.id, chapterhouse.id);
|
|
272
|
-
const compact = housekeepingModule.compactInboxPass();
|
|
273
|
-
assert.equal(compact.pass, "compactInboxPass");
|
|
274
|
-
assert.equal(compact.examined, 1);
|
|
275
|
-
assert.equal(compact.modified, 1);
|
|
276
|
-
assert.deepEqual(db.prepare(`SELECT status FROM mem_inbox ORDER BY id`).all(), [{ status: "rejected" }, { status: "pending" }]);
|
|
277
|
-
assert.equal(housekeepingModule.compactInboxPass().modified, 0);
|
|
278
|
-
});
|
|
279
|
-
test("runHousekeeping defaults to the active scope and can target all active scopes", async () => {
|
|
280
|
-
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
281
|
-
const db = dbModule.getDb();
|
|
282
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
283
|
-
const setActiveScope = getFunction(memoryModule, "setActiveScope");
|
|
284
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
285
|
-
const chapterhouse = getScope("chapterhouse");
|
|
286
|
-
const team = createTestScope(memoryModule, "team");
|
|
287
|
-
assert.ok(chapterhouse);
|
|
288
|
-
const chapterhouseOld = recordObservation({ scope_id: chapterhouse.id, content: "Chapterhouse old low", source: "test", confidence: 0.1 });
|
|
289
|
-
const teamOld = recordObservation({ scope_id: team.id, content: "Team old low", source: "test", confidence: 0.1 });
|
|
290
|
-
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-31 days') WHERE id IN (?, ?)`).run(chapterhouseOld.id, teamOld.id);
|
|
291
|
-
setActiveScope("chapterhouse");
|
|
292
|
-
const activeOnly = await housekeepingModule.runHousekeeping({ passes: ["decay"] });
|
|
293
|
-
assert.deepEqual(activeOnly.scopeIds, [chapterhouse.id]);
|
|
294
|
-
assert.equal(activeOnly.summaries.length, 1);
|
|
295
|
-
assert.equal(activeOnly.summaries[0]?.modified, 1);
|
|
296
|
-
assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(chapterhouseOld.id).archived_at);
|
|
297
|
-
assert.equal(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(teamOld.id).archived_at, null);
|
|
298
|
-
const allScopes = await housekeepingModule.runHousekeeping({ allScopes: true, passes: ["decay"] });
|
|
299
|
-
assert.ok(allScopes.scopeIds.includes(team.id));
|
|
300
|
-
assert.equal(allScopes.summaries.some((summary) => summary.modified === 1), true);
|
|
301
|
-
assert.ok(db.prepare(`SELECT archived_at FROM mem_observations WHERE id = ?`).get(teamOld.id).archived_at);
|
|
302
|
-
});
|
|
303
|
-
test("runHousekeeping starts all scoped passes before awaiting completion", async (t) => {
|
|
304
|
-
const releases = new Map();
|
|
305
|
-
const startedScopes = [];
|
|
306
|
-
const housekeepingModule = await loadMockedHousekeepingModule(t, {
|
|
307
|
-
scopes: [
|
|
308
|
-
{ id: 11, active: true },
|
|
309
|
-
{ id: 22, active: true },
|
|
310
|
-
],
|
|
311
|
-
tieringPass: async (scopeId) => {
|
|
312
|
-
startedScopes.push(scopeId);
|
|
313
|
-
await new Promise((resolve) => {
|
|
314
|
-
releases.set(scopeId, resolve);
|
|
315
|
-
});
|
|
316
|
-
return { pass: `tieringPass:${scopeId}`, examined: 1, modified: 1, errors: [] };
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
const pending = housekeepingModule.runHousekeeping({ allScopes: true, passes: ["tiering"] });
|
|
320
|
-
assert.equal(typeof pending.then, "function");
|
|
321
|
-
await Promise.resolve();
|
|
322
|
-
assert.deepEqual(startedScopes.sort((left, right) => left - right), [11, 22]);
|
|
323
|
-
releases.get(11)?.();
|
|
324
|
-
releases.get(22)?.();
|
|
325
|
-
const result = await pending;
|
|
326
|
-
assert.deepEqual(result.scopeIds, [11, 22]);
|
|
327
|
-
assert.deepEqual(result.summaries.map((summary) => summary.pass), ["tieringPass:11", "tieringPass:22"]);
|
|
328
|
-
});
|
|
329
|
-
test("runHousekeeping rejects overlapping runs that share an in-flight scope", async (t) => {
|
|
330
|
-
const releases = new Map();
|
|
331
|
-
const housekeepingModule = await loadMockedHousekeepingModule(t, {
|
|
332
|
-
scopes: [
|
|
333
|
-
{ id: 11, active: true },
|
|
334
|
-
{ id: 22, active: true },
|
|
335
|
-
],
|
|
336
|
-
tieringPass: async (scopeId) => {
|
|
337
|
-
await new Promise((resolve) => {
|
|
338
|
-
releases.set(scopeId, resolve);
|
|
339
|
-
});
|
|
340
|
-
return { pass: `tieringPass:${scopeId}`, examined: 1, modified: 1, errors: [] };
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
const firstRun = housekeepingModule.runHousekeeping({ scopeIds: [11, 22], passes: ["tiering"] });
|
|
344
|
-
await Promise.resolve();
|
|
345
|
-
assert.equal(housekeepingModule.isHousekeepingInFlight([22], ["tiering"]), true);
|
|
346
|
-
const secondRun = await housekeepingModule.runHousekeeping({ scopeIds: [22], passes: ["tiering"] });
|
|
347
|
-
assert.deepEqual(secondRun.scopeIds, [22]);
|
|
348
|
-
assert.match(secondRun.summaries[0]?.errors[0] ?? "", /already in flight/i);
|
|
349
|
-
releases.get(11)?.();
|
|
350
|
-
releases.get(22)?.();
|
|
351
|
-
await firstRun;
|
|
352
|
-
});
|
|
353
|
-
test("tieringPass promotes and demotes rows from lifecycle signals and is idempotent", async () => {
|
|
354
|
-
const { dbModule, memoryModule, housekeepingModule } = await loadModules();
|
|
355
|
-
const db = dbModule.getDb();
|
|
356
|
-
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
357
|
-
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
358
|
-
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
359
|
-
const team = createTestScope(memoryModule, "team");
|
|
360
|
-
const entity = upsertEntity({ scope_id: team.id, kind: "component", name: "memory", tier: "warm" });
|
|
361
|
-
const referencedObservation = recordObservation({
|
|
362
|
-
scope_id: team.id,
|
|
363
|
-
entity_id: entity.id,
|
|
364
|
-
content: "Referenced by a recent decision through its entity.",
|
|
365
|
-
source: "test",
|
|
366
|
-
tier: "warm",
|
|
367
|
-
});
|
|
368
|
-
const recentDecision = recordDecision({
|
|
369
|
-
scope_id: team.id,
|
|
370
|
-
entity_id: entity.id,
|
|
371
|
-
title: "Restate memory tiering decision",
|
|
372
|
-
rationale: "Recent entity-linked decisions keep related observations hot.",
|
|
373
|
-
decided_at: new Date().toISOString(),
|
|
374
|
-
tier: "warm",
|
|
375
|
-
});
|
|
376
|
-
const oldHot = recordObservation({
|
|
377
|
-
scope_id: team.id,
|
|
378
|
-
content: "Old hot row with no recall activity should cool down.",
|
|
379
|
-
source: "test",
|
|
380
|
-
tier: "hot",
|
|
381
|
-
});
|
|
382
|
-
const staleLowConfidence = recordObservation({
|
|
383
|
-
scope_id: team.id,
|
|
384
|
-
content: "Low confidence stale row should go cold.",
|
|
385
|
-
source: "test",
|
|
386
|
-
tier: "warm",
|
|
387
|
-
confidence: 0.2,
|
|
388
|
-
});
|
|
389
|
-
const archived = recordObservation({
|
|
390
|
-
scope_id: team.id,
|
|
391
|
-
content: "Archived row should always be cold.",
|
|
392
|
-
source: "test",
|
|
393
|
-
tier: "hot",
|
|
394
|
-
});
|
|
395
|
-
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-45 days') WHERE id = ?`).run(oldHot.id);
|
|
396
|
-
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-61 days') WHERE id = ?`).run(staleLowConfidence.id);
|
|
397
|
-
db.prepare(`UPDATE mem_observations SET archived_at = CURRENT_TIMESTAMP WHERE id = ?`).run(archived.id);
|
|
398
|
-
const summary = housekeepingModule.tieringPass(team.id);
|
|
399
|
-
assert.equal(summary.pass, "tieringPass");
|
|
400
|
-
assert.equal(summary.modified, 5);
|
|
401
|
-
assert.deepEqual(summary.errors, []);
|
|
402
|
-
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(referencedObservation.id).tier, "hot");
|
|
403
|
-
assert.equal(db.prepare(`SELECT tier FROM mem_decisions WHERE id = ?`).get(recentDecision.id).tier, "hot");
|
|
404
|
-
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(oldHot.id).tier, "warm");
|
|
405
|
-
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(staleLowConfidence.id).tier, "cold");
|
|
406
|
-
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(archived.id).tier, "cold");
|
|
407
|
-
const second = housekeepingModule.tieringPass(team.id);
|
|
408
|
-
assert.equal(second.modified, 0);
|
|
409
|
-
});
|
|
410
|
-
//# sourceMappingURL=housekeeping.test.js.map
|
package/dist/memory/inbox.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { getDb } from "../store/db.js";
|
|
2
|
-
import { getActiveScope } from "./active-scope.js";
|
|
3
|
-
import { getScope } from "./scopes.js";
|
|
4
|
-
function toInboxItem(row) {
|
|
5
|
-
return {
|
|
6
|
-
id: row.id,
|
|
7
|
-
scopeId: row.scope_id ?? undefined,
|
|
8
|
-
kind: row.kind,
|
|
9
|
-
payload: row.payload,
|
|
10
|
-
sourceAgent: row.source_agent,
|
|
11
|
-
sourceTaskId: row.source_task_id ?? undefined,
|
|
12
|
-
status: row.status,
|
|
13
|
-
createdAt: row.created_at,
|
|
14
|
-
resolvedAt: row.resolved_at ?? undefined,
|
|
15
|
-
resolutionReason: row.resolution_reason ?? undefined,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
export function resolveProposalScope(scopeSlug) {
|
|
19
|
-
if (scopeSlug) {
|
|
20
|
-
const explicit = getScope(scopeSlug);
|
|
21
|
-
if (!explicit) {
|
|
22
|
-
throw new Error(`Unknown memory scope '${scopeSlug}'.`);
|
|
23
|
-
}
|
|
24
|
-
return { scopeId: explicit.id, scopeSlug: explicit.slug };
|
|
25
|
-
}
|
|
26
|
-
const activeScope = getActiveScope();
|
|
27
|
-
if (!activeScope) {
|
|
28
|
-
throw new Error("No active memory scope is set. Use memory_set_scope or pass scope_slug explicitly.");
|
|
29
|
-
}
|
|
30
|
-
return { scopeId: activeScope.id, scopeSlug: activeScope.slug };
|
|
31
|
-
}
|
|
32
|
-
export function queueMemoryProposal(input) {
|
|
33
|
-
const { scopeId, scopeSlug } = resolveProposalScope(input.scopeSlug);
|
|
34
|
-
const envelope = {
|
|
35
|
-
kind: input.kind,
|
|
36
|
-
scope_slug: scopeSlug,
|
|
37
|
-
confidence: input.confidence ?? 0.5,
|
|
38
|
-
reason: input.reason,
|
|
39
|
-
payload: input.payload,
|
|
40
|
-
};
|
|
41
|
-
const result = getDb().prepare(`
|
|
42
|
-
INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status, created_at)
|
|
43
|
-
VALUES (?, 'memory_proposal', ?, ?, ?, 'pending', CURRENT_TIMESTAMP)
|
|
44
|
-
`).run(scopeId, JSON.stringify(envelope), input.sourceAgent, input.sourceTaskId ?? null);
|
|
45
|
-
return getInboxItem(Number(result.lastInsertRowid));
|
|
46
|
-
}
|
|
47
|
-
export function getInboxItem(id) {
|
|
48
|
-
const row = getDb().prepare(`
|
|
49
|
-
SELECT id, scope_id, kind, payload, source_agent, source_task_id, status, created_at, resolved_at, resolution_reason
|
|
50
|
-
FROM mem_inbox
|
|
51
|
-
WHERE id = ?
|
|
52
|
-
`).get(id);
|
|
53
|
-
return row ? toInboxItem(row) : undefined;
|
|
54
|
-
}
|
|
55
|
-
export function listPendingMemoryProposalsForTask(taskId) {
|
|
56
|
-
const rows = getDb().prepare(`
|
|
57
|
-
SELECT id, scope_id, kind, payload, source_agent, source_task_id, status, created_at, resolved_at, resolution_reason
|
|
58
|
-
FROM mem_inbox
|
|
59
|
-
WHERE source_task_id = ?
|
|
60
|
-
AND status = 'pending'
|
|
61
|
-
AND kind = 'memory_proposal'
|
|
62
|
-
ORDER BY id ASC
|
|
63
|
-
`).all(taskId);
|
|
64
|
-
return rows.map(toInboxItem);
|
|
65
|
-
}
|
|
66
|
-
export function listPendingInboxItems(input = {}) {
|
|
67
|
-
const rows = getDb().prepare(`
|
|
68
|
-
SELECT id, scope_id, kind, payload, source_agent, source_task_id, status, created_at, resolved_at, resolution_reason
|
|
69
|
-
FROM mem_inbox
|
|
70
|
-
WHERE status = 'pending'
|
|
71
|
-
ORDER BY id ASC
|
|
72
|
-
LIMIT ? OFFSET ?
|
|
73
|
-
`).all(input.limit ?? 100, input.offset ?? 0);
|
|
74
|
-
return rows.map(toInboxItem);
|
|
75
|
-
}
|
|
76
|
-
export function resolveInboxItem(id, status, reason) {
|
|
77
|
-
getDb().prepare(`
|
|
78
|
-
UPDATE mem_inbox
|
|
79
|
-
SET status = ?, resolution_reason = ?, resolved_at = CURRENT_TIMESTAMP
|
|
80
|
-
WHERE id = ?
|
|
81
|
-
`).run(status, reason, id);
|
|
82
|
-
}
|
|
83
|
-
//# sourceMappingURL=inbox.js.map
|
|
@@ -1,178 +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-inbox-${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 loadRealModules(cacheBust = `${Date.now()}-${Math.random()}`) {
|
|
15
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
16
|
-
const memoryModule = await import(new URL(`./index.js?case=${cacheBust}`, import.meta.url).href);
|
|
17
|
-
const inboxModule = await import(new URL(`./inbox.js?case=${cacheBust}`, import.meta.url).href);
|
|
18
|
-
return { dbModule, memoryModule, inboxModule };
|
|
19
|
-
}
|
|
20
|
-
async function loadMockedInboxModule(t, options) {
|
|
21
|
-
t.mock.module("../store/db.js", {
|
|
22
|
-
namedExports: {
|
|
23
|
-
getDb: () => ({ prepare: options.prepare }),
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
t.mock.module("./active-scope.js", {
|
|
27
|
-
namedExports: {
|
|
28
|
-
getActiveScope: () => options.activeScope ?? null,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
t.mock.module("./scopes.js", {
|
|
32
|
-
namedExports: {
|
|
33
|
-
getScope: (slug) => options.explicitScopes?.[slug],
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
return await import(new URL(`./inbox.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
37
|
-
}
|
|
38
|
-
function getFunction(module, name) {
|
|
39
|
-
const value = module[name];
|
|
40
|
-
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
function createTestScope(memoryModule, slug) {
|
|
44
|
-
const createScope = getFunction(memoryModule, "createScope");
|
|
45
|
-
return createScope({
|
|
46
|
-
slug,
|
|
47
|
-
title: slug.split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" "),
|
|
48
|
-
description: `${slug} test scope`,
|
|
49
|
-
keywords: [slug],
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
test.beforeEach(async () => {
|
|
53
|
-
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
54
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
55
|
-
dbModule.closeDb();
|
|
56
|
-
resetSandbox();
|
|
57
|
-
});
|
|
58
|
-
test.after(async () => {
|
|
59
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
60
|
-
dbModule.closeDb();
|
|
61
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
62
|
-
});
|
|
63
|
-
test("resolveProposalScope returns explicit scopes and falls back to the active scope for missing or empty values", async () => {
|
|
64
|
-
const { dbModule, memoryModule, inboxModule } = await loadRealModules("resolve-fallback");
|
|
65
|
-
dbModule.getDb();
|
|
66
|
-
const docs = createTestScope(memoryModule, "docs");
|
|
67
|
-
const setActiveScope = getFunction(memoryModule, "setActiveScope");
|
|
68
|
-
const active = createTestScope(memoryModule, "team-memory");
|
|
69
|
-
setActiveScope(active.slug);
|
|
70
|
-
assert.deepEqual(inboxModule.resolveProposalScope(docs.slug), { scopeId: docs.id, scopeSlug: docs.slug });
|
|
71
|
-
assert.deepEqual(inboxModule.resolveProposalScope(undefined), { scopeId: active.id, scopeSlug: active.slug });
|
|
72
|
-
assert.deepEqual(inboxModule.resolveProposalScope(""), { scopeId: active.id, scopeSlug: active.slug });
|
|
73
|
-
});
|
|
74
|
-
test("resolveProposalScope rejects invalid explicit scopes and missing default scopes", async () => {
|
|
75
|
-
const { dbModule, inboxModule } = await loadRealModules("resolve-errors");
|
|
76
|
-
dbModule.getDb();
|
|
77
|
-
assert.throws(() => inboxModule.resolveProposalScope("does-not-exist"), /Unknown memory scope 'does-not-exist'\./);
|
|
78
|
-
assert.throws(() => inboxModule.resolveProposalScope(), /No active memory scope is set\. Use memory_set_scope or pass scope_slug explicitly\./);
|
|
79
|
-
});
|
|
80
|
-
test("queueMemoryProposal enqueues explicit-scope proposals with the expected envelope defaults", async () => {
|
|
81
|
-
const { dbModule, memoryModule, inboxModule } = await loadRealModules("explicit-queue");
|
|
82
|
-
const db = dbModule.getDb();
|
|
83
|
-
const docs = createTestScope(memoryModule, "docs-queue");
|
|
84
|
-
const queued = inboxModule.queueMemoryProposal({
|
|
85
|
-
kind: "observation",
|
|
86
|
-
scopeSlug: docs.slug,
|
|
87
|
-
payload: {
|
|
88
|
-
content: "Inbox tests should verify the stored proposal envelope.",
|
|
89
|
-
source: "oracle",
|
|
90
|
-
},
|
|
91
|
-
sourceAgent: "oracle",
|
|
92
|
-
sourceTaskId: "task-inbox-explicit",
|
|
93
|
-
});
|
|
94
|
-
assert.equal(typeof queued.id, "number");
|
|
95
|
-
assert.equal(queued.scopeId, docs.id);
|
|
96
|
-
assert.equal(queued.kind, "memory_proposal");
|
|
97
|
-
assert.equal(queued.status, "pending");
|
|
98
|
-
assert.equal(queued.sourceAgent, "oracle");
|
|
99
|
-
assert.equal(queued.sourceTaskId, "task-inbox-explicit");
|
|
100
|
-
assert.match(queued.createdAt, /\d{4}-\d{2}-\d{2}/);
|
|
101
|
-
const persisted = db.prepare(`
|
|
102
|
-
SELECT payload
|
|
103
|
-
FROM mem_inbox
|
|
104
|
-
WHERE id = ?
|
|
105
|
-
`).get(queued.id);
|
|
106
|
-
assert.ok(persisted, "queueMemoryProposal should persist a mem_inbox row");
|
|
107
|
-
assert.deepEqual(JSON.parse(persisted.payload), {
|
|
108
|
-
kind: "observation",
|
|
109
|
-
scope_slug: docs.slug,
|
|
110
|
-
confidence: 0.5,
|
|
111
|
-
payload: {
|
|
112
|
-
content: "Inbox tests should verify the stored proposal envelope.",
|
|
113
|
-
source: "oracle",
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
assert.deepEqual(inboxModule.getInboxItem(queued.id), queued);
|
|
117
|
-
});
|
|
118
|
-
test("queueMemoryProposal falls back to the active scope, does not deduplicate duplicates, and pending lists exclude resolved items", async () => {
|
|
119
|
-
const { dbModule, memoryModule, inboxModule } = await loadRealModules("active-scope-queue");
|
|
120
|
-
dbModule.getDb();
|
|
121
|
-
const setActiveScope = getFunction(memoryModule, "setActiveScope");
|
|
122
|
-
setActiveScope("chapterhouse");
|
|
123
|
-
const first = inboxModule.queueMemoryProposal({
|
|
124
|
-
kind: "observation",
|
|
125
|
-
scopeSlug: "",
|
|
126
|
-
payload: { content: "Duplicate proposal payloads should still queue separately." },
|
|
127
|
-
confidence: 0.8,
|
|
128
|
-
reason: "First proposal",
|
|
129
|
-
sourceAgent: "oracle",
|
|
130
|
-
sourceTaskId: "task-inbox-duplicates",
|
|
131
|
-
});
|
|
132
|
-
const second = inboxModule.queueMemoryProposal({
|
|
133
|
-
kind: "observation",
|
|
134
|
-
payload: { content: "Duplicate proposal payloads should still queue separately." },
|
|
135
|
-
confidence: 0.8,
|
|
136
|
-
reason: "Second proposal",
|
|
137
|
-
sourceAgent: "oracle",
|
|
138
|
-
sourceTaskId: "task-inbox-duplicates",
|
|
139
|
-
});
|
|
140
|
-
assert.notEqual(first.id, second.id);
|
|
141
|
-
assert.equal(first.scopeId, second.scopeId);
|
|
142
|
-
assert.deepEqual(inboxModule.listPendingMemoryProposalsForTask("task-inbox-duplicates").map((item) => item.id), [first.id, second.id]);
|
|
143
|
-
inboxModule.resolveInboxItem(first.id, "accepted", "Durable enough to keep");
|
|
144
|
-
const resolved = inboxModule.getInboxItem(first.id);
|
|
145
|
-
assert.equal(resolved?.status, "accepted");
|
|
146
|
-
assert.equal(resolved?.resolutionReason, "Durable enough to keep");
|
|
147
|
-
assert.match(resolved?.resolvedAt ?? "", /\d{4}-\d{2}-\d{2}/);
|
|
148
|
-
assert.deepEqual(inboxModule.listPendingMemoryProposalsForTask("task-inbox-duplicates").map((item) => item.id), [second.id]);
|
|
149
|
-
});
|
|
150
|
-
test("queueMemoryProposal surfaces insert failures such as a full queue", async (t) => {
|
|
151
|
-
const fullError = new Error("database or disk is full");
|
|
152
|
-
const seenStatements = [];
|
|
153
|
-
const inboxModule = await loadMockedInboxModule(t, {
|
|
154
|
-
activeScope: { id: 99, slug: "chapterhouse" },
|
|
155
|
-
prepare: (sql) => {
|
|
156
|
-
seenStatements.push(sql);
|
|
157
|
-
if (sql.includes("INSERT INTO mem_inbox")) {
|
|
158
|
-
return {
|
|
159
|
-
run: () => {
|
|
160
|
-
throw fullError;
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
run: () => ({ changes: 0 }),
|
|
166
|
-
get: () => undefined,
|
|
167
|
-
all: () => [],
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
assert.throws(() => inboxModule.queueMemoryProposal({
|
|
172
|
-
kind: "observation",
|
|
173
|
-
payload: { content: "This proposal should fail before it is persisted." },
|
|
174
|
-
sourceAgent: "oracle",
|
|
175
|
-
}), /database or disk is full/);
|
|
176
|
-
assert.equal(seenStatements.some((sql) => sql.includes("INSERT INTO mem_inbox")), true);
|
|
177
|
-
});
|
|
178
|
-
//# sourceMappingURL=inbox.test.js.map
|