chapterhouse 0.4.0 → 0.4.2
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/server.js +14 -8
- package/dist/api/server.test.js +30 -0
- package/dist/copilot/agents.js +5 -2
- package/dist/copilot/agents.test.js +34 -0
- package/dist/copilot/orchestrator.js +8 -11
- package/dist/copilot/orchestrator.test.js +12 -4
- package/dist/copilot/prompt-date.js +8 -0
- package/dist/copilot/system-message.js +4 -0
- package/dist/copilot/system-message.test.js +34 -0
- package/dist/copilot/tools.agent.test.js +1 -0
- package/dist/copilot/tools.js +2 -1
- package/dist/copilot/tools.memory.test.js +49 -0
- package/dist/copilot/turn-event-log.js +35 -15
- package/dist/copilot/turn-event-log.test.js +31 -0
- package/dist/memory/eot.test.js +3 -3
- package/dist/memory/housekeeping.test.js +26 -26
- package/dist/memory/recall.js +15 -2
- package/dist/memory/recall.test.js +42 -0
- package/dist/store/db.js +336 -9
- package/dist/store/db.test.js +393 -7
- package/package.json +1 -1
- package/web/dist/assets/{index-DmYLALt0.js → index-B_cCSHan.js} +52 -52
- package/web/dist/assets/index-B_cCSHan.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-DmYLALt0.js.map +0 -1
|
@@ -45,30 +45,30 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
|
|
|
45
45
|
const team = getScope("team");
|
|
46
46
|
assert.ok(chapterhouse && team);
|
|
47
47
|
const first = recordObservation({
|
|
48
|
-
scope_id:
|
|
48
|
+
scope_id: team.id,
|
|
49
49
|
content: "The worker event stream uses server sent events for live task output.",
|
|
50
50
|
source: "test",
|
|
51
51
|
confidence: 0.4,
|
|
52
52
|
});
|
|
53
53
|
const keeper = recordObservation({
|
|
54
|
-
scope_id:
|
|
54
|
+
scope_id: team.id,
|
|
55
55
|
content: "Worker event streams use server sent events for live task output.",
|
|
56
56
|
source: "test",
|
|
57
57
|
confidence: 0.9,
|
|
58
58
|
});
|
|
59
59
|
const third = recordObservation({
|
|
60
|
-
scope_id:
|
|
60
|
+
scope_id: team.id,
|
|
61
61
|
content: "The worker event stream uses server sent events for live task output today.",
|
|
62
62
|
source: "test",
|
|
63
63
|
confidence: 0.9,
|
|
64
64
|
});
|
|
65
65
|
const otherScope = recordObservation({
|
|
66
|
-
scope_id:
|
|
66
|
+
scope_id: chapterhouse.id,
|
|
67
67
|
content: "Worker event streams use server sent events for live task output.",
|
|
68
68
|
source: "test",
|
|
69
69
|
confidence: 0.1,
|
|
70
70
|
});
|
|
71
|
-
const summary = housekeepingModule.dedupObservationsPass(
|
|
71
|
+
const summary = housekeepingModule.dedupObservationsPass(team.id);
|
|
72
72
|
assert.equal(summary.pass, "dedupObservationsPass");
|
|
73
73
|
assert.equal(summary.examined, 3);
|
|
74
74
|
assert.equal(summary.modified, 2);
|
|
@@ -79,7 +79,7 @@ test("dedupObservationsPass supersedes similar observations in scope determinist
|
|
|
79
79
|
{ id: third.id, superseded_by: keeper.id },
|
|
80
80
|
]);
|
|
81
81
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_observations WHERE id = ?`).get(otherScope.id).superseded_by, null);
|
|
82
|
-
const second = housekeepingModule.dedupObservationsPass(
|
|
82
|
+
const second = housekeepingModule.dedupObservationsPass(team.id);
|
|
83
83
|
assert.equal(second.modified, 0);
|
|
84
84
|
});
|
|
85
85
|
test("dedupDecisionsPass supersedes similar active decisions within scope and keeps the latest decision", async () => {
|
|
@@ -88,39 +88,39 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
|
|
|
88
88
|
const getScope = getFunction(memoryModule, "getScope");
|
|
89
89
|
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
90
90
|
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
91
|
-
const chapterhouse = getScope("chapterhouse");
|
|
92
91
|
const team = getScope("team");
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
92
|
+
const infra = getScope("infra");
|
|
93
|
+
assert.ok(team && infra);
|
|
94
|
+
const api = upsertEntity({ scope_id: team.id, kind: "component", name: "api" });
|
|
95
|
+
const web = upsertEntity({ scope_id: team.id, kind: "component", name: "web" });
|
|
96
96
|
const oldDecision = recordDecision({
|
|
97
|
-
scope_id:
|
|
97
|
+
scope_id: team.id,
|
|
98
98
|
entity_id: api.id,
|
|
99
99
|
title: "Use SQLite FTS5 for memory recall",
|
|
100
100
|
rationale: "Initial choice.",
|
|
101
101
|
decided_at: "2026-05-11",
|
|
102
102
|
});
|
|
103
103
|
const keeper = recordDecision({
|
|
104
|
-
scope_id:
|
|
104
|
+
scope_id: team.id,
|
|
105
105
|
entity_id: api.id,
|
|
106
106
|
title: "Use SQLite FTS5 for scoped memory recall",
|
|
107
107
|
rationale: "Latest choice.",
|
|
108
108
|
decided_at: "2026-05-13",
|
|
109
109
|
});
|
|
110
110
|
const otherEntity = recordDecision({
|
|
111
|
-
scope_id:
|
|
111
|
+
scope_id: team.id,
|
|
112
112
|
entity_id: web.id,
|
|
113
113
|
title: "Use SQLite FTS5 for memory recall",
|
|
114
114
|
rationale: "Same title, different entity context, but newer scope-level decision.",
|
|
115
115
|
decided_at: "2026-05-14",
|
|
116
116
|
});
|
|
117
117
|
const otherScope = recordDecision({
|
|
118
|
-
scope_id:
|
|
118
|
+
scope_id: infra.id,
|
|
119
119
|
title: "Use SQLite FTS5 for memory recall",
|
|
120
120
|
rationale: "Same title, different scope.",
|
|
121
121
|
decided_at: "2026-05-14",
|
|
122
122
|
});
|
|
123
|
-
const summary = housekeepingModule.dedupDecisionsPass(
|
|
123
|
+
const summary = housekeepingModule.dedupDecisionsPass(team.id);
|
|
124
124
|
assert.equal(summary.pass, "dedupDecisionsPass");
|
|
125
125
|
assert.equal(summary.examined, 3);
|
|
126
126
|
assert.equal(summary.modified, 2);
|
|
@@ -129,7 +129,7 @@ test("dedupDecisionsPass supersedes similar active decisions within scope and ke
|
|
|
129
129
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(keeper.id).superseded_by, otherEntity.id);
|
|
130
130
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherEntity.id).superseded_by, null);
|
|
131
131
|
assert.equal(db.prepare(`SELECT superseded_by FROM mem_decisions WHERE id = ?`).get(otherScope.id).superseded_by, null);
|
|
132
|
-
const second = housekeepingModule.dedupDecisionsPass(
|
|
132
|
+
const second = housekeepingModule.dedupDecisionsPass(team.id);
|
|
133
133
|
assert.equal(second.modified, 0);
|
|
134
134
|
});
|
|
135
135
|
test("orphanCleanupPass clears missing observation entity references without touching valid or out-of-scope rows", async () => {
|
|
@@ -225,18 +225,18 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
225
225
|
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
226
226
|
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
227
227
|
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
228
|
-
const
|
|
229
|
-
assert.ok(
|
|
230
|
-
const entity = upsertEntity({ scope_id:
|
|
228
|
+
const team = getScope("team");
|
|
229
|
+
assert.ok(team);
|
|
230
|
+
const entity = upsertEntity({ scope_id: team.id, kind: "component", name: "memory", tier: "warm" });
|
|
231
231
|
const referencedObservation = recordObservation({
|
|
232
|
-
scope_id:
|
|
232
|
+
scope_id: team.id,
|
|
233
233
|
entity_id: entity.id,
|
|
234
234
|
content: "Referenced by a recent decision through its entity.",
|
|
235
235
|
source: "test",
|
|
236
236
|
tier: "warm",
|
|
237
237
|
});
|
|
238
238
|
const recentDecision = recordDecision({
|
|
239
|
-
scope_id:
|
|
239
|
+
scope_id: team.id,
|
|
240
240
|
entity_id: entity.id,
|
|
241
241
|
title: "Restate memory tiering decision",
|
|
242
242
|
rationale: "Recent entity-linked decisions keep related observations hot.",
|
|
@@ -244,20 +244,20 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
244
244
|
tier: "warm",
|
|
245
245
|
});
|
|
246
246
|
const oldHot = recordObservation({
|
|
247
|
-
scope_id:
|
|
247
|
+
scope_id: team.id,
|
|
248
248
|
content: "Old hot row with no recall activity should cool down.",
|
|
249
249
|
source: "test",
|
|
250
250
|
tier: "hot",
|
|
251
251
|
});
|
|
252
252
|
const staleLowConfidence = recordObservation({
|
|
253
|
-
scope_id:
|
|
253
|
+
scope_id: team.id,
|
|
254
254
|
content: "Low confidence stale row should go cold.",
|
|
255
255
|
source: "test",
|
|
256
256
|
tier: "warm",
|
|
257
257
|
confidence: 0.2,
|
|
258
258
|
});
|
|
259
259
|
const archived = recordObservation({
|
|
260
|
-
scope_id:
|
|
260
|
+
scope_id: team.id,
|
|
261
261
|
content: "Archived row should always be cold.",
|
|
262
262
|
source: "test",
|
|
263
263
|
tier: "hot",
|
|
@@ -265,7 +265,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
265
265
|
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-45 days') WHERE id = ?`).run(oldHot.id);
|
|
266
266
|
db.prepare(`UPDATE mem_observations SET created_at = datetime('now', '-61 days') WHERE id = ?`).run(staleLowConfidence.id);
|
|
267
267
|
db.prepare(`UPDATE mem_observations SET archived_at = CURRENT_TIMESTAMP WHERE id = ?`).run(archived.id);
|
|
268
|
-
const summary = housekeepingModule.tieringPass(
|
|
268
|
+
const summary = housekeepingModule.tieringPass(team.id);
|
|
269
269
|
assert.equal(summary.pass, "tieringPass");
|
|
270
270
|
assert.equal(summary.modified, 5);
|
|
271
271
|
assert.deepEqual(summary.errors, []);
|
|
@@ -274,7 +274,7 @@ test("tieringPass promotes and demotes rows from lifecycle signals and is idempo
|
|
|
274
274
|
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(oldHot.id).tier, "warm");
|
|
275
275
|
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(staleLowConfidence.id).tier, "cold");
|
|
276
276
|
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(archived.id).tier, "cold");
|
|
277
|
-
const second = housekeepingModule.tieringPass(
|
|
277
|
+
const second = housekeepingModule.tieringPass(team.id);
|
|
278
278
|
assert.equal(second.modified, 0);
|
|
279
279
|
});
|
|
280
280
|
//# sourceMappingURL=housekeeping.test.js.map
|
package/dist/memory/recall.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
2
|
import { getDb, isFts5Available } from "../store/db.js";
|
|
3
3
|
import { getActiveScope } from "./active-scope.js";
|
|
4
|
+
function quoteFts5QueryTerms(query) {
|
|
5
|
+
return query
|
|
6
|
+
.trim()
|
|
7
|
+
.split(/\s+/)
|
|
8
|
+
.filter((term) => term.length > 0)
|
|
9
|
+
.map((term) => {
|
|
10
|
+
const unquoted = term.replace(/^["']|["']$/g, "");
|
|
11
|
+
return `"${unquoted.replace(/"/g, "\"\"")}"`;
|
|
12
|
+
})
|
|
13
|
+
.join(" ");
|
|
14
|
+
}
|
|
4
15
|
function recallHotTier(scopeId, options = {}) {
|
|
5
16
|
const rows = getDb().prepare(`
|
|
6
17
|
SELECT 'observation' AS kind, id, content
|
|
@@ -25,6 +36,7 @@ function recallHotTier(scopeId, options = {}) {
|
|
|
25
36
|
}
|
|
26
37
|
function recallObservationHits(query, scopeId, options = {}) {
|
|
27
38
|
if (isFts5Available()) {
|
|
39
|
+
const ftsQuery = quoteFts5QueryTerms(query);
|
|
28
40
|
const rows = getDb().prepare(`
|
|
29
41
|
SELECT
|
|
30
42
|
o.id,
|
|
@@ -43,7 +55,7 @@ function recallObservationHits(query, scopeId, options = {}) {
|
|
|
43
55
|
AND (? = 1 OR o.superseded_by IS NULL)
|
|
44
56
|
AND (? = 1 OR o.archived_at IS NULL)
|
|
45
57
|
ORDER BY score DESC, o.id DESC
|
|
46
|
-
`).all(config.memoryHotRecallBoost,
|
|
58
|
+
`).all(config.memoryHotRecallBoost, ftsQuery, scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0);
|
|
47
59
|
return rows.map((row) => ({
|
|
48
60
|
kind: "observation",
|
|
49
61
|
id: row.id,
|
|
@@ -78,6 +90,7 @@ function recallObservationHits(query, scopeId, options = {}) {
|
|
|
78
90
|
}
|
|
79
91
|
function recallDecisionHits(query, scopeId, options = {}) {
|
|
80
92
|
if (isFts5Available()) {
|
|
93
|
+
const ftsQuery = quoteFts5QueryTerms(query);
|
|
81
94
|
const rows = getDb().prepare(`
|
|
82
95
|
SELECT
|
|
83
96
|
d.id,
|
|
@@ -99,7 +112,7 @@ function recallDecisionHits(query, scopeId, options = {}) {
|
|
|
99
112
|
AND (? = 1 OR d.superseded_by IS NULL)
|
|
100
113
|
AND (? = 1 OR d.archived_at IS NULL)
|
|
101
114
|
ORDER BY score DESC, d.id DESC
|
|
102
|
-
`).all(config.memoryHotRecallBoost,
|
|
115
|
+
`).all(config.memoryHotRecallBoost, ftsQuery, scopeId ?? null, scopeId ?? null, options.includeCold ? 1 : 0, options.includeSuperseded ? 1 : 0, options.includeArchived ? 1 : 0);
|
|
103
116
|
return rows.map((row) => ({
|
|
104
117
|
kind: "decision",
|
|
105
118
|
id: row.id,
|
|
@@ -193,4 +193,46 @@ test("recall boosts hot rows and excludes cold rows unless includeCold is set",
|
|
|
193
193
|
const withCold = recall({ query: "tierboost sentinel", scope_id: chapterhouse.id, kinds: ["observation"], limit: 10, includeCold: true });
|
|
194
194
|
assert.equal(withCold.hits.some((hit) => hit.id === cold.id), true);
|
|
195
195
|
});
|
|
196
|
+
test("recall matches multi-word observation queries with exact tokens", async () => {
|
|
197
|
+
const { dbModule, memoryModule } = await loadModules();
|
|
198
|
+
dbModule.getDb();
|
|
199
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
200
|
+
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
201
|
+
const recall = getFunction(memoryModule, "recall");
|
|
202
|
+
const chapterhouse = getScope("chapterhouse");
|
|
203
|
+
assert.ok(chapterhouse);
|
|
204
|
+
const observation = recordObservation({
|
|
205
|
+
scope_id: chapterhouse.id,
|
|
206
|
+
content: "Chapterhouse memory P1 shipped on 2026-05-13",
|
|
207
|
+
source: "test",
|
|
208
|
+
});
|
|
209
|
+
const result = recall({
|
|
210
|
+
query: "memory P1 shipped",
|
|
211
|
+
scope_id: chapterhouse.id,
|
|
212
|
+
kinds: ["observation"],
|
|
213
|
+
limit: 10,
|
|
214
|
+
});
|
|
215
|
+
assert.equal(result.hits.some((hit) => hit.id === observation.id), true);
|
|
216
|
+
});
|
|
217
|
+
test("recall treats hyphenated observation query terms literally", async () => {
|
|
218
|
+
const { dbModule, memoryModule } = await loadModules();
|
|
219
|
+
dbModule.getDb();
|
|
220
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
221
|
+
const recordObservation = getFunction(memoryModule, "recordObservation");
|
|
222
|
+
const recall = getFunction(memoryModule, "recall");
|
|
223
|
+
const chapterhouse = getScope("chapterhouse");
|
|
224
|
+
assert.ok(chapterhouse);
|
|
225
|
+
const observation = recordObservation({
|
|
226
|
+
scope_id: chapterhouse.id,
|
|
227
|
+
content: "Chapterhouse agent-memory recall shipped safely",
|
|
228
|
+
source: "test",
|
|
229
|
+
});
|
|
230
|
+
const result = recall({
|
|
231
|
+
query: "agent-memory",
|
|
232
|
+
scope_id: chapterhouse.id,
|
|
233
|
+
kinds: ["observation"],
|
|
234
|
+
limit: 10,
|
|
235
|
+
});
|
|
236
|
+
assert.equal(result.hits.some((hit) => hit.id === observation.id), true);
|
|
237
|
+
});
|
|
196
238
|
//# sourceMappingURL=recall.test.js.map
|