chapterhouse 0.5.2 → 0.7.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.
Files changed (40) hide show
  1. package/.pr-types.json +14 -0
  2. package/README.md +6 -0
  3. package/dist/api/agent-edit-access.js +11 -0
  4. package/dist/api/agents.api.test.js +48 -0
  5. package/dist/api/server.js +182 -11
  6. package/dist/api/server.test.js +334 -3
  7. package/dist/config.test.js +29 -0
  8. package/dist/copilot/agent-event-bus.js +1 -0
  9. package/dist/copilot/agents.js +114 -46
  10. package/dist/copilot/agents.mcp-servers.test.js +87 -0
  11. package/dist/copilot/agents.parse.test.js +69 -0
  12. package/dist/copilot/agents.test.js +125 -1
  13. package/dist/copilot/memory-coordinator.js +234 -0
  14. package/dist/copilot/memory-coordinator.test.js +257 -0
  15. package/dist/copilot/orchestrator.js +81 -221
  16. package/dist/copilot/orchestrator.test.js +238 -1
  17. package/dist/copilot/pr-title.js +92 -0
  18. package/dist/copilot/pr-title.test.js +54 -0
  19. package/dist/copilot/router.test.js +30 -0
  20. package/dist/copilot/session-manager.js +34 -0
  21. package/dist/copilot/threat-model.js +50 -0
  22. package/dist/copilot/threat-model.test.js +129 -0
  23. package/dist/copilot/tools.js +61 -37
  24. package/dist/copilot/tools.wiki.test.js +15 -6
  25. package/dist/setup.js +15 -5
  26. package/dist/setup.test.js +20 -3
  27. package/dist/sprint-merge.js +168 -0
  28. package/dist/sprint-merge.test.js +131 -0
  29. package/dist/store/db.js +63 -0
  30. package/dist/store/db.test.js +279 -0
  31. package/dist/test/setup-env.js +2 -1
  32. package/dist/test/setup-env.test.js +8 -1
  33. package/package.json +8 -1
  34. package/web/dist/assets/index-DuKYxMIR.css +10 -0
  35. package/web/dist/assets/index-DytB69KC.js +223 -0
  36. package/web/dist/assets/index-DytB69KC.js.map +1 -0
  37. package/web/dist/index.html +2 -2
  38. package/web/dist/assets/index-CPaILy2j.js +0 -223
  39. package/web/dist/assets/index-CPaILy2j.js.map +0 -1
  40. package/web/dist/assets/index-Cs7AGeaL.css +0 -10
