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,274 +0,0 @@
1
- import { config } from "../config.js";
2
- import { runOneShotPrompt } from "../copilot/oneshot.js";
3
- import { childLogger } from "../util/logger.js";
4
- import { recordDecision } from "./decisions.js";
5
- import { listEntities } from "./entities.js";
6
- import { recordObservation, listObservations } from "./observations.js";
7
- import { getScope } from "./scopes.js";
8
- import { releaseScopeWriteLocks, tryAcquireScopeWriteLocks } from "./scope-lock.js";
9
- import { listDecisions } from "./decisions.js";
10
- import { buildCheckpointSystemPrompt, buildCheckpointUserPrompt, } from "./checkpoint-prompt.js";
11
- const log = childLogger("memory.checkpoint");
12
- const MAX_WRITES_PER_CHECKPOINT = 5;
13
- const MIN_CONFIDENCE = 0.5;
14
- const RECENT_MEMORY_LIMIT = 50;
15
- const inFlightSessions = new Set();
16
- function getInFlightKey(sessionKey) {
17
- return sessionKey?.trim() || "__global__";
18
- }
19
- function normalizeText(value) {
20
- return value
21
- .toLowerCase()
22
- .replace(/[^a-z0-9\s]/g, " ")
23
- .replace(/\s+/g, " ")
24
- .trim();
25
- }
26
- function calculateSimilarity(left, right) {
27
- const normalizedLeft = normalizeText(left);
28
- const normalizedRight = normalizeText(right);
29
- if (!normalizedLeft || !normalizedRight) {
30
- return 0;
31
- }
32
- if (normalizedLeft === normalizedRight) {
33
- return 1;
34
- }
35
- if (normalizedLeft.includes(normalizedRight) || normalizedRight.includes(normalizedLeft)) {
36
- return Math.min(normalizedLeft.length, normalizedRight.length) / Math.max(normalizedLeft.length, normalizedRight.length);
37
- }
38
- const leftTokens = new Set(normalizedLeft.split(" ").filter(Boolean));
39
- const rightTokens = new Set(normalizedRight.split(" ").filter(Boolean));
40
- if (leftTokens.size === 0 || rightTokens.size === 0) {
41
- return 0;
42
- }
43
- let overlap = 0;
44
- for (const token of leftTokens) {
45
- if (rightTokens.has(token)) {
46
- overlap++;
47
- }
48
- }
49
- return overlap / new Set([...leftTokens, ...rightTokens]).size;
50
- }
51
- function parseProposals(raw) {
52
- try {
53
- const parsed = JSON.parse(raw);
54
- if (!Array.isArray(parsed.proposals)) {
55
- return [];
56
- }
57
- return parsed.proposals.flatMap((proposal) => {
58
- if (!proposal || typeof proposal !== "object") {
59
- return [];
60
- }
61
- const candidate = proposal;
62
- if ((candidate.kind !== "observation" && candidate.kind !== "decision")
63
- || typeof candidate.content !== "string"
64
- || typeof candidate.confidence !== "number") {
65
- return [];
66
- }
67
- return [{
68
- kind: candidate.kind,
69
- title: typeof candidate.title === "string" ? candidate.title.trim() : undefined,
70
- content: candidate.content.trim(),
71
- scope: typeof candidate.scope === "string" ? candidate.scope.trim() : undefined,
72
- confidence: candidate.confidence,
73
- decided_at: typeof candidate.decided_at === "string" ? candidate.decided_at.trim() : undefined,
74
- }];
75
- });
76
- }
77
- catch (err) {
78
- log.warn({ err }, "malformed checkpoint proposals response");
79
- return [];
80
- }
81
- }
82
- function resolveProposalScope(proposal, activeScope) {
83
- if (!proposal.scope || proposal.scope === activeScope.slug) {
84
- return activeScope;
85
- }
86
- return getScope(proposal.scope) ?? null;
87
- }
88
- function isDuplicateObservation(content, scopeId, batchContents) {
89
- const existing = listObservations({ scope_id: scopeId, limit: RECENT_MEMORY_LIMIT });
90
- const combined = [...existing.map((observation) => observation.content), ...batchContents];
91
- return combined.some((candidate) => calculateSimilarity(candidate, content) >= config.memoryCheckpointDuplicateThreshold);
92
- }
93
- function isDuplicateDecision(proposal, scopeId) {
94
- const existing = listDecisions({ scope_id: scopeId, limit: RECENT_MEMORY_LIMIT });
95
- return existing.some((decision) => {
96
- const titleSimilarity = proposal.title ? calculateSimilarity(decision.title, proposal.title) : 0;
97
- const rationaleSimilarity = calculateSimilarity(decision.rationale, proposal.content);
98
- return titleSimilarity >= config.memoryCheckpointDuplicateThreshold
99
- || rationaleSimilarity >= config.memoryCheckpointDuplicateThreshold;
100
- });
101
- }
102
- export class CheckpointTracker {
103
- turnCountSinceLastFire = 0;
104
- cadence;
105
- enabled;
106
- constructor(options = {}) {
107
- this.cadence = options.turns ?? config.memoryCheckpointTurns;
108
- this.enabled = options.enabled ?? config.memoryCheckpointEnabled;
109
- }
110
- tickOrchestratorTurn() {
111
- this.turnCountSinceLastFire++;
112
- }
113
- shouldFire() {
114
- return this.enabled && this.turnCountSinceLastFire >= this.cadence;
115
- }
116
- turnsSinceLastFire() {
117
- return this.turnCountSinceLastFire;
118
- }
119
- markFired() {
120
- this.turnCountSinceLastFire = 0;
121
- }
122
- markScopeChangeFire() {
123
- this.turnCountSinceLastFire = 0;
124
- }
125
- reset() {
126
- this.turnCountSinceLastFire = 0;
127
- }
128
- }
129
- export function isCheckpointInFlight(sessionKey) {
130
- if (sessionKey) {
131
- return inFlightSessions.has(getInFlightKey(sessionKey));
132
- }
133
- return inFlightSessions.size > 0;
134
- }
135
- export async function runCheckpointExtraction(input) {
136
- const model = input.model ?? config.copilotModel;
137
- const inFlightKey = getInFlightKey(input.sessionKey);
138
- const errors = [];
139
- const trigger = input.trigger ?? "cadence";
140
- if (!input.activeScope) {
141
- log.info({ sessionKey: input.sessionKey ?? "default", reason: "no_scope" }, "memory.checkpoint.skip");
142
- return { written: 0, skipped: 0, errors };
143
- }
144
- if (isCheckpointInFlight(input.sessionKey)) {
145
- log.info({ sessionKey: input.sessionKey ?? "default" }, "memory.checkpoint.in_flight_skip");
146
- return { written: 0, skipped: 0, errors };
147
- }
148
- inFlightSessions.add(inFlightKey);
149
- try {
150
- log.info({
151
- turnCount: input.turns.length,
152
- scope: input.activeScope.slug,
153
- model,
154
- sessionKey: input.sessionKey ?? "default",
155
- trigger,
156
- }, "memory.checkpoint.fire");
157
- const entities = listEntities({ scope_id: input.activeScope.id, limit: 8 });
158
- const decisions = listDecisions({ scope_id: input.activeScope.id, limit: 8 });
159
- const system = buildCheckpointSystemPrompt();
160
- const user = buildCheckpointUserPrompt({
161
- turns: input.turns,
162
- activeScope: input.activeScope,
163
- entities,
164
- decisions,
165
- scopeChangeContext: input.scopeChangeContext ?? null,
166
- });
167
- const callLLM = input.callLLM
168
- ?? (async ({ system: systemPrompt, user: userPrompt, model: chosenModel }) => {
169
- const result = await runOneShotPrompt({
170
- client: input.copilotClient,
171
- model: chosenModel,
172
- system: systemPrompt,
173
- user: userPrompt,
174
- expectJson: true,
175
- });
176
- return result.content;
177
- });
178
- const rawResponse = await callLLM({ system, user, model });
179
- const proposals = parseProposals(rawResponse)
180
- .filter((proposal) => proposal.content.length > 0)
181
- .sort((left, right) => right.confidence - left.confidence);
182
- log.info({
183
- count: proposals.length,
184
- scope: input.activeScope.slug,
185
- sessionKey: input.sessionKey ?? "default",
186
- }, "memory.checkpoint.proposal_count");
187
- const resolvedProposals = proposals.map((proposal) => ({
188
- proposal,
189
- scope: resolveProposalScope(proposal, input.activeScope),
190
- }));
191
- const lockedScopeIds = [...new Set(resolvedProposals.flatMap(({ scope }) => (scope ? [scope.id] : [])))];
192
- if (lockedScopeIds.length > 0 && !tryAcquireScopeWriteLocks(lockedScopeIds)) {
193
- log.info({ sessionKey: input.sessionKey ?? "default", scope: input.activeScope.slug }, "memory.checkpoint.scope_in_flight_skip");
194
- return { written: 0, skipped: 0, errors };
195
- }
196
- let written = 0;
197
- let skipped = 0;
198
- const pendingObservationContents = [];
199
- try {
200
- for (const { proposal, scope } of resolvedProposals) {
201
- if (proposal.confidence < MIN_CONFIDENCE) {
202
- skipped++;
203
- log.info({ reason: "low_confidence", confidence: proposal.confidence, scope: input.activeScope.slug }, "memory.checkpoint.skip");
204
- continue;
205
- }
206
- if (written >= MAX_WRITES_PER_CHECKPOINT) {
207
- skipped++;
208
- log.info({ reason: "cap_exceeded", scope: input.activeScope.slug }, "memory.checkpoint.skip");
209
- continue;
210
- }
211
- if (!scope) {
212
- skipped++;
213
- log.info({ reason: "no_scope", scope: proposal.scope ?? input.activeScope.slug }, "memory.checkpoint.skip");
214
- continue;
215
- }
216
- if (proposal.kind === "observation" && isDuplicateObservation(proposal.content, scope.id, pendingObservationContents)) {
217
- skipped++;
218
- log.info({ reason: "duplicate", scope_id: scope.id }, "memory.checkpoint.skip");
219
- continue;
220
- }
221
- if (proposal.kind === "decision") {
222
- if (!proposal.title) {
223
- skipped++;
224
- log.info({ reason: "missing_title", scope_id: scope.id }, "memory.checkpoint.skip");
225
- continue;
226
- }
227
- if (isDuplicateDecision(proposal, scope.id)) {
228
- skipped++;
229
- log.info({ reason: "duplicate", scope_id: scope.id }, "memory.checkpoint.skip");
230
- continue;
231
- }
232
- const decision = recordDecision({
233
- scope_id: scope.id,
234
- title: proposal.title,
235
- rationale: proposal.content,
236
- decided_at: proposal.decided_at || new Date().toISOString().slice(0, 10),
237
- tier: "warm",
238
- });
239
- written++;
240
- log.info({ kind: "decision", scope_id: scope.id, id: decision.id }, "memory.checkpoint.write");
241
- continue;
242
- }
243
- const observation = recordObservation({
244
- scope_id: scope.id,
245
- content: proposal.content,
246
- source: "checkpoint:orchestrator",
247
- tier: "warm",
248
- confidence: proposal.confidence,
249
- });
250
- pendingObservationContents.push(proposal.content);
251
- written++;
252
- log.info({ kind: "observation", scope_id: scope.id, id: observation.id }, "memory.checkpoint.write");
253
- }
254
- return { written, skipped, errors };
255
- }
256
- finally {
257
- releaseScopeWriteLocks(lockedScopeIds);
258
- }
259
- }
260
- catch (error) {
261
- const message = error instanceof Error ? error.message : String(error);
262
- errors.push(message);
263
- log.error({
264
- err: error,
265
- sessionKey: input.sessionKey ?? "default",
266
- scope: input.activeScope.slug,
267
- }, "memory.checkpoint.error");
268
- return { written: 0, skipped: 0, errors };
269
- }
270
- finally {
271
- inFlightSessions.delete(inFlightKey);
272
- }
273
- }
274
- //# sourceMappingURL=checkpoint.js.map
@@ -1,275 +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-checkpoint-${process.pid}`);
7
- const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
8
- process.env.CHAPTERHOUSE_HOME = sandboxRoot;
9
- function resetSandbox() {
10
- mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
11
- rmSync(sandboxRoot, { recursive: true, force: true });
12
- mkdirSync(chapterhouseHome, { recursive: true });
13
- }
14
- async function loadModules(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 checkpointModule = await import(new URL(`./checkpoint.js?case=${cacheBust}`, import.meta.url).href);
18
- const checkpointPromptModule = await import(new URL(`./checkpoint-prompt.js?case=${cacheBust}`, import.meta.url).href);
19
- return { dbModule, memoryModule, checkpointModule, checkpointPromptModule };
20
- }
21
- async function loadModulesWithWarnSpy(t, cacheBust) {
22
- const warnings = [];
23
- t.mock.module("../util/logger.js", {
24
- namedExports: {
25
- childLogger: () => ({
26
- info: () => { },
27
- warn: (...args) => {
28
- warnings.push(args);
29
- },
30
- error: () => { },
31
- }),
32
- },
33
- });
34
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
35
- const memoryModule = await import(new URL(`./index.js?case=${cacheBust}`, import.meta.url).href);
36
- const checkpointModule = await import(new URL(`./checkpoint.js?case=${cacheBust}`, import.meta.url).href);
37
- return { dbModule, memoryModule, checkpointModule, warnings };
38
- }
39
- function getFunction(module, name) {
40
- const value = module[name];
41
- assert.equal(typeof value, "function", `expected ${name} to be exported`);
42
- return value;
43
- }
44
- function getConstructor(module, name) {
45
- const value = module[name];
46
- assert.equal(typeof value, "function", `expected ${name} to be exported`);
47
- return value;
48
- }
49
- test.beforeEach(async () => {
50
- delete process.env.CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED;
51
- delete process.env.CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE;
52
- delete process.env.CHAPTERHOUSE_MEMORY_CHECKPOINT_MIN_TURNS_FOR_SCOPE_FIRE;
53
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
54
- dbModule.closeDb();
55
- resetSandbox();
56
- });
57
- test.after(async () => {
58
- delete process.env.CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED;
59
- delete process.env.CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE;
60
- delete process.env.CHAPTERHOUSE_MEMORY_CHECKPOINT_MIN_TURNS_FOR_SCOPE_FIRE;
61
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
62
- dbModule.closeDb();
63
- rmSync(sandboxRoot, { recursive: true, force: true });
64
- });
65
- test("CheckpointTracker increments on orchestrator turns, fires at cadence, and resets after markFired", async () => {
66
- const { checkpointModule } = await loadModules();
67
- const CheckpointTracker = getConstructor(checkpointModule, "CheckpointTracker");
68
- const tracker = new CheckpointTracker({ turns: 3, enabled: true });
69
- assert.equal(tracker.turnsSinceLastFire(), 0);
70
- assert.equal(tracker.shouldFire(), false);
71
- tracker.tickOrchestratorTurn();
72
- assert.equal(tracker.turnsSinceLastFire(), 1);
73
- assert.equal(tracker.shouldFire(), false);
74
- tracker.tickOrchestratorTurn();
75
- assert.equal(tracker.turnsSinceLastFire(), 2);
76
- assert.equal(tracker.shouldFire(), false);
77
- tracker.tickOrchestratorTurn();
78
- assert.equal(tracker.turnsSinceLastFire(), 3);
79
- assert.equal(tracker.shouldFire(), true);
80
- tracker.markFired();
81
- assert.equal(tracker.turnsSinceLastFire(), 0);
82
- assert.equal(tracker.shouldFire(), false);
83
- tracker.tickOrchestratorTurn();
84
- assert.equal(tracker.turnsSinceLastFire(), 1);
85
- assert.equal(tracker.shouldFire(), false);
86
- tracker.markScopeChangeFire();
87
- assert.equal(tracker.turnsSinceLastFire(), 0);
88
- tracker.reset();
89
- assert.equal(tracker.turnsSinceLastFire(), 0);
90
- assert.equal(tracker.shouldFire(), false);
91
- });
92
- test("CheckpointTracker stays disabled when CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED=0", async () => {
93
- process.env.CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED = "0";
94
- const { checkpointModule } = await loadModules("disabled");
95
- const CheckpointTracker = getConstructor(checkpointModule, "CheckpointTracker");
96
- const tracker = new CheckpointTracker();
97
- for (let index = 0; index < 10; index++) {
98
- tracker.tickOrchestratorTurn();
99
- }
100
- assert.equal(tracker.shouldFire(), false);
101
- });
102
- test("runCheckpointExtraction writes high-confidence memories, skips low-confidence duplicates, and caps at five writes", async () => {
103
- const { dbModule, memoryModule, checkpointModule } = await loadModules();
104
- dbModule.getDb();
105
- const getScope = getFunction(memoryModule, "getScope");
106
- const recordObservation = getFunction(memoryModule, "recordObservation");
107
- const listObservations = getFunction(memoryModule, "listObservations");
108
- const listDecisions = getFunction(memoryModule, "listDecisions");
109
- const runCheckpointExtraction = getFunction(checkpointModule, "runCheckpointExtraction");
110
- const chapterhouse = getScope("chapterhouse");
111
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
112
- recordObservation({
113
- scope_id: chapterhouse.id,
114
- content: "Base new memory implementation branches from origin/main after dependent PRs merge.",
115
- source: "agent:test",
116
- });
117
- const result = await runCheckpointExtraction({
118
- turns: [
119
- {
120
- user: "Implement checkpoint extraction after the response is committed.",
121
- assistant: "I will add an async checkpoint pass with a five-turn cadence.",
122
- },
123
- ],
124
- activeScope: chapterhouse,
125
- copilotClient: {},
126
- callLLM: async () => JSON.stringify({
127
- proposals: [
128
- {
129
- kind: "observation",
130
- content: "Base new memory implementation branches from origin/main after dependent PRs merge.",
131
- confidence: 0.99,
132
- },
133
- {
134
- kind: "decision",
135
- title: "Run checkpoint extraction asynchronously",
136
- content: "Checkpoint extraction runs in the background after the assistant response completes so the next user turn is never blocked.",
137
- confidence: 0.98,
138
- },
139
- {
140
- kind: "observation",
141
- content: "Only orchestrator turns count toward the checkpoint cadence.",
142
- confidence: 0.97,
143
- },
144
- {
145
- kind: "observation",
146
- content: "Checkpoint extraction skips overlapping runs instead of queueing them.",
147
- confidence: 0.96,
148
- },
149
- {
150
- kind: "observation",
151
- content: "Checkpoint extraction defaults to the active scope when writing remembered items.",
152
- confidence: 0.95,
153
- },
154
- {
155
- kind: "observation",
156
- content: "Checkpoint prompts include recent durable decisions already recorded in the active scope to avoid duplicates.",
157
- confidence: 0.94,
158
- },
159
- {
160
- kind: "observation",
161
- content: "Checkpoint prompts stay under a tight token budget.",
162
- confidence: 0.93,
163
- },
164
- {
165
- kind: "observation",
166
- content: "Low-confidence chatter about ephemeral commit messages should not be remembered.",
167
- confidence: 0.49,
168
- },
169
- ],
170
- }),
171
- });
172
- assert.deepEqual(result.errors, []);
173
- assert.equal(result.written, 5);
174
- assert.equal(result.skipped, 3);
175
- assert.equal(listDecisions({ scope_id: chapterhouse.id }).some((row) => row.title === "Run checkpoint extraction asynchronously"), true);
176
- assert.equal(listObservations({ scope_id: chapterhouse.id, limit: 20 }).filter((row) => row.content === "Base new memory implementation branches from origin/main after dependent PRs merge.").length, 1);
177
- });
178
- test("runCheckpointExtraction ignores malformed JSON responses and warns", async (t) => {
179
- const { dbModule, memoryModule, checkpointModule, warnings } = await loadModulesWithWarnSpy(t, "malformed-json");
180
- dbModule.getDb();
181
- const getScope = getFunction(memoryModule, "getScope");
182
- const runCheckpointExtraction = getFunction(checkpointModule, "runCheckpointExtraction");
183
- const chapterhouse = getScope("chapterhouse");
184
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
185
- const result = await runCheckpointExtraction({
186
- turns: [{ user: "Remember this.", assistant: "Okay." }],
187
- activeScope: chapterhouse,
188
- copilotClient: {},
189
- callLLM: async () => "{ definitely-not-json",
190
- });
191
- assert.equal(result.written, 0);
192
- assert.equal(result.skipped, 0);
193
- assert.deepEqual(result.errors, []);
194
- assert.equal(warnings.length, 1);
195
- });
196
- test("runCheckpointExtraction prevents overlapping executions with the in-flight guard", async () => {
197
- const { dbModule, memoryModule, checkpointModule } = await loadModules();
198
- dbModule.getDb();
199
- const getScope = getFunction(memoryModule, "getScope");
200
- const runCheckpointExtraction = getFunction(checkpointModule, "runCheckpointExtraction");
201
- const isCheckpointInFlight = getFunction(checkpointModule, "isCheckpointInFlight");
202
- const chapterhouse = getScope("chapterhouse");
203
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
204
- let resolveFirst;
205
- let llmCalls = 0;
206
- const pendingResponse = new Promise((resolve) => {
207
- resolveFirst = resolve;
208
- });
209
- const firstRun = runCheckpointExtraction({
210
- turns: [{ user: "First", assistant: "Turn" }],
211
- activeScope: chapterhouse,
212
- copilotClient: {},
213
- callLLM: async () => {
214
- llmCalls++;
215
- return await pendingResponse;
216
- },
217
- });
218
- assert.equal(isCheckpointInFlight(), true);
219
- const secondRun = await runCheckpointExtraction({
220
- turns: [{ user: "Second", assistant: "Turn" }],
221
- activeScope: chapterhouse,
222
- copilotClient: {},
223
- callLLM: async () => {
224
- llmCalls++;
225
- return JSON.stringify({ proposals: [] });
226
- },
227
- });
228
- assert.equal(secondRun.written, 0);
229
- assert.equal(llmCalls, 1);
230
- resolveFirst(JSON.stringify({ proposals: [] }));
231
- await firstRun;
232
- assert.equal(isCheckpointInFlight(), false);
233
- });
234
- test("buildCheckpointUserPrompt adds scope-change instructions when context is provided", async () => {
235
- const { checkpointPromptModule } = await loadModules("scope-change-prompt");
236
- const buildCheckpointUserPrompt = getFunction(checkpointPromptModule, "buildCheckpointUserPrompt");
237
- const prompt = buildCheckpointUserPrompt({
238
- turns: [{ user: "Wrap PR 4.5", assistant: "Switching to wiki lint next." }],
239
- activeScope: {
240
- id: 1,
241
- slug: "chapterhouse",
242
- title: "Chapterhouse",
243
- description: "Core Chapterhouse work.",
244
- },
245
- entities: [],
246
- decisions: [],
247
- scopeChangeContext: { from: "chapterhouse", to: "wiki" },
248
- });
249
- assert.match(prompt, /moving from scope chapterhouse to wiki/i);
250
- assert.match(prompt, /extract everything worth remembering about scope chapterhouse/i);
251
- });
252
- test("runCheckpointExtraction passes scope-change context through to the prompt", async () => {
253
- const { dbModule, memoryModule, checkpointModule } = await loadModules("scope-change-context");
254
- dbModule.getDb();
255
- const getScope = getFunction(memoryModule, "getScope");
256
- const runCheckpointExtraction = getFunction(checkpointModule, "runCheckpointExtraction");
257
- const chapterhouse = getScope("chapterhouse");
258
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
259
- let capturedUserPrompt = "";
260
- const result = await runCheckpointExtraction({
261
- turns: [{ user: "Finish memory work", assistant: "Next I will move to wiki cleanup." }],
262
- activeScope: chapterhouse,
263
- copilotClient: {},
264
- trigger: "scope_change",
265
- scopeChangeContext: { from: "chapterhouse", to: "wiki" },
266
- callLLM: async ({ user }) => {
267
- capturedUserPrompt = user;
268
- return JSON.stringify({ proposals: [] });
269
- },
270
- });
271
- assert.deepEqual(result.errors, []);
272
- assert.match(capturedUserPrompt, /moving from scope chapterhouse to wiki/i);
273
- assert.match(capturedUserPrompt, /before the context shifts/i);
274
- });
275
- //# sourceMappingURL=checkpoint.test.js.map
@@ -1,54 +0,0 @@
1
- import { getDb } from "../store/db.js";
2
- function toDecision(row) {
3
- return {
4
- id: row.id,
5
- scopeId: row.scope_id,
6
- entityId: row.entity_id ?? undefined,
7
- title: row.title,
8
- rationale: row.rationale,
9
- decidedAt: row.decided_at,
10
- source: row.source ?? undefined,
11
- tier: row.tier,
12
- supersededBy: row.superseded_by ?? undefined,
13
- archivedAt: row.archived_at ?? undefined,
14
- createdAt: row.created_at,
15
- };
16
- }
17
- export function recordDecision(input) {
18
- const result = getDb().prepare(`
19
- INSERT INTO mem_decisions (scope_id, entity_id, title, rationale, decided_at, source, tier, created_at)
20
- VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
21
- `).run(input.scope_id, input.entity_id ?? null, input.title, input.rationale, input.decided_at ?? new Date().toISOString().slice(0, 10), input.source ?? null, input.tier ?? "warm");
22
- return getDecision(Number(result.lastInsertRowid));
23
- }
24
- export function getDecision(id) {
25
- const row = getDb().prepare(`
26
- SELECT id, scope_id, entity_id, title, rationale, decided_at, source, tier, superseded_by, archived_at, created_at
27
- FROM mem_decisions
28
- WHERE id = ?
29
- `).get(id);
30
- return row ? toDecision(row) : undefined;
31
- }
32
- export function listDecisions(input = {}) {
33
- const rows = getDb().prepare(`
34
- SELECT id, scope_id, entity_id, title, rationale, decided_at, source, tier, superseded_by, archived_at, created_at
35
- FROM mem_decisions
36
- WHERE (? IS NULL OR scope_id = ?)
37
- AND (? IS NULL OR entity_id = ?)
38
- ORDER BY decided_at DESC, id DESC
39
- LIMIT ? OFFSET ?
40
- `).all(input.scope_id ?? null, input.scope_id ?? null, input.entity_id ?? null, input.entity_id ?? null, input.limit ?? 50, input.offset ?? 0);
41
- return rows.map(toDecision);
42
- }
43
- export function supersedeDecision(id, supersededByDecisionId) {
44
- const result = getDb().prepare(`
45
- UPDATE mem_decisions
46
- SET superseded_by = ?
47
- WHERE id = ?
48
- `).run(supersededByDecisionId, id);
49
- if (result.changes === 0) {
50
- throw new Error(`Unknown decision id '${id}'.`);
51
- }
52
- return getDecision(id);
53
- }
54
- //# sourceMappingURL=decisions.js.map