chapterhouse 0.13.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/route-coverage.test.js +1 -3
- package/dist/api/server.js +0 -2
- package/dist/api/server.test.js +0 -281
- package/dist/config.js +3 -85
- package/dist/config.test.js +5 -123
- package/dist/copilot/agents.js +13 -10
- package/dist/copilot/agents.test.js +10 -11
- package/dist/copilot/memory-coordinator.js +12 -227
- package/dist/copilot/memory-coordinator.test.js +31 -250
- package/dist/copilot/orchestrator.js +8 -66
- package/dist/copilot/orchestrator.test.js +9 -467
- package/dist/copilot/skills.js +15 -1
- package/dist/copilot/system-message.js +9 -15
- package/dist/copilot/system-message.test.js +9 -22
- package/dist/copilot/tools/index.js +3 -3
- package/dist/copilot/tools-deps.js +1 -1
- package/dist/copilot/tools.agent.test.js +6 -0
- package/dist/copilot/tools.inventory.test.js +1 -14
- package/dist/daemon.js +7 -9
- package/dist/memory/assets.js +33 -0
- package/dist/memory/domains.js +58 -0
- package/dist/memory/domains.test.js +47 -0
- package/dist/memory/git.js +66 -0
- package/dist/memory/git.test.js +32 -0
- package/dist/memory/history.js +19 -0
- package/dist/memory/hottier.js +32 -0
- package/dist/memory/hottier.test.js +33 -0
- package/dist/memory/index.js +5 -13
- package/dist/memory/instructions.js +17 -0
- package/dist/memory/manager.js +92 -0
- package/dist/memory/markdown.js +78 -0
- package/dist/memory/markdown.test.js +42 -0
- package/dist/memory/mutex.js +18 -0
- package/dist/memory/path-guard.js +26 -0
- package/dist/memory/path-guard.test.js +27 -0
- package/dist/memory/paths.js +12 -0
- package/dist/memory/reconcile.js +75 -0
- package/dist/memory/reconcile.test.js +50 -0
- package/dist/memory/scaffold.js +37 -0
- package/dist/memory/scaffold.test.js +52 -0
- package/dist/memory/tools/commit-wrapper.js +32 -0
- package/dist/memory/tools/domains.js +73 -0
- package/dist/memory/tools/domains.test.js +66 -0
- package/dist/memory/tools/git.js +52 -0
- package/dist/memory/tools/index.js +25 -0
- package/dist/memory/tools/read.js +101 -0
- package/dist/memory/tools/read.test.js +69 -0
- package/dist/memory/tools/search.js +103 -0
- package/dist/memory/tools/search.test.js +63 -0
- package/dist/memory/tools/sessions.js +45 -0
- package/dist/memory/tools/sessions.test.js +74 -0
- package/dist/memory/tools/shared.js +7 -0
- package/dist/memory/tools/write.js +116 -0
- package/dist/memory/tools/write.test.js +107 -0
- package/dist/memory/walk.js +39 -0
- package/dist/store/repositories/sessions.js +40 -0
- package/dist/wiki/consolidation.js +3 -31
- package/dist/wiki/consolidation.test.js +0 -19
- package/memory-assets/domain-skill.md +38 -0
- package/memory-assets/seed/cog-meta/improvements.md +8 -0
- package/memory-assets/seed/cog-meta/patterns.md +5 -0
- package/memory-assets/seed/cog-meta/reflect-cursor.md +4 -0
- package/memory-assets/seed/cog-meta/scenario-calibration.md +14 -0
- package/memory-assets/seed/cog-meta/self-observations.md +4 -0
- package/memory-assets/seed/domains.yml +19 -0
- package/memory-assets/seed/glacier/index.md +6 -0
- package/memory-assets/seed/hot-memory.md +5 -0
- package/memory-assets/seed/link-index.md +6 -0
- package/memory-assets/system-instructions.md +214 -0
- package/memory-assets/templates/action-items.md +8 -0
- package/memory-assets/templates/entities.md +4 -0
- package/memory-assets/templates/generic.md +2 -0
- package/memory-assets/templates/hot-memory.md +4 -0
- package/memory-assets/templates/observations.md +4 -0
- package/package.json +2 -1
- package/skills/system/evolve/SKILL.md +131 -0
- package/skills/system/foresight/SKILL.md +116 -0
- package/skills/system/history/SKILL.md +58 -0
- package/skills/system/housekeeping/SKILL.md +185 -0
- package/skills/system/reflect/SKILL.md +214 -0
- package/skills/system/scenario/SKILL.md +198 -0
- package/skills/system/setup/SKILL.md +113 -0
- package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
- package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
- package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
- package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/dist/api/routes/memory.js +0 -475
- package/dist/api/routes/memory.test.js +0 -108
- package/dist/copilot/tools/memory.js +0 -678
- package/dist/copilot/tools.memory.test.js +0 -590
- package/dist/memory/action-items.js +0 -100
- package/dist/memory/action-items.test.js +0 -83
- package/dist/memory/active-scope.js +0 -78
- package/dist/memory/active-scope.test.js +0 -80
- package/dist/memory/checkpoint-prompt.js +0 -71
- package/dist/memory/checkpoint.js +0 -274
- package/dist/memory/checkpoint.test.js +0 -275
- package/dist/memory/decisions.js +0 -54
- package/dist/memory/decisions.test.js +0 -92
- package/dist/memory/entities.js +0 -70
- package/dist/memory/entities.test.js +0 -65
- package/dist/memory/eot.js +0 -459
- package/dist/memory/eot.test.js +0 -949
- package/dist/memory/hooks.js +0 -149
- package/dist/memory/hooks.test.js +0 -325
- package/dist/memory/hot-tier.js +0 -283
- package/dist/memory/hot-tier.test.js +0 -275
- package/dist/memory/housekeeping-scheduler.js +0 -187
- package/dist/memory/housekeeping-scheduler.test.js +0 -236
- package/dist/memory/housekeeping.js +0 -497
- package/dist/memory/housekeeping.test.js +0 -410
- package/dist/memory/inbox.js +0 -83
- package/dist/memory/inbox.test.js +0 -178
- package/dist/memory/migration.js +0 -244
- package/dist/memory/migration.test.js +0 -108
- package/dist/memory/observations.js +0 -46
- package/dist/memory/observations.test.js +0 -86
- package/dist/memory/recall.js +0 -269
- package/dist/memory/recall.test.js +0 -265
- package/dist/memory/reflect.js +0 -273
- package/dist/memory/reflect.test.js +0 -256
- package/dist/memory/scope-lock.js +0 -26
- package/dist/memory/scope-lock.test.js +0 -118
- package/dist/memory/scopes.js +0 -89
- package/dist/memory/scopes.test.js +0 -176
- package/dist/memory/tiering.js +0 -223
- package/dist/memory/tiering.test.js +0 -323
- package/dist/memory/types.js +0 -2
- package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
package/dist/copilot/agents.js
CHANGED
|
@@ -12,6 +12,7 @@ import { loadMcpConfig } from "./mcp-config.js";
|
|
|
12
12
|
import { getCurrentDateSystemLine } from "./prompt-date.js";
|
|
13
13
|
import { getSkillDirectories } from "./skills.js";
|
|
14
14
|
import { EXCLUDED_BUILTIN_TOOLS } from "./builtin-tools.js";
|
|
15
|
+
import { memorySystemInstructions } from "../memory/index.js";
|
|
15
16
|
import { childLogger } from "../util/logger.js";
|
|
16
17
|
const log = childLogger("agents");
|
|
17
18
|
const toolAgentContext = new AsyncLocalStorage();
|
|
@@ -297,12 +298,12 @@ function getAgentBasePrompt() {
|
|
|
297
298
|
You are an agent within Chapterhouse, a team-level AI assistant for engineering teams. You run on the user's local machine.
|
|
298
299
|
|
|
299
300
|
### Agent Memory
|
|
300
|
-
Chapterhouse
|
|
301
|
+
Chapterhouse gives every agent a persistent, file-based memory tree under \`memory/\`, organized into domains. Read it with \`cog_read\`, \`cog_l0_scan\`, \`cog_l1_outline\`, \`cog_tree\`, and \`cog_search\`; write directly with \`cog_write\`, \`cog_edit\`, \`cog_append\`, and \`cog_move\` whenever you learn something worth remembering — don't wait for end of task. Every write auto-commits to git. The full memory operating instructions are included below.
|
|
301
302
|
|
|
302
303
|
### Shared Wiki
|
|
303
304
|
All agents share a wiki knowledge base for persistent memory. Use \`wiki_read\` and \`wiki_search\` to find existing knowledge, and \`wiki_update\` to save important findings.
|
|
304
305
|
|
|
305
|
-
Invoke \`wiki-conventions\` before wiki writes or restructuring work. Treat \`wiki_update\` and \`wiki_ingest_source\` as write-sensitive workflows
|
|
306
|
+
Invoke \`wiki-conventions\` before wiki writes or restructuring work. Treat \`wiki_update\` and \`wiki_ingest_source\` as write-sensitive workflows. Before using wiki write tools, read \`pages/index.md\`, scan the last 20-30 entries of \`pages/_meta/log.md\`, and run \`wiki_search\` for the topic when the wiki is large or the topic is ambiguous.
|
|
306
307
|
|
|
307
308
|
### Communication
|
|
308
309
|
- You receive tasks from @chapterhouse (the orchestrator) or directly from the user
|
|
@@ -321,11 +322,12 @@ export function composeAgentSystemMessage(agent, rosterInfo) {
|
|
|
321
322
|
const agentPrompt = agent.systemMessage;
|
|
322
323
|
const currentDateLine = getCurrentDateSystemLine();
|
|
323
324
|
const currentDateBlock = currentDateLine ? `${currentDateLine}\n\n` : "";
|
|
325
|
+
const memoryBlock = memorySystemInstructions() ? `\n\n${memorySystemInstructions()}` : "";
|
|
324
326
|
// For @chapterhouse, inject the agent roster
|
|
325
327
|
if (agent.slug === "chapterhouse" && rosterInfo) {
|
|
326
|
-
return `${currentDateBlock}${agentPrompt.replace("{agent_roster}", rosterInfo)}`;
|
|
328
|
+
return `${currentDateBlock}${agentPrompt.replace("{agent_roster}", rosterInfo)}${memoryBlock}`;
|
|
327
329
|
}
|
|
328
|
-
return `${currentDateBlock}${agentPrompt}\n\n${base}`;
|
|
330
|
+
return `${currentDateBlock}${agentPrompt}\n\n${base}${memoryBlock}`;
|
|
329
331
|
}
|
|
330
332
|
/** Build a roster description of all agents for @chapterhouse's system prompt. */
|
|
331
333
|
export function buildAgentRoster() {
|
|
@@ -341,10 +343,13 @@ export function buildAgentRoster() {
|
|
|
341
343
|
return "No agents registered.";
|
|
342
344
|
return chLines.join("\n");
|
|
343
345
|
}
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
+
// Tools every agent gets regardless of tool config: the shared wiki and the
|
|
347
|
+
// full file-based memory surface (all agents read and write memory directly).
|
|
348
|
+
const ALWAYS_AVAILABLE_TOOL_NAMES = new Set([
|
|
346
349
|
"wiki_search", "wiki_read", "wiki_update", "wiki_reindex", "wiki_append_timeline", "wiki_ingest_source",
|
|
347
|
-
"
|
|
350
|
+
"cog_read", "cog_l0_scan", "cog_l1_outline", "cog_tree", "cog_search", "cog_stats",
|
|
351
|
+
"cog_write", "cog_edit", "cog_append", "cog_move",
|
|
352
|
+
"cog_git", "cog_domains", "cog_domain_create", "cog_sessions",
|
|
348
353
|
]);
|
|
349
354
|
// Management tools that only @chapterhouse should have
|
|
350
355
|
const MANAGEMENT_TOOL_NAMES = new Set([
|
|
@@ -353,8 +358,6 @@ const MANAGEMENT_TOOL_NAMES = new Set([
|
|
|
353
358
|
"switch_model", "toggle_auto", "list_models",
|
|
354
359
|
"restart_chapterhouse", "list_skills", "learn_skill", "uninstall_skill",
|
|
355
360
|
"list_machine_sessions", "attach_machine_session",
|
|
356
|
-
"memory_remember", "memory_set_scope", "memory_housekeep", "memory_reflect", "memory_promote", "memory_demote",
|
|
357
|
-
"memory_add_action_item", "memory_complete_action_item", "memory_drop_action_item", "memory_snooze_action_item",
|
|
358
361
|
]);
|
|
359
362
|
export function getCurrentToolAgentSlug() {
|
|
360
363
|
return toolAgentContext.getStore();
|
|
@@ -375,7 +378,7 @@ export function bindToolsToAgent(agentSlug, allTools, taskId) {
|
|
|
375
378
|
export function filterToolsForAgent(agent, allTools) {
|
|
376
379
|
if (agent.tools && agent.tools.length > 0) {
|
|
377
380
|
// Agent specifies an explicit allowlist — give those + wiki tools
|
|
378
|
-
const allowed = new Set([...agent.tools, ...
|
|
381
|
+
const allowed = new Set([...agent.tools, ...ALWAYS_AVAILABLE_TOOL_NAMES]);
|
|
379
382
|
return allTools.filter((t) => allowed.has(t.name));
|
|
380
383
|
}
|
|
381
384
|
// Default: all tools except management (only @chapterhouse gets those by default)
|
|
@@ -50,18 +50,17 @@ test("composeAgentSystemMessage steers wiki-capable agents to wiki-conventions",
|
|
|
50
50
|
for (const slug of ["coder", "general-purpose"]) {
|
|
51
51
|
const message = composeAgentSystemMessage(makeAgent(slug));
|
|
52
52
|
assert.match(message, /invoke `wiki-conventions` before wiki writes/i);
|
|
53
|
-
assert.match(message, /wiki_update[\s\S]{0,120}wiki_ingest_source
|
|
53
|
+
assert.match(message, /wiki_update[\s\S]{0,120}wiki_ingest_source/i);
|
|
54
54
|
assert.doesNotMatch(message, /`remember`|`forget`|`wiki_ingest`(?!_source)|`wiki_lint`|`wiki_rebuild_index`/);
|
|
55
55
|
assert.match(message, /read `pages\/index\.md`/i);
|
|
56
56
|
assert.match(message, /scan the last 20-30 entries of `pages\/_meta\/log\.md`/i);
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
|
-
test("composeAgentSystemMessage teaches subagents the
|
|
59
|
+
test("composeAgentSystemMessage teaches subagents the file-based memory tools", () => {
|
|
60
60
|
const message = composeAgentSystemMessage(makeAgent("coder"));
|
|
61
|
-
assert.match(message, /
|
|
62
|
-
assert.match(message, /
|
|
63
|
-
assert.match(message, /
|
|
64
|
-
assert.match(message, /do not call `memory_remember` directly|should not call `memory_remember` directly/i);
|
|
61
|
+
assert.match(message, /cog_read/);
|
|
62
|
+
assert.match(message, /cog_write/);
|
|
63
|
+
assert.match(message, /write directly/i);
|
|
65
64
|
});
|
|
66
65
|
test("parseAgentMd detects persistent agent scope from charter frontmatter", () => {
|
|
67
66
|
const agent = parseAgentMd([
|
|
@@ -103,22 +102,22 @@ test("korg charter is persistent and standardizes on the Summary heading", () =>
|
|
|
103
102
|
assert.match(charter, /## Summary/);
|
|
104
103
|
assert.doesNotMatch(charter, /## Compiled Truth/);
|
|
105
104
|
});
|
|
106
|
-
test("persistent agents cannot receive
|
|
105
|
+
test("persistent agents cannot receive management tools", () => {
|
|
107
106
|
const agent = {
|
|
108
107
|
...makeAgent("bellonda"),
|
|
109
108
|
persistent: true,
|
|
110
109
|
scope: "infra",
|
|
111
110
|
};
|
|
112
111
|
const tools = [
|
|
113
|
-
{ name: "
|
|
114
|
-
{ name: "
|
|
115
|
-
{ name: "memory_set_scope" },
|
|
112
|
+
{ name: "cog_read" },
|
|
113
|
+
{ name: "cog_write" },
|
|
116
114
|
{ name: "delegate_to_agent" },
|
|
115
|
+
{ name: "hire_agent" },
|
|
117
116
|
{ name: "bash" },
|
|
118
117
|
];
|
|
119
118
|
const filtered = filterToolsForAgent(agent, tools);
|
|
120
119
|
const names = filtered.map((tool) => tool.name);
|
|
121
|
-
assert.deepEqual(names.sort(), ["bash", "
|
|
120
|
+
assert.deepEqual(names.sort(), ["bash", "cog_read", "cog_write"].sort());
|
|
122
121
|
});
|
|
123
122
|
test("bindToolsToAgent uses the per-turn task context when no fixed task id is provided", async () => {
|
|
124
123
|
const tools = bindToolsToAgent("bellonda", [{
|
|
@@ -1,234 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { childLogger } from "../util/logger.js";
|
|
9
|
-
const log = childLogger("memory-coordinator");
|
|
10
|
-
const MAX_CHECKPOINT_CHARS_PER_SIDE = 4_000;
|
|
1
|
+
import { getMemoryManager } from "../memory/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wires the file-based memory subsystem into the agent runtime. Its only job is
|
|
4
|
+
* to inject the always-loaded hot-tier block into each turn via the
|
|
5
|
+
* onUserPromptSubmitted hook — all memory reads and writes are agent-driven via
|
|
6
|
+
* the cog_* tools.
|
|
7
|
+
*/
|
|
11
8
|
export class MemoryCoordinator {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
completedTaskIds = new Set();
|
|
16
|
-
getCopilotClient;
|
|
17
|
-
resolveScopeForSession;
|
|
18
|
-
config;
|
|
19
|
-
constructor(options) {
|
|
20
|
-
this.getCopilotClient = options.getCopilotClient;
|
|
21
|
-
this.resolveScopeForSession = options.resolveScopeForSession ?? (() => getActiveScope());
|
|
22
|
-
this.config = options.config ?? defaultConfig;
|
|
23
|
-
}
|
|
24
|
-
async onTurnComplete(sessionKey, prompt, response, source) {
|
|
25
|
-
const sourceType = this.normalizeSource(source);
|
|
26
|
-
if (sourceType === "background") {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
this.scheduleCheckpointExtraction(sessionKey, prompt, response);
|
|
30
|
-
this.scheduleHousekeeping(sessionKey);
|
|
31
|
-
}
|
|
32
|
-
async onScopeChange(sessionKey, prev, next) {
|
|
33
|
-
if (!prev) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const previousScope = getScope(prev) ?? null;
|
|
37
|
-
if (!previousScope) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (!this.config.memoryCheckpointOnScopeChange) {
|
|
41
|
-
log.info({ sessionKey, scope: previousScope.slug }, "memory.checkpoint.scope_change_disabled");
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const tracker = this.getCheckpointTracker(sessionKey);
|
|
45
|
-
const turnsSinceLast = tracker.turnsSinceLastFire();
|
|
46
|
-
if (turnsSinceLast < this.config.memoryCheckpointMinTurnsForScopeFire) {
|
|
47
|
-
log.info({
|
|
48
|
-
sessionKey,
|
|
49
|
-
scope: previousScope.slug,
|
|
50
|
-
turns_since_last: turnsSinceLast,
|
|
51
|
-
min_required: this.config.memoryCheckpointMinTurnsForScopeFire,
|
|
52
|
-
}, "memory.checkpoint.scope_change_skip");
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (isCheckpointInFlight(sessionKey)) {
|
|
56
|
-
log.info({ sessionKey, trigger: "scope_change" }, "memory.checkpoint.in_flight_skip");
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const copilotClient = this.getCopilotClient();
|
|
60
|
-
if (!copilotClient) {
|
|
61
|
-
log.error({ sessionKey }, "memory.checkpoint.error");
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const turns = this.checkpointTurnsBySession.get(sessionKey) ?? [];
|
|
65
|
-
if (turns.length === 0) {
|
|
66
|
-
log.info({
|
|
67
|
-
sessionKey,
|
|
68
|
-
scope: previousScope.slug,
|
|
69
|
-
turns_since_last: turnsSinceLast,
|
|
70
|
-
min_required: this.config.memoryCheckpointMinTurnsForScopeFire,
|
|
71
|
-
}, "memory.checkpoint.scope_change_skip");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
tracker.markScopeChangeFire();
|
|
75
|
-
const nextScope = next ? (getScope(next) ?? null) : null;
|
|
76
|
-
void runCheckpointExtraction({
|
|
77
|
-
sessionKey,
|
|
78
|
-
turns: turns.slice(-this.config.memoryCheckpointTurns),
|
|
79
|
-
activeScope: previousScope,
|
|
80
|
-
copilotClient,
|
|
81
|
-
trigger: "scope_change",
|
|
82
|
-
scopeChangeContext: {
|
|
83
|
-
from: previousScope.slug,
|
|
84
|
-
to: nextScope?.slug ?? "no active scope",
|
|
85
|
-
},
|
|
86
|
-
}).catch((error) => {
|
|
87
|
-
log.error({ err: error, sessionKey }, "memory.checkpoint.error");
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
async buildHotTierContext(sessionKey) {
|
|
91
|
-
if (!this.config.memoryInjectEnabled) {
|
|
92
|
-
return "";
|
|
93
|
-
}
|
|
94
|
-
const scope = this.resolveScopeForSession(sessionKey);
|
|
95
|
-
if (!scope) {
|
|
96
|
-
return "";
|
|
97
|
-
}
|
|
98
|
-
const activeScope = getActiveScope();
|
|
99
|
-
const hotTierXml = activeScope?.id === scope.id
|
|
100
|
-
? renderHotTierForActiveScope()
|
|
101
|
-
: renderHotTierXML(getHotTierEntries(scope.id));
|
|
102
|
-
return hotTierXml ? hotTierXml.trimEnd() : "";
|
|
103
|
-
}
|
|
104
|
-
buildPerTurnHooks(sessionKey) {
|
|
105
|
-
if (!this.config.memoryInjectEnabled) {
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
const hooks = {
|
|
9
|
+
/** Returns the per-turn hooks that inject the hot-tier memory block. */
|
|
10
|
+
buildPerTurnHooks() {
|
|
11
|
+
return {
|
|
109
12
|
onUserPromptSubmitted: async () => {
|
|
110
|
-
const
|
|
111
|
-
return
|
|
13
|
+
const block = getMemoryManager().hotTier();
|
|
14
|
+
return block ? { additionalContext: block } : undefined;
|
|
112
15
|
},
|
|
113
16
|
};
|
|
114
|
-
return hooks;
|
|
115
|
-
}
|
|
116
|
-
async onAgentTaskComplete(taskId, result) {
|
|
117
|
-
if (this.completedTaskIds.has(taskId)) {
|
|
118
|
-
log.info({ taskId }, "memory.eot.duplicate_skip");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
this.completedTaskIds.add(taskId);
|
|
122
|
-
const copilotClient = this.getCopilotClient();
|
|
123
|
-
if (!copilotClient) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const finalResult = typeof result === "string" ? result : result == null ? "" : String(result);
|
|
127
|
-
await runEndOfTaskMemoryHook({
|
|
128
|
-
taskId,
|
|
129
|
-
finalResult,
|
|
130
|
-
copilotClient,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
reset(sessionKey) {
|
|
134
|
-
this.getCheckpointTracker(sessionKey).reset();
|
|
135
|
-
this.checkpointTurnsBySession.delete(sessionKey);
|
|
136
|
-
this.housekeepingTurnsBySession.delete(sessionKey);
|
|
137
|
-
this.completedTaskIds.clear();
|
|
138
|
-
}
|
|
139
|
-
shutdown() {
|
|
140
|
-
this.checkpointTrackers.clear();
|
|
141
|
-
this.checkpointTurnsBySession.clear();
|
|
142
|
-
this.housekeepingTurnsBySession.clear();
|
|
143
|
-
this.completedTaskIds.clear();
|
|
144
|
-
}
|
|
145
|
-
normalizeSource(source) {
|
|
146
|
-
return source === "background" ? "background" : source === "sse-web" ? "sse-web" : "web";
|
|
147
|
-
}
|
|
148
|
-
truncateCheckpointText(value) {
|
|
149
|
-
const trimmed = value.trim();
|
|
150
|
-
if (trimmed.length <= MAX_CHECKPOINT_CHARS_PER_SIDE) {
|
|
151
|
-
return trimmed;
|
|
152
|
-
}
|
|
153
|
-
return `${trimmed.slice(0, MAX_CHECKPOINT_CHARS_PER_SIDE)}…`;
|
|
154
|
-
}
|
|
155
|
-
getCheckpointTracker(sessionKey) {
|
|
156
|
-
let tracker = this.checkpointTrackers.get(sessionKey);
|
|
157
|
-
if (!tracker) {
|
|
158
|
-
tracker = new CheckpointTracker();
|
|
159
|
-
this.checkpointTrackers.set(sessionKey, tracker);
|
|
160
|
-
}
|
|
161
|
-
return tracker;
|
|
162
|
-
}
|
|
163
|
-
appendCheckpointTurn(sessionKey, turn) {
|
|
164
|
-
const turns = this.checkpointTurnsBySession.get(sessionKey) ?? [];
|
|
165
|
-
turns.push(turn);
|
|
166
|
-
const overflow = turns.length - this.config.memoryCheckpointTurns;
|
|
167
|
-
if (overflow > 0) {
|
|
168
|
-
turns.splice(0, overflow);
|
|
169
|
-
}
|
|
170
|
-
this.checkpointTurnsBySession.set(sessionKey, turns);
|
|
171
|
-
return turns;
|
|
172
|
-
}
|
|
173
|
-
scheduleCheckpointExtraction(sessionKey, prompt, response) {
|
|
174
|
-
const tracker = this.getCheckpointTracker(sessionKey);
|
|
175
|
-
const turns = this.appendCheckpointTurn(sessionKey, {
|
|
176
|
-
user: this.truncateCheckpointText(prompt),
|
|
177
|
-
assistant: this.truncateCheckpointText(response),
|
|
178
|
-
});
|
|
179
|
-
if (!this.config.memoryCheckpointEnabled) {
|
|
180
|
-
log.info({ sessionKey }, "memory.checkpoint.disabled");
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
tracker.tickOrchestratorTurn();
|
|
184
|
-
if (!tracker.shouldFire()) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
tracker.markFired();
|
|
188
|
-
if (isCheckpointInFlight(sessionKey)) {
|
|
189
|
-
log.info({ sessionKey }, "memory.checkpoint.in_flight_skip");
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const copilotClient = this.getCopilotClient();
|
|
193
|
-
if (!copilotClient) {
|
|
194
|
-
log.error({ sessionKey }, "memory.checkpoint.error");
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const activeScope = this.resolveScopeForSession(sessionKey);
|
|
198
|
-
void runCheckpointExtraction({
|
|
199
|
-
sessionKey,
|
|
200
|
-
turns: turns.slice(-this.config.memoryCheckpointTurns),
|
|
201
|
-
activeScope,
|
|
202
|
-
copilotClient,
|
|
203
|
-
trigger: "cadence",
|
|
204
|
-
}).catch((error) => {
|
|
205
|
-
log.error({ err: error, sessionKey }, "memory.checkpoint.error");
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
scheduleHousekeeping(sessionKey) {
|
|
209
|
-
if (!this.config.memoryHousekeepingEnabled) {
|
|
210
|
-
log.info({ sessionKey }, "memory.housekeeping.disabled");
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const turns = (this.housekeepingTurnsBySession.get(sessionKey) ?? 0) + 1;
|
|
214
|
-
if (turns < this.config.memoryHousekeepingTurns) {
|
|
215
|
-
this.housekeepingTurnsBySession.set(sessionKey, turns);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
this.housekeepingTurnsBySession.set(sessionKey, 0);
|
|
219
|
-
const activeScope = this.resolveScopeForSession(sessionKey);
|
|
220
|
-
if (!activeScope) {
|
|
221
|
-
log.info({ sessionKey }, "memory.housekeeping.no_active_scope");
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const scopeIds = [activeScope.id];
|
|
225
|
-
if (isHousekeepingInFlight(scopeIds)) {
|
|
226
|
-
log.info({ sessionKey, scope_ids: scopeIds }, "memory.housekeeping.in_flight_skip");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
void runHousekeeping({ scopeIds }).catch((error) => {
|
|
230
|
-
log.error({ err: error, sessionKey, scope_ids: scopeIds }, "memory.housekeeping.error");
|
|
231
|
-
});
|
|
232
17
|
}
|
|
233
18
|
}
|
|
234
19
|
//# sourceMappingURL=memory-coordinator.js.map
|
|
@@ -1,257 +1,38 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import test from "node:test";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
updatedAt: "2026-05-14T00:00:00.000Z",
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
async function loadMemoryCoordinatorModule(t, overrides = {}) {
|
|
16
|
-
const chapterhouseScope = makeScope(1, "chapterhouse", "Chapterhouse", "Core Chapterhouse work.");
|
|
17
|
-
const wikiScope = makeScope(2, "wiki", "Wiki", "Wiki work.");
|
|
18
|
-
const state = {
|
|
19
|
-
config: {
|
|
20
|
-
copilotModel: "claude-sonnet-4.6",
|
|
21
|
-
memoryInjectEnabled: true,
|
|
22
|
-
memoryCheckpointEnabled: true,
|
|
23
|
-
memoryCheckpointTurns: 2,
|
|
24
|
-
memoryCheckpointOnScopeChange: true,
|
|
25
|
-
memoryCheckpointMinTurnsForScopeFire: 2,
|
|
26
|
-
memoryHousekeepingEnabled: true,
|
|
27
|
-
memoryHousekeepingTurns: 2,
|
|
28
|
-
},
|
|
29
|
-
activeScope: chapterhouseScope,
|
|
30
|
-
scopes: new Map([
|
|
31
|
-
[chapterhouseScope.slug, chapterhouseScope],
|
|
32
|
-
[wikiScope.slug, wikiScope],
|
|
33
|
-
]),
|
|
34
|
-
sessionScopes: new Map([["default", chapterhouseScope]]),
|
|
35
|
-
hotTierXmlByScope: new Map([[chapterhouseScope.slug, "<memory_context scope=\"chapterhouse\">\n <decision>Ship it</decision>\n</memory_context>\n"]]),
|
|
36
|
-
checkpointRuns: [],
|
|
37
|
-
checkpointInFlight: false,
|
|
38
|
-
checkpointTickCalls: 0,
|
|
39
|
-
checkpointMarkFiredCalls: 0,
|
|
40
|
-
checkpointMarkScopeChangeFireCalls: 0,
|
|
41
|
-
checkpointResetCalls: 0,
|
|
42
|
-
housekeepingRuns: [],
|
|
43
|
-
housekeepingInFlight: false,
|
|
44
|
-
eotCalls: [],
|
|
45
|
-
...overrides,
|
|
46
|
-
};
|
|
47
|
-
t.mock.module("../config.js", {
|
|
48
|
-
namedExports: {
|
|
49
|
-
config: state.config,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
t.mock.module("../memory/active-scope.js", {
|
|
53
|
-
namedExports: {
|
|
54
|
-
getActiveScope: () => state.activeScope,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
t.mock.module("../memory/scopes.js", {
|
|
58
|
-
namedExports: {
|
|
59
|
-
getScope: (slug) => state.scopes.get(slug) ?? null,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
t.mock.module("../memory/hot-tier.js", {
|
|
63
|
-
namedExports: {
|
|
64
|
-
getHotTierEntries: (scopeId) => {
|
|
65
|
-
const scope = Array.from(state.scopes.values()).find((candidate) => candidate.id === scopeId) ?? null;
|
|
66
|
-
return { scope, entities: [], observations: [], decisions: [], actionItems: [] };
|
|
67
|
-
},
|
|
68
|
-
renderHotTierXML: (entries) => entries.scope ? (state.hotTierXmlByScope.get(entries.scope.slug) ?? "") : "",
|
|
69
|
-
renderHotTierForActiveScope: () => state.activeScope ? (state.hotTierXmlByScope.get(state.activeScope.slug) ?? "") : "",
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
t.mock.module("../memory/checkpoint.js", {
|
|
73
|
-
namedExports: {
|
|
74
|
-
CheckpointTracker: class {
|
|
75
|
-
turns = 0;
|
|
76
|
-
tickOrchestratorTurn() {
|
|
77
|
-
state.checkpointTickCalls++;
|
|
78
|
-
this.turns++;
|
|
79
|
-
}
|
|
80
|
-
shouldFire() {
|
|
81
|
-
return this.turns >= state.config.memoryCheckpointTurns;
|
|
82
|
-
}
|
|
83
|
-
turnsSinceLastFire() {
|
|
84
|
-
return this.turns;
|
|
85
|
-
}
|
|
86
|
-
markFired() {
|
|
87
|
-
state.checkpointMarkFiredCalls++;
|
|
88
|
-
this.turns = 0;
|
|
89
|
-
}
|
|
90
|
-
markScopeChangeFire() {
|
|
91
|
-
state.checkpointMarkScopeChangeFireCalls++;
|
|
92
|
-
this.turns = 0;
|
|
93
|
-
}
|
|
94
|
-
reset() {
|
|
95
|
-
state.checkpointResetCalls++;
|
|
96
|
-
this.turns = 0;
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
isCheckpointInFlight: () => state.checkpointInFlight,
|
|
100
|
-
runCheckpointExtraction: async (input) => {
|
|
101
|
-
state.checkpointRuns.push(input);
|
|
102
|
-
return { written: 0, skipped: 0, errors: [] };
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
t.mock.module("../memory/housekeeping.js", {
|
|
107
|
-
namedExports: {
|
|
108
|
-
isHousekeepingInFlight: () => state.housekeepingInFlight,
|
|
109
|
-
runHousekeeping: async (input) => {
|
|
110
|
-
state.housekeepingRuns.push(input);
|
|
111
|
-
return { scopeIds: input.scopeIds ?? [], summaries: [], totalExamined: 0, totalModified: 0, durationMs: 0 };
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
t.mock.module("../memory/eot.js", {
|
|
116
|
-
namedExports: {
|
|
117
|
-
runEndOfTaskMemoryHook: async (input) => {
|
|
118
|
-
state.eotCalls.push(input);
|
|
119
|
-
return {
|
|
120
|
-
task_id: String(input.taskId ?? "task"),
|
|
121
|
-
proposals_total: 0,
|
|
122
|
-
accepted: 0,
|
|
123
|
-
rejected: 0,
|
|
124
|
-
implicit_extracted: 0,
|
|
125
|
-
auto_accept: true,
|
|
126
|
-
};
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
const module = await import(new URL(`./memory-coordinator.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
131
|
-
return { module, state };
|
|
132
|
-
}
|
|
133
|
-
test("buildHotTierContext and buildPerTurnHooks inject trimmed scoped hot-tier memory", async (t) => {
|
|
134
|
-
const { module, state } = await loadMemoryCoordinatorModule(t);
|
|
135
|
-
const client = { name: "mock-client" };
|
|
136
|
-
const coordinator = new module.MemoryCoordinator({
|
|
137
|
-
getCopilotClient: () => client,
|
|
138
|
-
config: state.config,
|
|
139
|
-
resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
|
|
140
|
-
});
|
|
141
|
-
const hotTierContext = await coordinator.buildHotTierContext("default");
|
|
142
|
-
const hooks = coordinator.buildPerTurnHooks("default");
|
|
143
|
-
const hookResult = await hooks?.onUserPromptSubmitted?.();
|
|
144
|
-
assert.equal(hotTierContext, "<memory_context scope=\"chapterhouse\">\n <decision>Ship it</decision>\n</memory_context>");
|
|
145
|
-
assert.deepEqual(hookResult, {
|
|
146
|
-
additionalContext: "<memory_context scope=\"chapterhouse\">\n <decision>Ship it</decision>\n</memory_context>",
|
|
147
|
-
});
|
|
5
|
+
const sandbox = join(process.cwd(), ".test-work", `memory-coordinator-${process.pid}`);
|
|
6
|
+
process.env.CHAPTERHOUSE_HOME = sandbox;
|
|
7
|
+
import { MemoryCoordinator } from "./memory-coordinator.js";
|
|
8
|
+
import { gitAvailable } from "../memory/git.js";
|
|
9
|
+
import { getMemoryManager, resetMemoryManagerForTests } from "../memory/index.js";
|
|
10
|
+
test.beforeEach(() => {
|
|
11
|
+
rmSync(sandbox, { recursive: true, force: true });
|
|
12
|
+
mkdirSync(sandbox, { recursive: true });
|
|
13
|
+
resetMemoryManagerForTests();
|
|
148
14
|
});
|
|
149
|
-
test(
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
|
|
155
|
-
});
|
|
156
|
-
await coordinator.onTurnComplete("default", "First prompt", "First response", "web");
|
|
157
|
-
assert.equal(state.checkpointRuns.length, 0);
|
|
158
|
-
assert.equal(state.housekeepingRuns.length, 0);
|
|
159
|
-
await coordinator.onTurnComplete("default", "Second prompt", "Second response", "web");
|
|
160
|
-
assert.equal(state.checkpointMarkFiredCalls, 1);
|
|
161
|
-
assert.equal(state.checkpointRuns.length, 1);
|
|
162
|
-
assert.equal(state.housekeepingRuns.length, 1);
|
|
163
|
-
assert.deepEqual(state.housekeepingRuns[0]?.scopeIds, [1]);
|
|
164
|
-
assert.equal((state.checkpointRuns[0]?.activeScope).slug, "chapterhouse");
|
|
165
|
-
assert.deepEqual(state.checkpointRuns[0]?.turns, [
|
|
166
|
-
{ user: "First prompt", assistant: "First response" },
|
|
167
|
-
{ user: "Second prompt", assistant: "Second response" },
|
|
168
|
-
]);
|
|
169
|
-
await coordinator.onTurnComplete("default", "Background prompt", "Background response", "background");
|
|
170
|
-
assert.equal(state.checkpointRuns.length, 1);
|
|
171
|
-
assert.equal(state.housekeepingRuns.length, 1);
|
|
15
|
+
test.after(() => rmSync(sandbox, { recursive: true, force: true }));
|
|
16
|
+
test("buildPerTurnHooks returns an onUserPromptSubmitted hook", () => {
|
|
17
|
+
const hooks = new MemoryCoordinator().buildPerTurnHooks();
|
|
18
|
+
assert.ok(hooks);
|
|
19
|
+
assert.equal(typeof hooks.onUserPromptSubmitted, "function");
|
|
172
20
|
});
|
|
173
|
-
test("
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
memoryInjectEnabled: true,
|
|
178
|
-
memoryCheckpointEnabled: true,
|
|
179
|
-
memoryCheckpointTurns: 5,
|
|
180
|
-
memoryCheckpointOnScopeChange: true,
|
|
181
|
-
memoryCheckpointMinTurnsForScopeFire: 2,
|
|
182
|
-
memoryHousekeepingEnabled: true,
|
|
183
|
-
memoryHousekeepingTurns: 2,
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
const coordinator = new module.MemoryCoordinator({
|
|
187
|
-
getCopilotClient: () => ({ name: "mock-client" }),
|
|
188
|
-
config: state.config,
|
|
189
|
-
resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
|
|
190
|
-
});
|
|
191
|
-
await coordinator.onTurnComplete("default", "Turn one", "Reply one", "web");
|
|
192
|
-
await coordinator.onTurnComplete("default", "Turn two", "Reply two", "web");
|
|
193
|
-
state.checkpointRuns.length = 0;
|
|
194
|
-
await coordinator.onScopeChange("default", "chapterhouse", "wiki");
|
|
195
|
-
assert.equal(state.checkpointMarkScopeChangeFireCalls, 1);
|
|
196
|
-
assert.equal(state.checkpointRuns.length, 1);
|
|
197
|
-
assert.equal((state.checkpointRuns[0]?.activeScope).slug, "chapterhouse");
|
|
198
|
-
assert.equal(state.checkpointRuns[0]?.trigger, "scope_change");
|
|
199
|
-
assert.deepEqual(state.checkpointRuns[0]?.scopeChangeContext, { from: "chapterhouse", to: "wiki" });
|
|
21
|
+
test("the hook returns undefined when memory has no hot-tier content", async () => {
|
|
22
|
+
const hooks = new MemoryCoordinator().buildPerTurnHooks();
|
|
23
|
+
const result = await hooks.onUserPromptSubmitted();
|
|
24
|
+
assert.equal(result, undefined);
|
|
200
25
|
});
|
|
201
|
-
test("
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
assert.
|
|
211
|
-
assert.
|
|
212
|
-
assert.equal(state.eotCalls[0]?.finalResult, "Final delegated result");
|
|
213
|
-
assert.equal(state.eotCalls[0]?.copilotClient, client);
|
|
214
|
-
});
|
|
215
|
-
test("onAgentTaskComplete is a no-op for a duplicate task ID (double-fire guard)", async (t) => {
|
|
216
|
-
const { module, state } = await loadMemoryCoordinatorModule(t);
|
|
217
|
-
const client = { name: "mock-client" };
|
|
218
|
-
const coordinator = new module.MemoryCoordinator({
|
|
219
|
-
getCopilotClient: () => client,
|
|
220
|
-
config: state.config,
|
|
221
|
-
resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
|
|
222
|
-
});
|
|
223
|
-
await coordinator.onAgentTaskComplete("task-99", "result one");
|
|
224
|
-
await coordinator.onAgentTaskComplete("task-99", "result two");
|
|
225
|
-
assert.equal(state.eotCalls.length, 1);
|
|
226
|
-
assert.equal(state.eotCalls[0]?.taskId, "task-99");
|
|
227
|
-
assert.equal(state.eotCalls[0]?.finalResult, "result one");
|
|
228
|
-
});
|
|
229
|
-
test("reset clears turn buffers and tracker state for a session", async (t) => {
|
|
230
|
-
const { module, state } = await loadMemoryCoordinatorModule(t);
|
|
231
|
-
const coordinator = new module.MemoryCoordinator({
|
|
232
|
-
getCopilotClient: () => ({ name: "mock-client" }),
|
|
233
|
-
config: state.config,
|
|
234
|
-
resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
|
|
235
|
-
});
|
|
236
|
-
await coordinator.onTurnComplete("default", "Turn one", "Reply one", "web");
|
|
237
|
-
coordinator.reset("default");
|
|
238
|
-
await coordinator.onTurnComplete("default", "Turn two", "Reply two", "web");
|
|
239
|
-
await coordinator.onScopeChange("default", "chapterhouse", "wiki");
|
|
240
|
-
assert.equal(state.checkpointResetCalls, 1);
|
|
241
|
-
assert.equal(state.checkpointRuns.length, 0);
|
|
242
|
-
assert.equal(state.housekeepingRuns.length, 0);
|
|
243
|
-
});
|
|
244
|
-
test("shutdown clears session state across all tracked maps", async (t) => {
|
|
245
|
-
const { module, state } = await loadMemoryCoordinatorModule(t);
|
|
246
|
-
const coordinator = new module.MemoryCoordinator({
|
|
247
|
-
getCopilotClient: () => ({ name: "mock-client" }),
|
|
248
|
-
config: state.config,
|
|
249
|
-
resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
|
|
250
|
-
});
|
|
251
|
-
await coordinator.onTurnComplete("default", "Turn one", "Reply one", "web");
|
|
252
|
-
coordinator.shutdown();
|
|
253
|
-
await coordinator.onTurnComplete("default", "Turn two", "Reply two", "web");
|
|
254
|
-
assert.equal(state.checkpointRuns.length, 0);
|
|
255
|
-
assert.equal(state.housekeepingRuns.length, 0);
|
|
26
|
+
test("the hook injects the hot-tier block once memory is scaffolded", async () => {
|
|
27
|
+
if (!gitAvailable())
|
|
28
|
+
return;
|
|
29
|
+
const manager = getMemoryManager();
|
|
30
|
+
await manager.ensureReady();
|
|
31
|
+
writeFileSync(join(manager.paths.memoryRoot, "hot-memory.md"), "<!-- L0: hot -->\ntop of mind");
|
|
32
|
+
const hooks = new MemoryCoordinator().buildPerTurnHooks();
|
|
33
|
+
const result = await hooks.onUserPromptSubmitted();
|
|
34
|
+
assert.ok(result && typeof result === "object");
|
|
35
|
+
assert.match(result.additionalContext, /top of mind/);
|
|
36
|
+
assert.match(result.additionalContext, /<chapterhouse-memory>/);
|
|
256
37
|
});
|
|
257
38
|
//# sourceMappingURL=memory-coordinator.test.js.map
|