chapterhouse 0.5.1 → 0.6.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/.pr-types.json +14 -0
- package/README.md +6 -0
- package/dist/api/server.js +5 -3
- package/dist/cli.js +4 -2
- package/dist/config.js +75 -13
- package/dist/config.test.js +73 -0
- package/dist/copilot/memory-coordinator.js +234 -0
- package/dist/copilot/memory-coordinator.test.js +257 -0
- package/dist/copilot/orchestrator.js +31 -212
- package/dist/copilot/orchestrator.test.js +111 -0
- package/dist/copilot/pr-title.js +92 -0
- package/dist/copilot/pr-title.test.js +54 -0
- package/dist/copilot/router.js +43 -8
- package/dist/copilot/router.test.js +60 -18
- package/dist/copilot/threat-model.js +50 -0
- package/dist/copilot/threat-model.test.js +129 -0
- package/dist/copilot/tools.js +65 -39
- package/dist/copilot/tools.wiki.test.js +15 -6
- package/dist/daemon.js +7 -2
- package/dist/integrations/team-push.js +8 -1
- package/dist/integrations/teams-notify.js +8 -1
- package/dist/memory/housekeeping.js +73 -25
- package/dist/memory/housekeeping.test.js +95 -3
- package/dist/memory/inbox.test.js +178 -0
- package/dist/memory/tiering.test.js +323 -0
- package/dist/mode-context.js +28 -0
- package/dist/mode-context.test.js +42 -0
- package/dist/setup.js +162 -95
- package/dist/setup.test.js +139 -0
- package/dist/sprint-merge.js +168 -0
- package/dist/sprint-merge.test.js +131 -0
- package/dist/store/db.js +63 -0
- package/dist/store/db.test.js +279 -0
- package/dist/wiki/team-sync.js +8 -1
- package/package.json +6 -1
- package/web/dist/assets/{index-BfHqP3-C.js → index-B5oDsQ5y.js} +84 -84
- package/web/dist/assets/index-B5oDsQ5y.js.map +1 -0
- package/web/dist/assets/index-DknKAtDS.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BfHqP3-C.js.map +0 -1
- package/web/dist/assets/index-_O6AoWOS.css +0 -10
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
function makeScope(id, slug, title = slug, description = `${slug} scope`) {
|
|
4
|
+
return {
|
|
5
|
+
id,
|
|
6
|
+
slug,
|
|
7
|
+
title,
|
|
8
|
+
description,
|
|
9
|
+
keywords: [],
|
|
10
|
+
active: true,
|
|
11
|
+
createdAt: "2026-05-14T00:00:00.000Z",
|
|
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
|
+
});
|
|
148
|
+
});
|
|
149
|
+
test("onTurnComplete schedules checkpoint and housekeeping at the configured turn boundary", async (t) => {
|
|
150
|
+
const { module, state } = await loadMemoryCoordinatorModule(t);
|
|
151
|
+
const coordinator = new module.MemoryCoordinator({
|
|
152
|
+
getCopilotClient: () => ({ name: "mock-client" }),
|
|
153
|
+
config: state.config,
|
|
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);
|
|
172
|
+
});
|
|
173
|
+
test("onScopeChange triggers checkpoint extraction for the previous scope", async (t) => {
|
|
174
|
+
const { module, state } = await loadMemoryCoordinatorModule(t, {
|
|
175
|
+
config: {
|
|
176
|
+
copilotModel: "claude-sonnet-4.6",
|
|
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" });
|
|
200
|
+
});
|
|
201
|
+
test("onAgentTaskComplete runs the end-of-task memory hook with the mock client", async (t) => {
|
|
202
|
+
const { module, state } = await loadMemoryCoordinatorModule(t);
|
|
203
|
+
const client = { name: "mock-client" };
|
|
204
|
+
const coordinator = new module.MemoryCoordinator({
|
|
205
|
+
getCopilotClient: () => client,
|
|
206
|
+
config: state.config,
|
|
207
|
+
resolveScopeForSession: (sessionKey) => state.sessionScopes.get(sessionKey) ?? null,
|
|
208
|
+
});
|
|
209
|
+
await coordinator.onAgentTaskComplete("task-42", "Final delegated result");
|
|
210
|
+
assert.equal(state.eotCalls.length, 1);
|
|
211
|
+
assert.equal(state.eotCalls[0]?.taskId, "task-42");
|
|
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);
|
|
256
|
+
});
|
|
257
|
+
//# sourceMappingURL=memory-coordinator.test.js.map
|
|
@@ -3,13 +3,9 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { approveAll } from "@github/copilot-sdk";
|
|
4
4
|
import { createTools } from "./tools.js";
|
|
5
5
|
import { getOrchestratorSystemMessage } from "./system-message.js";
|
|
6
|
-
import { renderHotTierForActiveScope } from "../memory/hot-tier.js";
|
|
7
|
-
import { getHotTierEntries, renderHotTierXML } from "../memory/hot-tier.js";
|
|
8
6
|
import { getActiveScope, withActiveScope } from "../memory/active-scope.js";
|
|
9
7
|
import { getScope } from "../memory/scopes.js";
|
|
10
|
-
import {
|
|
11
|
-
import { isHousekeepingInFlight, runHousekeeping } from "../memory/housekeeping.js";
|
|
12
|
-
import { runEndOfTaskMemoryHook } from "../memory/eot.js";
|
|
8
|
+
import { MemoryCoordinator } from "./memory-coordinator.js";
|
|
13
9
|
import { CHAPTERHOUSE_VERSION } from "../version.js";
|
|
14
10
|
import { config, DEFAULT_MODEL } from "../config.js";
|
|
15
11
|
import { loadMcpConfig } from "./mcp-config.js";
|
|
@@ -77,160 +73,15 @@ let currentUserContext;
|
|
|
77
73
|
let currentAuthenticatedUser;
|
|
78
74
|
let currentAuthorizationHeader;
|
|
79
75
|
let lastRouteResult;
|
|
80
|
-
|
|
81
|
-
const checkpointTurnsBySession = new Map();
|
|
82
|
-
const housekeepingTurnsBySession = new Map();
|
|
83
|
-
const MAX_CHECKPOINT_CHARS_PER_SIDE = 4_000;
|
|
76
|
+
let memoryCoordinator;
|
|
84
77
|
export function getLastRouteResult() {
|
|
85
78
|
return lastRouteResult;
|
|
86
79
|
}
|
|
87
|
-
function truncateCheckpointText(value) {
|
|
88
|
-
const trimmed = value.trim();
|
|
89
|
-
if (trimmed.length <= MAX_CHECKPOINT_CHARS_PER_SIDE) {
|
|
90
|
-
return trimmed;
|
|
91
|
-
}
|
|
92
|
-
return `${trimmed.slice(0, MAX_CHECKPOINT_CHARS_PER_SIDE)}…`;
|
|
93
|
-
}
|
|
94
|
-
function getCheckpointTracker(sessionKey) {
|
|
95
|
-
let tracker = checkpointTrackers.get(sessionKey);
|
|
96
|
-
if (!tracker) {
|
|
97
|
-
tracker = new CheckpointTracker();
|
|
98
|
-
checkpointTrackers.set(sessionKey, tracker);
|
|
99
|
-
}
|
|
100
|
-
return tracker;
|
|
101
|
-
}
|
|
102
80
|
export function resetCheckpointSessionState(sessionKey) {
|
|
103
|
-
|
|
104
|
-
checkpointTurnsBySession.delete(sessionKey);
|
|
105
|
-
housekeepingTurnsBySession.delete(sessionKey);
|
|
106
|
-
}
|
|
107
|
-
function appendCheckpointTurn(sessionKey, turn) {
|
|
108
|
-
const turns = checkpointTurnsBySession.get(sessionKey) ?? [];
|
|
109
|
-
turns.push(turn);
|
|
110
|
-
const overflow = turns.length - config.memoryCheckpointTurns;
|
|
111
|
-
if (overflow > 0) {
|
|
112
|
-
turns.splice(0, overflow);
|
|
113
|
-
}
|
|
114
|
-
checkpointTurnsBySession.set(sessionKey, turns);
|
|
115
|
-
return turns;
|
|
116
|
-
}
|
|
117
|
-
function scheduleCheckpointExtraction(sessionKey, prompt, finalContent, source) {
|
|
118
|
-
if (source.type === "background") {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const tracker = getCheckpointTracker(sessionKey);
|
|
122
|
-
const turns = appendCheckpointTurn(sessionKey, {
|
|
123
|
-
user: truncateCheckpointText(prompt),
|
|
124
|
-
assistant: truncateCheckpointText(finalContent),
|
|
125
|
-
});
|
|
126
|
-
if (!config.memoryCheckpointEnabled) {
|
|
127
|
-
log.info({ sessionKey }, "memory.checkpoint.disabled");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
tracker.tickOrchestratorTurn();
|
|
131
|
-
if (!tracker.shouldFire()) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
tracker.markFired();
|
|
135
|
-
if (isCheckpointInFlight(sessionKey)) {
|
|
136
|
-
log.info({ sessionKey }, "memory.checkpoint.in_flight_skip");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (!copilotClient) {
|
|
140
|
-
log.error({ sessionKey }, "memory.checkpoint.error");
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const activeScope = getMemoryScopeForSession(sessionKey);
|
|
144
|
-
void runCheckpointExtraction({
|
|
145
|
-
sessionKey,
|
|
146
|
-
turns: turns.slice(-config.memoryCheckpointTurns),
|
|
147
|
-
activeScope,
|
|
148
|
-
copilotClient,
|
|
149
|
-
trigger: "cadence",
|
|
150
|
-
}).catch((error) => {
|
|
151
|
-
log.error({ err: error, sessionKey }, "memory.checkpoint.error");
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
function scheduleHousekeeping(sessionKey, source) {
|
|
155
|
-
if (source.type === "background") {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (!config.memoryHousekeepingEnabled) {
|
|
159
|
-
log.info({ sessionKey }, "memory.housekeeping.disabled");
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const turns = (housekeepingTurnsBySession.get(sessionKey) ?? 0) + 1;
|
|
163
|
-
if (turns < config.memoryHousekeepingTurns) {
|
|
164
|
-
housekeepingTurnsBySession.set(sessionKey, turns);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
housekeepingTurnsBySession.set(sessionKey, 0);
|
|
168
|
-
const activeScope = getMemoryScopeForSession(sessionKey);
|
|
169
|
-
if (!activeScope) {
|
|
170
|
-
log.info({ sessionKey }, "memory.housekeeping.no_active_scope");
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const scopeIds = [activeScope.id];
|
|
174
|
-
if (isHousekeepingInFlight(scopeIds)) {
|
|
175
|
-
log.info({ sessionKey, scope_ids: scopeIds }, "memory.housekeeping.in_flight_skip");
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
void runHousekeeping({ scopeIds });
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
log.error({ err: error, sessionKey, scope_ids: scopeIds }, "memory.housekeeping.error");
|
|
183
|
-
}
|
|
81
|
+
memoryCoordinator?.reset(sessionKey);
|
|
184
82
|
}
|
|
185
83
|
export function maybeScheduleScopeChangeCheckpoint(sessionKey, previousScope, nextScope) {
|
|
186
|
-
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (!config.memoryCheckpointOnScopeChange) {
|
|
190
|
-
log.info({ sessionKey, scope: previousScope.slug }, "memory.checkpoint.scope_change_disabled");
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
const tracker = getCheckpointTracker(sessionKey);
|
|
194
|
-
const turnsSinceLast = tracker.turnsSinceLastFire();
|
|
195
|
-
if (turnsSinceLast < config.memoryCheckpointMinTurnsForScopeFire) {
|
|
196
|
-
log.info({
|
|
197
|
-
sessionKey,
|
|
198
|
-
scope: previousScope.slug,
|
|
199
|
-
turns_since_last: turnsSinceLast,
|
|
200
|
-
min_required: config.memoryCheckpointMinTurnsForScopeFire,
|
|
201
|
-
}, "memory.checkpoint.scope_change_skip");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (isCheckpointInFlight(sessionKey)) {
|
|
205
|
-
log.info({ sessionKey, trigger: "scope_change" }, "memory.checkpoint.in_flight_skip");
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (!copilotClient) {
|
|
209
|
-
log.error({ sessionKey }, "memory.checkpoint.error");
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const turns = checkpointTurnsBySession.get(sessionKey) ?? [];
|
|
213
|
-
if (turns.length === 0) {
|
|
214
|
-
log.info({
|
|
215
|
-
sessionKey,
|
|
216
|
-
scope: previousScope.slug,
|
|
217
|
-
turns_since_last: turnsSinceLast,
|
|
218
|
-
min_required: config.memoryCheckpointMinTurnsForScopeFire,
|
|
219
|
-
}, "memory.checkpoint.scope_change_skip");
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
tracker.markScopeChangeFire();
|
|
223
|
-
void runCheckpointExtraction({
|
|
224
|
-
sessionKey,
|
|
225
|
-
turns: turns.slice(-config.memoryCheckpointTurns),
|
|
226
|
-
activeScope: previousScope,
|
|
227
|
-
copilotClient,
|
|
228
|
-
trigger: "scope_change",
|
|
229
|
-
scopeChangeContext: {
|
|
230
|
-
from: previousScope.slug,
|
|
231
|
-
to: nextScope?.slug ?? "no active scope",
|
|
232
|
-
},
|
|
233
|
-
}).catch((error) => {
|
|
84
|
+
void memoryCoordinator?.onScopeChange(sessionKey, previousScope?.slug ?? "", nextScope?.slug ?? "").catch((error) => {
|
|
234
85
|
log.error({ err: error, sessionKey }, "memory.checkpoint.error");
|
|
235
86
|
});
|
|
236
87
|
}
|
|
@@ -342,39 +193,11 @@ function getMemoryScopeForSession(sessionKey) {
|
|
|
342
193
|
}
|
|
343
194
|
return getActiveScope();
|
|
344
195
|
}
|
|
345
|
-
function
|
|
346
|
-
if (!config.memoryInjectEnabled || !scope) {
|
|
347
|
-
return undefined;
|
|
348
|
-
}
|
|
349
|
-
const hotTierXml = renderHotTierXML(getHotTierEntries(scope.id));
|
|
350
|
-
return hotTierXml ? hotTierXml.trimEnd() : undefined;
|
|
351
|
-
}
|
|
352
|
-
function buildHotTierContext() {
|
|
353
|
-
if (!config.memoryInjectEnabled) {
|
|
354
|
-
return undefined;
|
|
355
|
-
}
|
|
356
|
-
const hotTierXml = renderHotTierForActiveScope();
|
|
357
|
-
if (!hotTierXml) {
|
|
358
|
-
return undefined;
|
|
359
|
-
}
|
|
360
|
-
return hotTierXml.trimEnd();
|
|
361
|
-
}
|
|
362
|
-
function buildPerTurnMemoryHooks(sessionKey) {
|
|
363
|
-
if (!config.memoryInjectEnabled) {
|
|
364
|
-
return undefined;
|
|
365
|
-
}
|
|
366
|
-
return {
|
|
367
|
-
onUserPromptSubmitted: () => {
|
|
368
|
-
const hotTierXml = buildScopedHotTierContext(getMemoryScopeForSession(sessionKey));
|
|
369
|
-
return hotTierXml ? { additionalContext: hotTierXml } : undefined;
|
|
370
|
-
},
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
function getSystemMessageOptions(memorySummary) {
|
|
196
|
+
function getSystemMessageOptions(memorySummary, hotTierXml) {
|
|
374
197
|
return {
|
|
375
198
|
selfEditEnabled: config.selfEditEnabled,
|
|
376
199
|
memorySummary: memorySummary || undefined,
|
|
377
|
-
hotTierXml
|
|
200
|
+
hotTierXml,
|
|
378
201
|
agentRoster: buildAgentRoster(),
|
|
379
202
|
userContext: currentUserContext,
|
|
380
203
|
};
|
|
@@ -410,15 +233,9 @@ function updateRequestContext(source) {
|
|
|
410
233
|
}
|
|
411
234
|
}
|
|
412
235
|
export function feedAgentResult(taskId, agentSlug, result) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
finalResult: result,
|
|
417
|
-
copilotClient,
|
|
418
|
-
}).catch((error) => {
|
|
419
|
-
log.error({ err: error, taskId }, "memory.eot.error");
|
|
420
|
-
});
|
|
421
|
-
}
|
|
236
|
+
void memoryCoordinator?.onAgentTaskComplete(taskId, result).catch((error) => {
|
|
237
|
+
log.error({ err: error, taskId }, "memory.eot.error");
|
|
238
|
+
});
|
|
422
239
|
const sessionKey = getTaskSessionKey(taskId) || "default";
|
|
423
240
|
const agentTurnId = randomUUID();
|
|
424
241
|
const agentDisplayName = getAgentRegistry().find((agent) => agent.slug === agentSlug)?.name ?? agentSlug;
|
|
@@ -512,13 +329,12 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
512
329
|
let { tools, mcpServers, skillDirectories } = baseConfig;
|
|
513
330
|
const isProjectSession = sessionKey.startsWith("project:");
|
|
514
331
|
const persistentAgent = getPersistentAgentForSessionKey(sessionKey);
|
|
515
|
-
const agentScope = persistentAgent?.scope ? getScope(persistentAgent.scope) ?? null : null;
|
|
516
332
|
const infiniteSessions = {
|
|
517
333
|
enabled: true,
|
|
518
334
|
backgroundCompactionThreshold: 0.80,
|
|
519
335
|
bufferExhaustionThreshold: 0.95,
|
|
520
336
|
};
|
|
521
|
-
const memoryHooks =
|
|
337
|
+
const memoryHooks = memoryCoordinator?.buildPerTurnHooks(sessionKey);
|
|
522
338
|
let model = config.copilotModel;
|
|
523
339
|
let systemMessageContent;
|
|
524
340
|
let sessionMode = isProjectSession ? "project" : "default";
|
|
@@ -528,7 +344,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
528
344
|
client: copilotClient,
|
|
529
345
|
onAgentTaskComplete: feedAgentResult,
|
|
530
346
|
})));
|
|
531
|
-
const scopedHotTier =
|
|
347
|
+
const scopedHotTier = (await memoryCoordinator?.buildHotTierContext(sessionKey)) || undefined;
|
|
532
348
|
const channelNote = `You are in your persistent Chapterhouse channel (${sessionKey}). Your memory scope is ${persistentAgent.scope}.`;
|
|
533
349
|
systemMessageContent = [
|
|
534
350
|
composeAgentSystemMessage(persistentAgent),
|
|
@@ -539,8 +355,9 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
539
355
|
}
|
|
540
356
|
else {
|
|
541
357
|
const memorySummary = getWikiSummary();
|
|
358
|
+
const hotTierXml = (await memoryCoordinator?.buildHotTierContext(sessionKey)) || undefined;
|
|
542
359
|
systemMessageContent = getOrchestratorSystemMessage({
|
|
543
|
-
...getSystemMessageOptions(memorySummary),
|
|
360
|
+
...getSystemMessageOptions(memorySummary, hotTierXml),
|
|
544
361
|
version: CHAPTERHOUSE_VERSION,
|
|
545
362
|
});
|
|
546
363
|
}
|
|
@@ -562,7 +379,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
562
379
|
infiniteSessions,
|
|
563
380
|
});
|
|
564
381
|
log.info({ sessionKey }, "Session resumed successfully");
|
|
565
|
-
|
|
382
|
+
memoryCoordinator?.reset(sessionKey);
|
|
566
383
|
upsertCopilotSession(sessionKey, sessionMode, session.sessionId, projectRoot, model);
|
|
567
384
|
const mgr = registry?.get(sessionKey);
|
|
568
385
|
if (mgr)
|
|
@@ -589,7 +406,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
589
406
|
infiniteSessions,
|
|
590
407
|
});
|
|
591
408
|
log.info({ sessionKey, sessionId: session.sessionId.slice(0, 8) }, "Session created");
|
|
592
|
-
|
|
409
|
+
memoryCoordinator?.reset(sessionKey);
|
|
593
410
|
upsertCopilotSession(sessionKey, sessionMode, session.sessionId, projectRoot, model);
|
|
594
411
|
if (sessionKey === "default")
|
|
595
412
|
setState(ORCHESTRATOR_SESSION_KEY, session.sessionId);
|
|
@@ -600,6 +417,12 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
600
417
|
}
|
|
601
418
|
export async function initOrchestrator(client) {
|
|
602
419
|
copilotClient = client;
|
|
420
|
+
memoryCoordinator?.shutdown();
|
|
421
|
+
memoryCoordinator = new MemoryCoordinator({
|
|
422
|
+
getCopilotClient: () => copilotClient,
|
|
423
|
+
config,
|
|
424
|
+
resolveScopeForSession: getMemoryScopeForSession,
|
|
425
|
+
});
|
|
603
426
|
// Initialize per-task ring buffer — subscribes to agentEventBus for session:tool_call events.
|
|
604
427
|
initTaskEventLog();
|
|
605
428
|
// (Re-)create the registry — supports multiple initOrchestrator calls in tests
|
|
@@ -896,12 +719,8 @@ async function executeOnSession(manager, item) {
|
|
|
896
719
|
spawnArgsMap.delete(taskId);
|
|
897
720
|
activeSubagentTaskIds.delete(taskId);
|
|
898
721
|
db.prepare(`UPDATE agent_tasks SET status = 'completed', result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(finalResult?.slice(0, 10000) ?? null, taskId);
|
|
899
|
-
if (
|
|
900
|
-
void
|
|
901
|
-
taskId,
|
|
902
|
-
finalResult,
|
|
903
|
-
copilotClient,
|
|
904
|
-
}).catch((error) => {
|
|
722
|
+
if (finalResult) {
|
|
723
|
+
void memoryCoordinator?.onAgentTaskComplete(taskId, finalResult).catch((error) => {
|
|
905
724
|
log.error({ err: error, taskId }, "memory.eot.error");
|
|
906
725
|
});
|
|
907
726
|
}
|
|
@@ -1252,8 +1071,9 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1252
1071
|
logConversation("assistant", finalContent, logSource, sessionKey, { turnId });
|
|
1253
1072
|
}
|
|
1254
1073
|
catch { /* best-effort */ }
|
|
1255
|
-
|
|
1256
|
-
|
|
1074
|
+
void memoryCoordinator?.onTurnComplete(sessionKey, prompt, finalContent, source.type).catch((error) => {
|
|
1075
|
+
log.error({ err: error, sessionKey }, "memory.turn_complete.error");
|
|
1076
|
+
});
|
|
1257
1077
|
if (copilotClient) {
|
|
1258
1078
|
maybeWriteEpisode(copilotClient).catch((err) => {
|
|
1259
1079
|
log.error({ err: err instanceof Error ? err.message : err }, "Episode write failed (non-fatal)");
|
|
@@ -1353,8 +1173,9 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
1353
1173
|
logConversation("assistant", finalContent, sourceLabel, sessionKey, { turnId });
|
|
1354
1174
|
}
|
|
1355
1175
|
catch { /* best-effort */ }
|
|
1356
|
-
|
|
1357
|
-
|
|
1176
|
+
void memoryCoordinator?.onTurnComplete(sessionKey, newPrompt, finalContent, source.type).catch((error) => {
|
|
1177
|
+
log.error({ err: error, sessionKey }, "memory.turn_complete.error");
|
|
1178
|
+
});
|
|
1358
1179
|
if (copilotClient) {
|
|
1359
1180
|
maybeWriteEpisode(copilotClient).catch((err) => {
|
|
1360
1181
|
log.error({ err: err instanceof Error ? err.message : err }, "Episode write failed (non-fatal)");
|
|
@@ -1494,15 +1315,13 @@ export function getAgentInfo() {
|
|
|
1494
1315
|
}
|
|
1495
1316
|
/** Clean up on shutdown/restart. */
|
|
1496
1317
|
export async function shutdownAgents() {
|
|
1318
|
+
memoryCoordinator?.shutdown();
|
|
1319
|
+
memoryCoordinator = undefined;
|
|
1497
1320
|
if (!registry) {
|
|
1498
|
-
checkpointTrackers.clear();
|
|
1499
|
-
checkpointTurnsBySession.clear();
|
|
1500
1321
|
await clearActiveTasks();
|
|
1501
1322
|
return;
|
|
1502
1323
|
}
|
|
1503
1324
|
await registry.shutdown();
|
|
1504
|
-
checkpointTrackers.clear();
|
|
1505
|
-
checkpointTurnsBySession.clear();
|
|
1506
1325
|
await clearActiveTasks();
|
|
1507
1326
|
}
|
|
1508
1327
|
//# sourceMappingURL=orchestrator.js.map
|