@@ -0,0 +1,87 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ const MCP_SERVERS = {
7
+ filesystem: { command: "filesystem" },
8
+ truenas: { command: "truenas" },
9
+ };
10
+ async function loadIsolatedAgentsModule(t, agentFrontmatter) {
11
+ const root = mkdtempSync(join(tmpdir(), "chapterhouse-agents-mcp-"));
12
+ const agentsDir = join(root, "agents");
13
+ const sessionsDir = join(root, "sessions");
14
+ mkdirSync(agentsDir, { recursive: true });
15
+ mkdirSync(sessionsDir, { recursive: true });
16
+ writeFileSync(join(agentsDir, "mcp-test-agent.agent.md"), [
17
+ "---",
18
+ "name: MCP Test Agent",
19
+ "description: Agent used to verify MCP session config",
20
+ "model: claude-sonnet-4.6",
21
+ agentFrontmatter,
22
+ "---",
23
+ "",
24
+ "You are an MCP test agent.",
25
+ ].filter(Boolean).join("\n"));
26
+ t.after(() => rmSync(root, { recursive: true, force: true }));
27
+ t.mock.module("../paths.js", {
28
+ namedExports: {
29
+ AGENTS_DIR: agentsDir,
30
+ SESSIONS_DIR: sessionsDir,
31
+ },
32
+ });
33
+ t.mock.module("./mcp-config.js", {
34
+ namedExports: {
35
+ loadMcpConfig: () => MCP_SERVERS,
36
+ },
37
+ });
38
+ t.mock.module("./skills.js", {
39
+ namedExports: {
40
+ getSkillDirectories: () => [],
41
+ },
42
+ });
43
+ t.mock.module("../store/db.js", {
44
+ namedExports: {
45
+ getState: () => undefined,
46
+ setState: () => undefined,
47
+ },
48
+ });
49
+ let createSessionOptions;
50
+ const agentsModule = await import(new URL(`./agents.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
51
+ agentsModule.loadAgents();
52
+ return {
53
+ agentsModule,
54
+ get createSessionOptions() {
55
+ return createSessionOptions;
56
+ },
57
+ set createSessionOptions(value) {
58
+ createSessionOptions = value;
59
+ },
60
+ };
61
+ }
62
+ async function createAgentSession(agentsModule, setCreateSessionOptions) {
63
+ const client = {
64
+ createSession: async (options) => {
65
+ setCreateSessionOptions(options);
66
+ return {};
67
+ },
68
+ };
69
+ await agentsModule.createEphemeralAgentSession("mcp-test-agent", client, []);
70
+ }
71
+ test("createEphemeralAgentSession only passes MCP servers listed by the agent allowlist", async (t) => {
72
+ const context = await loadIsolatedAgentsModule(t, "mcpServers: [truenas]");
73
+ await createAgentSession(context.agentsModule, (options) => {
74
+ context.createSessionOptions = options;
75
+ });
76
+ assert.deepEqual(context.createSessionOptions?.mcpServers, {
77
+ truenas: { command: "truenas" },
78
+ });
79
+ });
80
+ test("createEphemeralAgentSession passes all MCP servers when the agent has no allowlist", async (t) => {
81
+ const context = await loadIsolatedAgentsModule(t, "");
82
+ await createAgentSession(context.agentsModule, (options) => {
83
+ context.createSessionOptions = options;
84
+ });
85
+ assert.deepEqual(context.createSessionOptions?.mcpServers, MCP_SERVERS);
86
+ });
87
+ //# sourceMappingURL=agents.mcp-servers.test.js.map
@@ -0,0 +1,69 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { parseAgentMd } from "./agents.js";
4
+ test("parseAgentMd parses block-style YAML arrays", () => {
5
+ const agent = parseAgentMd([
6
+ "---",
7
+ "name: Designer",
8
+ "description: Handles UI flows",
9
+ "model: claude-sonnet-4.6",
10
+ "skills:",
11
+ " - frontend-design",
12
+ " - ux-copy",
13
+ "tools:",
14
+ " - read",
15
+ " - write",
16
+ "---",
17
+ "",
18
+ "You are Designer.",
19
+ ].join("\n"), "designer");
20
+ assert.ok(agent, "agent charter should still parse");
21
+ assert.deepEqual(agent.skills, ["frontend-design", "ux-copy"]);
22
+ assert.deepEqual(agent.tools, ["read", "write"]);
23
+ });
24
+ test("parseAgentMd still parses inline YAML arrays", () => {
25
+ const agent = parseAgentMd([
26
+ "---",
27
+ "name: Designer",
28
+ "description: Handles UI flows",
29
+ "model: claude-sonnet-4.6",
30
+ "skills: [frontend-design, ux-copy]",
31
+ "tools: [read, write]",
32
+ "---",
33
+ "",
34
+ "You are Designer.",
35
+ ].join("\n"), "designer");
36
+ assert.ok(agent, "agent charter should parse");
37
+ assert.deepEqual(agent.skills, ["frontend-design", "ux-copy"]);
38
+ assert.deepEqual(agent.tools, ["read", "write"]);
39
+ });
40
+ test("parseAgentMd tolerates missing optional fields", () => {
41
+ const agent = parseAgentMd([
42
+ "---",
43
+ "name: Designer",
44
+ "description: Handles UI flows",
45
+ "model: claude-sonnet-4.6",
46
+ "---",
47
+ "",
48
+ "You are Designer.",
49
+ ].join("\n"), "designer");
50
+ assert.ok(agent, "agent charter should parse");
51
+ assert.equal(agent.skills, undefined);
52
+ assert.equal(agent.tools, undefined);
53
+ assert.equal(agent.mcpServers, undefined);
54
+ assert.equal(agent.allowedPaths, undefined);
55
+ });
56
+ test("parseAgentMd returns null for invalid YAML frontmatter", () => {
57
+ const agent = parseAgentMd([
58
+ "---",
59
+ "name: Designer",
60
+ "description: Handles UI flows",
61
+ "model: claude-sonnet-4.6",
62
+ "skills: [frontend-design",
63
+ "---",
64
+ "",
65
+ "You are Designer.",
66
+ ].join("\n"), "designer");
67
+ assert.equal(agent, null);
68
+ });
69
+ //# sourceMappingURL=agents.parse.test.js.map
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { composeAgentSystemMessage, filterToolsForAgent, bindToolsToAgent, getCurrentToolTaskId, parseAgentMd, withToolTaskContext, } from "./agents.js";
3
+ import { composeAgentSystemMessage, filterToolsForAgent, bindToolsToAgent, getCurrentToolTaskId, getAgentRegistry, parseAgentMd, withToolTaskContext, } from "./agents.js";
4
4
  function makeAgent(slug) {
5
5
  return {
6
6
  slug,
@@ -76,6 +76,22 @@ test("parseAgentMd detects persistent agent scope from charter frontmatter", ()
76
76
  assert.equal(agent.persistent, true);
77
77
  assert.equal(agent.scope, "infra");
78
78
  });
79
+ test("parseAgentMd preserves block-style skills arrays from charter frontmatter", () => {
80
+ const agent = parseAgentMd([
81
+ "---",
82
+ "name: Designer",
83
+ "description: UI/UX design specialist",
84
+ "model: claude-opus-4.6",
85
+ "skills:",
86
+ " - frontend-design",
87
+ " - accessibility-review",
88
+ "---",
89
+ "",
90
+ "You are the Designer.",
91
+ ].join("\n"), "designer");
92
+ assert.ok(agent, "agent charter should parse");
93
+ assert.deepEqual(agent.skills, ["frontend-design", "accessibility-review"]);
94
+ });
79
95
  test("persistent agents cannot receive scope-changing management tools", () => {
80
96
  const agent = {
81
97
  ...makeAgent("bellonda"),
@@ -102,4 +118,112 @@ test("bindToolsToAgent uses the per-turn task context when no fixed task id is p
102
118
  const taskId = await withToolTaskContext("delegated-persistent-001", () => tools[0].handler({}, {}));
103
119
  assert.equal(taskId, "delegated-persistent-001");
104
120
  });
121
+ test("notifyAgentSaved uses the provided charter instead of rereading from CHAPTERHOUSE_HOME", async () => {
122
+ const slug = "notify-agent-saved-inline-charter";
123
+ const agents = await import("./agents.js");
124
+ const config = {
125
+ ...makeAgent(slug),
126
+ persistent: true,
127
+ scope: "infra",
128
+ };
129
+ agents.setAgentSaveRuntimeHooks({
130
+ reloadPersistentSession: async () => "none",
131
+ });
132
+ try {
133
+ await agents.notifyAgentSaved(slug, config);
134
+ }
135
+ finally {
136
+ agents.resetAgentSaveRuntimeHooks();
137
+ }
138
+ const saved = getAgentRegistry().find((entry) => entry.slug === slug);
139
+ assert.deepEqual(saved, config);
140
+ });
141
+ test("notifyAgentSaved reloads idle persistent sessions and emits a restart event", async () => {
142
+ const events = [];
143
+ const reloads = [];
144
+ const agents = await import("./agents.js");
145
+ const config = {
146
+ ...makeAgent("bellonda"),
147
+ persistent: true,
148
+ scope: "infra",
149
+ };
150
+ agents.setAgentSaveRuntimeHooks({
151
+ reloadPersistentSession: async (slug) => {
152
+ reloads.push(slug);
153
+ return "reloaded";
154
+ },
155
+ emitAgentReloadEvent: (event) => {
156
+ events.push(event);
157
+ },
158
+ });
159
+ try {
160
+ await agents.notifyAgentSaved("bellonda", config);
161
+ }
162
+ finally {
163
+ agents.resetAgentSaveRuntimeHooks();
164
+ }
165
+ assert.deepEqual(reloads, ["bellonda"]);
166
+ assert.deepEqual(events, [{ type: "agent_reloaded", slug: "bellonda", reason: "session_restart" }]);
167
+ });
168
+ test("notifyAgentSaved marks in-flight persistent sessions as pending and emits restart after the deferred reload", async () => {
169
+ const events = [];
170
+ const reloads = [];
171
+ const agents = await import("./agents.js");
172
+ const config = {
173
+ ...makeAgent("bellonda"),
174
+ persistent: true,
175
+ scope: "infra",
176
+ };
177
+ let finishReload;
178
+ agents.setAgentSaveRuntimeHooks({
179
+ reloadPersistentSession: async (slug, onReloaded) => {
180
+ reloads.push(slug);
181
+ finishReload = onReloaded;
182
+ return "scheduled";
183
+ },
184
+ emitAgentReloadEvent: (event) => {
185
+ events.push(event);
186
+ },
187
+ });
188
+ try {
189
+ await agents.notifyAgentSaved("bellonda", config);
190
+ assert.deepEqual(events, [{ type: "agent_reload_pending", slug: "bellonda", reason: "in_flight" }]);
191
+ finishReload?.();
192
+ }
193
+ finally {
194
+ agents.resetAgentSaveRuntimeHooks();
195
+ }
196
+ assert.deepEqual(reloads, ["bellonda"]);
197
+ assert.deepEqual(events, [
198
+ { type: "agent_reload_pending", slug: "bellonda", reason: "in_flight" },
199
+ { type: "agent_reloaded", slug: "bellonda", reason: "session_restart" },
200
+ ]);
201
+ });
202
+ test("notifyAgentSaved leaves agents without open persistent sessions alone", async () => {
203
+ const events = [];
204
+ const reloads = [];
205
+ const agents = await import("./agents.js");
206
+ const config = {
207
+ ...makeAgent("bellonda"),
208
+ persistent: true,
209
+ scope: "infra",
210
+ };
211
+ agents.setAgentSaveRuntimeHooks({
212
+ reloadPersistentSession: async (slug) => {
213
+ reloads.push(slug);
214
+ return "none";
215
+ },
216
+ emitAgentReloadEvent: (event) => {
217
+ events.push(event);
218
+ },
219
+ });
220
+ try {
221
+ await agents.notifyAgentSaved("bellonda", config);
222
+ }
223
+ finally {
224
+ agents.resetAgentSaveRuntimeHooks();
225
+ }
226
+ assert.deepEqual(reloads, ["bellonda"]);
227
+ assert.deepEqual(events, []);
228
+ });
105
229
  //# sourceMappingURL=agents.test.js.map
@@ -0,0 +1,234 @@
1
+ import { getActiveScope } from "../memory/active-scope.js";
2
+ import { CheckpointTracker, isCheckpointInFlight, runCheckpointExtraction } from "../memory/checkpoint.js";
3
+ import { runEndOfTaskMemoryHook } from "../memory/eot.js";
4
+ import { getHotTierEntries, renderHotTierForActiveScope, renderHotTierXML } from "../memory/hot-tier.js";
5
+ import { isHousekeepingInFlight, runHousekeeping } from "../memory/housekeeping.js";
6
+ import { getScope } from "../memory/scopes.js";
7
+ import { config as defaultConfig } from "../config.js";
8
+ import { childLogger } from "../util/logger.js";
9
+ const log = childLogger("memory-coordinator");
10
+ const MAX_CHECKPOINT_CHARS_PER_SIDE = 4_000;
11
+ export class MemoryCoordinator {
12
+ checkpointTrackers = new Map();
13
+ checkpointTurnsBySession = new Map();
14
+ housekeepingTurnsBySession = new Map();
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 = {
109
+ onUserPromptSubmitted: async () => {
110
+ const hotTierXml = await this.buildHotTierContext(sessionKey);
111
+ return hotTierXml ? { additionalContext: hotTierXml } : undefined;
112
+ },
113
+ };
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
+ }
233
+ }
234
+ //# sourceMappingURL=memory-coordinator.js.map