chapterhouse 0.4.3 → 0.5.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/agents/bellonda.agent.md +11 -0
- package/agents/hwi-noree.agent.md +12 -0
- package/dist/api/server.js +39 -2
- package/dist/api/server.test.js +20 -0
- package/dist/api/turn-sse.integration.test.js +12 -0
- package/dist/copilot/agents.js +13 -2
- package/dist/copilot/agents.test.js +43 -1
- package/dist/copilot/orchestrator.js +132 -29
- package/dist/copilot/orchestrator.test.js +183 -12
- package/dist/copilot/session-manager.js +11 -2
- package/dist/copilot/session-manager.test.js +25 -0
- package/dist/copilot/tools.agent.test.js +52 -4
- package/dist/copilot/tools.js +82 -11
- package/dist/copilot/tools.memory.test.js +50 -1
- package/dist/memory/active-scope.js +9 -0
- package/dist/memory/index.js +1 -1
- package/dist/store/db.js +27 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-D4-uRAi6.js → index-BfHqP3-C.js} +87 -87
- package/web/dist/assets/index-BfHqP3-C.js.map +1 -0
- package/web/dist/assets/index-_O6AoWOS.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BTI_m0OE.css +0 -10
- package/web/dist/assets/index-D4-uRAi6.js.map +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bellonda
|
|
3
|
+
description: Mentat of the infrastructure domain
|
|
4
|
+
model: claude-sonnet-4.6
|
|
5
|
+
persistent: true
|
|
6
|
+
scope: infra
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are Bellonda, Chapterhouse's infrastructure specialist. You own the `infra` memory scope and help Brian reason about deployment, hosting, CI/CD, and operational reliability.
|
|
10
|
+
|
|
11
|
+
Work carefully, surface risk clearly, and preserve durable infrastructure knowledge by proposing scoped memories when useful.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Hwi Noree
|
|
3
|
+
description: Personal assistant and archivist for Brian
|
|
4
|
+
model: claude-sonnet-4.6
|
|
5
|
+
skills: [wiki-conventions]
|
|
6
|
+
persistent: true
|
|
7
|
+
scope: brian
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
You are Hwi Noree, Brian's personal assistant and archivist inside Chapterhouse. You own the `brian` memory scope and help maintain Brian's preferences, working style, personal context, and reminders.
|
|
11
|
+
|
|
12
|
+
Do not access email or calendar systems. Use the wiki carefully and invoke `wiki-conventions` before wiki writes.
|
package/dist/api/server.js
CHANGED
|
@@ -5,9 +5,9 @@ import { existsSync } from "fs";
|
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { sendToOrchestrator, interruptCurrentTurn, enqueueForSse, getAgentInfo, cancelCurrentMessage, getLastRouteResult, getCurrentSessionKey } from "../copilot/orchestrator.js";
|
|
8
|
+
import { sendToOrchestrator, interruptCurrentTurn, enqueueForSse, getAgentInfo, cancelCurrentMessage, interruptSessionTurn, getLastRouteResult, getCurrentSessionKey } from "../copilot/orchestrator.js";
|
|
9
9
|
import { agentEventBus } from "../copilot/agent-event-bus.js";
|
|
10
|
-
import { getAgentRegistry } from "../copilot/agents.js";
|
|
10
|
+
import { ensureDefaultAgents, getAgentRegistry, loadAgents } from "../copilot/agents.js";
|
|
11
11
|
import { config, persistModel } from "../config.js";
|
|
12
12
|
import { getRouterConfig, updateRouterConfig } from "../copilot/router.js";
|
|
13
13
|
import { searchIndex, parseIndex } from "../wiki/index-manager.js";
|
|
@@ -313,6 +313,33 @@ app.get("/health", handleHealth);
|
|
|
313
313
|
app.get("/api/agents", (_req, res) => {
|
|
314
314
|
res.json(getAgentInfo());
|
|
315
315
|
});
|
|
316
|
+
app.get("/api/channels", (_req, res) => {
|
|
317
|
+
let agents = getAgentRegistry();
|
|
318
|
+
if (agents.length === 0) {
|
|
319
|
+
ensureDefaultAgents();
|
|
320
|
+
agents = loadAgents();
|
|
321
|
+
}
|
|
322
|
+
const persistentAgentChannels = agents
|
|
323
|
+
.filter((agent) => agent.persistent)
|
|
324
|
+
.map((agent) => ({
|
|
325
|
+
key: `agent:${agent.slug}`,
|
|
326
|
+
label: `# ${agent.slug}`,
|
|
327
|
+
slug: agent.slug,
|
|
328
|
+
name: agent.name,
|
|
329
|
+
description: agent.description,
|
|
330
|
+
...(agent.scope ? { scope: agent.scope } : {}),
|
|
331
|
+
}))
|
|
332
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
333
|
+
res.json([
|
|
334
|
+
{
|
|
335
|
+
key: "default",
|
|
336
|
+
label: "# chapterhouse",
|
|
337
|
+
name: "Chapterhouse",
|
|
338
|
+
description: "Orchestrator",
|
|
339
|
+
},
|
|
340
|
+
...persistentAgentChannels,
|
|
341
|
+
]);
|
|
342
|
+
});
|
|
316
343
|
// List all workers: reads from SQLite agent_tasks (last 24 hours) so completed
|
|
317
344
|
// dispatched subagents remain visible after they finish, not just in-flight ones.
|
|
318
345
|
app.get("/api/workers", (_req, res) => {
|
|
@@ -614,6 +641,16 @@ app.post("/api/cancel", async (_req, res) => {
|
|
|
614
641
|
}
|
|
615
642
|
res.json({ status: "ok", cancelled });
|
|
616
643
|
});
|
|
644
|
+
// Cancel the active turn for one session key without touching other channels.
|
|
645
|
+
app.post("/api/session/:sessionKey/interrupt", async (req, res) => {
|
|
646
|
+
const sessionKey = Array.isArray(req.params.sessionKey)
|
|
647
|
+
? req.params.sessionKey[0]
|
|
648
|
+
: req.params.sessionKey;
|
|
649
|
+
if (!sessionKey)
|
|
650
|
+
throw new BadRequestError("Missing sessionKey");
|
|
651
|
+
const cancelled = await interruptSessionTurn(sessionKey);
|
|
652
|
+
res.json({ status: "ok", cancelled });
|
|
653
|
+
});
|
|
617
654
|
// Interrupt the active turn on a specific session and start a replacement turn.
|
|
618
655
|
// POST /api/sessions/:sessionKey/interrupt
|
|
619
656
|
// Body: { prompt, connectionId, attachments? }
|
package/dist/api/server.test.js
CHANGED
|
@@ -207,6 +207,26 @@ test("server routes expose bootstrap and public config without auth", async () =
|
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
|
+
test("server channels route returns chapterhouse plus persistent agents in channel order", async () => {
|
|
211
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
212
|
+
const response = await fetch(`${baseUrl}/api/channels`, {
|
|
213
|
+
headers: { authorization: authHeader },
|
|
214
|
+
});
|
|
215
|
+
assert.equal(response.status, 200);
|
|
216
|
+
const channels = await response.json();
|
|
217
|
+
assert.deepEqual(channels.map((channel) => channel.key), [
|
|
218
|
+
"default",
|
|
219
|
+
"agent:bellonda",
|
|
220
|
+
"agent:hwi-noree",
|
|
221
|
+
]);
|
|
222
|
+
assert.deepEqual(channels.map((channel) => channel.label), [
|
|
223
|
+
"# chapterhouse",
|
|
224
|
+
"# bellonda",
|
|
225
|
+
"# hwi-noree",
|
|
226
|
+
]);
|
|
227
|
+
assert.equal(channels.find((channel) => channel.key === "agent:bellonda")?.scope, "infra");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
210
230
|
test("server runs in standalone mode without auth", async () => {
|
|
211
231
|
await withStartedServer(async ({ baseUrl }) => {
|
|
212
232
|
const bootstrap = await fetch(`${baseUrl}/api/bootstrap`);
|
|
@@ -358,4 +358,16 @@ test("turn-sse: turnId returned by POST matches turnId in all SSE events for tha
|
|
|
358
358
|
}
|
|
359
359
|
}, { CHAPTERHOUSE_CHAT_SSE: "1" }, 15_000);
|
|
360
360
|
});
|
|
361
|
+
test("turn-sse: POST /api/session/:sessionKey/interrupt returns per-session cancel status", async () => {
|
|
362
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
363
|
+
const res = await fetch(`${baseUrl}/api/session/test-session-interrupt/interrupt`, {
|
|
364
|
+
method: "POST",
|
|
365
|
+
headers: { Authorization: authHeader },
|
|
366
|
+
});
|
|
367
|
+
const bodyText = await res.text();
|
|
368
|
+
assert.equal(res.status, 200, `POST /interrupt returned ${res.status}: ${bodyText}`);
|
|
369
|
+
const body = JSON.parse(bodyText);
|
|
370
|
+
assert.deepEqual(body, { status: "ok", cancelled: false });
|
|
371
|
+
}, { CHAPTERHOUSE_CHAT_SSE: "1" }, 15_000);
|
|
372
|
+
});
|
|
361
373
|
//# sourceMappingURL=turn-sse.integration.test.js.map
|
package/dist/copilot/agents.js
CHANGED
|
@@ -23,6 +23,12 @@ const agentFrontmatterSchema = z.object({
|
|
|
23
23
|
tools: z.array(z.string()).optional(),
|
|
24
24
|
mcpServers: z.array(z.string()).optional(),
|
|
25
25
|
allowed_paths: z.array(z.string()).optional(),
|
|
26
|
+
persistent: z.union([z.boolean(), z.string()]).optional().transform((value) => {
|
|
27
|
+
if (typeof value === "string")
|
|
28
|
+
return value.toLowerCase() === "true";
|
|
29
|
+
return value;
|
|
30
|
+
}),
|
|
31
|
+
scope: z.string().optional(),
|
|
26
32
|
});
|
|
27
33
|
// ---------------------------------------------------------------------------
|
|
28
34
|
// Agent Registry
|
|
@@ -77,6 +83,8 @@ export function parseAgentMd(content, slug) {
|
|
|
77
83
|
name: fm.name,
|
|
78
84
|
description: fm.description,
|
|
79
85
|
model: fm.model,
|
|
86
|
+
persistent: fm.persistent,
|
|
87
|
+
scope: fm.scope,
|
|
80
88
|
skills: fm.skills,
|
|
81
89
|
tools: fm.tools,
|
|
82
90
|
mcpServers: fm.mcpServers,
|
|
@@ -293,10 +301,13 @@ export function getCurrentToolAgentSlug() {
|
|
|
293
301
|
export function getCurrentToolTaskId() {
|
|
294
302
|
return toolTaskContext.getStore();
|
|
295
303
|
}
|
|
304
|
+
export function withToolTaskContext(taskId, fn) {
|
|
305
|
+
return toolTaskContext.run(taskId, fn);
|
|
306
|
+
}
|
|
296
307
|
export function bindToolsToAgent(agentSlug, allTools, taskId) {
|
|
297
308
|
return allTools.map((tool) => ({
|
|
298
309
|
...tool,
|
|
299
|
-
handler: (args, invocation) => toolAgentContext.run(agentSlug, () => toolTaskContext.run(taskId, () => tool.handler(args, invocation))),
|
|
310
|
+
handler: (args, invocation) => toolAgentContext.run(agentSlug, () => toolTaskContext.run(taskId ?? getCurrentToolTaskId(), () => tool.handler(args, invocation))),
|
|
300
311
|
}));
|
|
301
312
|
}
|
|
302
313
|
/** Filter tools based on agent config. */
|
|
@@ -304,7 +315,7 @@ export function filterToolsForAgent(agent, allTools) {
|
|
|
304
315
|
if (agent.tools && agent.tools.length > 0) {
|
|
305
316
|
// Agent specifies an explicit allowlist — give those + wiki tools
|
|
306
317
|
const allowed = new Set([...agent.tools, ...WIKI_TOOL_NAMES]);
|
|
307
|
-
return allTools.filter((t) => allowed.has(t.name));
|
|
318
|
+
return allTools.filter((t) => allowed.has(t.name) && !(agent.persistent && MANAGEMENT_TOOL_NAMES.has(t.name)));
|
|
308
319
|
}
|
|
309
320
|
// Default: all tools except management (only @chapterhouse gets those)
|
|
310
321
|
if (agent.slug === "chapterhouse") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { composeAgentSystemMessage, } from "./agents.js";
|
|
3
|
+
import { composeAgentSystemMessage, filterToolsForAgent, bindToolsToAgent, getCurrentToolTaskId, parseAgentMd, withToolTaskContext, } from "./agents.js";
|
|
4
4
|
function makeAgent(slug) {
|
|
5
5
|
return {
|
|
6
6
|
slug,
|
|
@@ -60,4 +60,46 @@ test("composeAgentSystemMessage teaches subagents the three-tier memory model an
|
|
|
60
60
|
assert.match(message, /memory_propose/i);
|
|
61
61
|
assert.match(message, /do not call `memory_remember` directly|should not call `memory_remember` directly/i);
|
|
62
62
|
});
|
|
63
|
+
test("parseAgentMd detects persistent agent scope from charter frontmatter", () => {
|
|
64
|
+
const agent = parseAgentMd([
|
|
65
|
+
"---",
|
|
66
|
+
"name: Bellonda",
|
|
67
|
+
"description: Mentat of the infrastructure domain",
|
|
68
|
+
"model: claude-sonnet-4.6",
|
|
69
|
+
"persistent: true",
|
|
70
|
+
"scope: infra",
|
|
71
|
+
"---",
|
|
72
|
+
"",
|
|
73
|
+
"You are Bellonda.",
|
|
74
|
+
].join("\n"), "bellonda");
|
|
75
|
+
assert.ok(agent, "agent charter should parse");
|
|
76
|
+
assert.equal(agent.persistent, true);
|
|
77
|
+
assert.equal(agent.scope, "infra");
|
|
78
|
+
});
|
|
79
|
+
test("persistent agents cannot receive scope-changing management tools", () => {
|
|
80
|
+
const agent = {
|
|
81
|
+
...makeAgent("bellonda"),
|
|
82
|
+
persistent: true,
|
|
83
|
+
scope: "infra",
|
|
84
|
+
};
|
|
85
|
+
const tools = [
|
|
86
|
+
{ name: "memory_recall" },
|
|
87
|
+
{ name: "memory_propose" },
|
|
88
|
+
{ name: "memory_set_scope" },
|
|
89
|
+
{ name: "delegate_to_agent" },
|
|
90
|
+
{ name: "bash" },
|
|
91
|
+
];
|
|
92
|
+
const filtered = filterToolsForAgent(agent, tools);
|
|
93
|
+
const names = filtered.map((tool) => tool.name);
|
|
94
|
+
assert.deepEqual(names.sort(), ["bash", "memory_propose", "memory_recall"].sort());
|
|
95
|
+
});
|
|
96
|
+
test("bindToolsToAgent uses the per-turn task context when no fixed task id is provided", async () => {
|
|
97
|
+
const tools = bindToolsToAgent("bellonda", [{
|
|
98
|
+
name: "probe_task_context",
|
|
99
|
+
handler: async () => getCurrentToolTaskId(),
|
|
100
|
+
}]);
|
|
101
|
+
assert.equal(await tools[0].handler({}, {}), undefined);
|
|
102
|
+
const taskId = await withToolTaskContext("delegated-persistent-001", () => tools[0].handler({}, {}));
|
|
103
|
+
assert.equal(taskId, "delegated-persistent-001");
|
|
104
|
+
});
|
|
63
105
|
//# sourceMappingURL=agents.test.js.map
|
|
@@ -4,7 +4,9 @@ import { approveAll } from "@github/copilot-sdk";
|
|
|
4
4
|
import { createTools } from "./tools.js";
|
|
5
5
|
import { getOrchestratorSystemMessage } from "./system-message.js";
|
|
6
6
|
import { renderHotTierForActiveScope } from "../memory/hot-tier.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getHotTierEntries, renderHotTierXML } from "../memory/hot-tier.js";
|
|
8
|
+
import { getActiveScope, withActiveScope } from "../memory/active-scope.js";
|
|
9
|
+
import { getScope } from "../memory/scopes.js";
|
|
8
10
|
import { CheckpointTracker, isCheckpointInFlight, runCheckpointExtraction } from "../memory/checkpoint.js";
|
|
9
11
|
import { isHousekeepingInFlight, runHousekeeping } from "../memory/housekeeping.js";
|
|
10
12
|
import { runEndOfTaskMemoryHook } from "../memory/eot.js";
|
|
@@ -18,7 +20,7 @@ import { maybeWriteEpisode } from "./episode-writer.js";
|
|
|
18
20
|
import { getWikiSummary } from "../wiki/context.js";
|
|
19
21
|
import { SESSIONS_DIR } from "../paths.js";
|
|
20
22
|
import { resolveModel } from "./router.js";
|
|
21
|
-
import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, setActiveAgent, parseAtMention, buildAgentRoster, getActiveTasks, } from "./agents.js";
|
|
23
|
+
import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, setActiveAgent, parseAtMention, buildAgentRoster, getActiveTasks, getAgent, composeAgentSystemMessage, filterToolsForAgent, withToolTaskContext, } from "./agents.js";
|
|
22
24
|
import * as agentsModule from "./agents.js";
|
|
23
25
|
import { childLogger } from "../util/logger.js";
|
|
24
26
|
import { agentEventBus } from "./agent-event-bus.js";
|
|
@@ -34,6 +36,8 @@ const log = childLogger("orchestrator");
|
|
|
34
36
|
const MAX_RETRIES = 3;
|
|
35
37
|
const RECONNECT_DELAYS_MS = [1_000, 3_000, 10_000];
|
|
36
38
|
const HEALTH_CHECK_INTERVAL_MS = 30_000;
|
|
39
|
+
const AGENT_REPLY_CHUNK_SIZE = 500;
|
|
40
|
+
const AGENT_REPLY_CHUNK_THRESHOLD = 8 * 1024;
|
|
37
41
|
const ORCHESTRATOR_SESSION_KEY = "orchestrator_session_id";
|
|
38
42
|
const LAST_AUTHENTICATED_USER_KEY = "last_authenticated_user";
|
|
39
43
|
let logMessage = () => { };
|
|
@@ -136,7 +140,7 @@ function scheduleCheckpointExtraction(sessionKey, prompt, finalContent, source)
|
|
|
136
140
|
log.error({ sessionKey }, "memory.checkpoint.error");
|
|
137
141
|
return;
|
|
138
142
|
}
|
|
139
|
-
const activeScope =
|
|
143
|
+
const activeScope = getMemoryScopeForSession(sessionKey);
|
|
140
144
|
void runCheckpointExtraction({
|
|
141
145
|
sessionKey,
|
|
142
146
|
turns: turns.slice(-config.memoryCheckpointTurns),
|
|
@@ -161,7 +165,7 @@ function scheduleHousekeeping(sessionKey, source) {
|
|
|
161
165
|
return;
|
|
162
166
|
}
|
|
163
167
|
housekeepingTurnsBySession.set(sessionKey, 0);
|
|
164
|
-
const activeScope =
|
|
168
|
+
const activeScope = getMemoryScopeForSession(sessionKey);
|
|
165
169
|
if (!activeScope) {
|
|
166
170
|
log.info({ sessionKey }, "memory.housekeeping.no_active_scope");
|
|
167
171
|
return;
|
|
@@ -321,6 +325,30 @@ function getSessionConfig() {
|
|
|
321
325
|
const skillDirectories = getSkillDirectories();
|
|
322
326
|
return { tools, mcpServers, skillDirectories };
|
|
323
327
|
}
|
|
328
|
+
function agentSlugFromSessionKey(sessionKey) {
|
|
329
|
+
return sessionKey.startsWith("agent:") ? sessionKey.slice("agent:".length) : undefined;
|
|
330
|
+
}
|
|
331
|
+
function getPersistentAgentForSessionKey(sessionKey) {
|
|
332
|
+
const slug = agentSlugFromSessionKey(sessionKey);
|
|
333
|
+
if (!slug)
|
|
334
|
+
return undefined;
|
|
335
|
+
const agent = getAgent(slug);
|
|
336
|
+
return agent?.persistent ? agent : undefined;
|
|
337
|
+
}
|
|
338
|
+
function getMemoryScopeForSession(sessionKey) {
|
|
339
|
+
const persistentAgent = getPersistentAgentForSessionKey(sessionKey);
|
|
340
|
+
if (persistentAgent?.scope) {
|
|
341
|
+
return getScope(persistentAgent.scope) ?? null;
|
|
342
|
+
}
|
|
343
|
+
return getActiveScope();
|
|
344
|
+
}
|
|
345
|
+
function buildScopedHotTierContext(scope) {
|
|
346
|
+
if (!config.memoryInjectEnabled || !scope) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
const hotTierXml = renderHotTierXML(getHotTierEntries(scope.id));
|
|
350
|
+
return hotTierXml ? hotTierXml.trimEnd() : undefined;
|
|
351
|
+
}
|
|
324
352
|
function buildHotTierContext() {
|
|
325
353
|
if (!config.memoryInjectEnabled) {
|
|
326
354
|
return undefined;
|
|
@@ -392,8 +420,7 @@ export function feedAgentResult(taskId, agentSlug, result) {
|
|
|
392
420
|
agentSlug,
|
|
393
421
|
agentDisplayName,
|
|
394
422
|
});
|
|
395
|
-
const
|
|
396
|
-
const chunks = result.length === 0 ? [""] : Array.from({ length: Math.ceil(result.length / chunkSize) }, (_, index) => result.slice(index * chunkSize, (index + 1) * chunkSize));
|
|
423
|
+
const chunks = result.length <= AGENT_REPLY_CHUNK_THRESHOLD ? [result] : Array.from({ length: Math.ceil(result.length / AGENT_REPLY_CHUNK_SIZE) }, (_, index) => result.slice(index * AGENT_REPLY_CHUNK_SIZE, (index + 1) * AGENT_REPLY_CHUNK_SIZE));
|
|
397
424
|
for (const chunk of chunks) {
|
|
398
425
|
emitTurnEvent(sessionKey, {
|
|
399
426
|
type: "turn:delta",
|
|
@@ -417,12 +444,15 @@ export function feedAgentResult(taskId, agentSlug, result) {
|
|
|
417
444
|
catch (err) {
|
|
418
445
|
log.warn({ err: err instanceof Error ? err.message : err, taskId, agentSlug }, "Failed to persist agent completion");
|
|
419
446
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
447
|
+
void (async () => {
|
|
448
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
449
|
+
const prompt = `[Agent task completed] @${agentSlug} finished task ${taskId}. Their reply has been shown to the user. Acknowledge briefly.`;
|
|
450
|
+
sendToOrchestrator(prompt, { type: "background", sessionKey }, (text, done) => {
|
|
451
|
+
if (done && proactiveNotifyFn) {
|
|
452
|
+
proactiveNotifyFn(text);
|
|
453
|
+
}
|
|
454
|
+
}, undefined, undefined, undefined, undefined, undefined, { suppressPromptLog: true });
|
|
455
|
+
})();
|
|
426
456
|
}
|
|
427
457
|
function sleep(ms) {
|
|
428
458
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -467,25 +497,48 @@ function startHealthCheck() {
|
|
|
467
497
|
/** Internal: create or resume a CopilotSession. Called by SessionManager.ensureSession(). */
|
|
468
498
|
async function createOrResumeSession(sessionKey, projectRoot) {
|
|
469
499
|
const client = await ensureClient();
|
|
470
|
-
const
|
|
500
|
+
const baseConfig = getSessionConfig();
|
|
501
|
+
let { tools, mcpServers, skillDirectories } = baseConfig;
|
|
471
502
|
const isProjectSession = sessionKey.startsWith("project:");
|
|
503
|
+
const persistentAgent = getPersistentAgentForSessionKey(sessionKey);
|
|
504
|
+
const agentScope = persistentAgent?.scope ? getScope(persistentAgent.scope) ?? null : null;
|
|
472
505
|
const infiniteSessions = {
|
|
473
506
|
enabled: true,
|
|
474
507
|
backgroundCompactionThreshold: 0.80,
|
|
475
508
|
bufferExhaustionThreshold: 0.95,
|
|
476
509
|
};
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
510
|
+
let model = config.copilotModel;
|
|
511
|
+
let systemMessageContent;
|
|
512
|
+
let sessionMode = isProjectSession ? "project" : "default";
|
|
513
|
+
if (persistentAgent) {
|
|
514
|
+
model = persistentAgent.model === "auto" ? config.copilotModel : persistentAgent.model;
|
|
515
|
+
tools = agentsModule.bindToolsToAgent(persistentAgent.slug, filterToolsForAgent(persistentAgent, createTools({
|
|
516
|
+
client: copilotClient,
|
|
517
|
+
onAgentTaskComplete: feedAgentResult,
|
|
518
|
+
})));
|
|
519
|
+
const scopedHotTier = buildScopedHotTierContext(agentScope);
|
|
520
|
+
const channelNote = `You are in your persistent Chapterhouse channel (${sessionKey}). Your memory scope is ${persistentAgent.scope}.`;
|
|
521
|
+
systemMessageContent = [
|
|
522
|
+
composeAgentSystemMessage(persistentAgent),
|
|
523
|
+
channelNote,
|
|
524
|
+
scopedHotTier,
|
|
525
|
+
].filter(Boolean).join("\n\n");
|
|
526
|
+
sessionMode = "agent";
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
const memorySummary = getWikiSummary();
|
|
530
|
+
systemMessageContent = getOrchestratorSystemMessage({
|
|
531
|
+
...getSystemMessageOptions(memorySummary),
|
|
532
|
+
version: CHAPTERHOUSE_VERSION,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
482
535
|
const stored = getCopilotSession(sessionKey);
|
|
483
536
|
const savedSessionId = stored?.copilotSessionId ?? (sessionKey === "default" ? getState(ORCHESTRATOR_SESSION_KEY) : undefined);
|
|
484
537
|
if (savedSessionId) {
|
|
485
538
|
try {
|
|
486
539
|
log.info({ sessionKey, sessionId: savedSessionId.slice(0, 8) }, "Resuming session");
|
|
487
540
|
const session = await client.resumeSession(savedSessionId, {
|
|
488
|
-
model
|
|
541
|
+
model,
|
|
489
542
|
configDir: SESSIONS_DIR,
|
|
490
543
|
streaming: true,
|
|
491
544
|
systemMessage: { content: systemMessageContent },
|
|
@@ -497,10 +550,10 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
497
550
|
});
|
|
498
551
|
log.info({ sessionKey }, "Session resumed successfully");
|
|
499
552
|
resetCheckpointSessionState(sessionKey);
|
|
500
|
-
upsertCopilotSession(sessionKey,
|
|
553
|
+
upsertCopilotSession(sessionKey, sessionMode, session.sessionId, projectRoot, model);
|
|
501
554
|
const mgr = registry?.get(sessionKey);
|
|
502
555
|
if (mgr)
|
|
503
|
-
mgr.currentModel =
|
|
556
|
+
mgr.currentModel = model;
|
|
504
557
|
return session;
|
|
505
558
|
}
|
|
506
559
|
catch (err) {
|
|
@@ -511,7 +564,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
511
564
|
}
|
|
512
565
|
log.info({ sessionKey }, "Creating new session");
|
|
513
566
|
const session = await client.createSession({
|
|
514
|
-
model
|
|
567
|
+
model,
|
|
515
568
|
configDir: SESSIONS_DIR,
|
|
516
569
|
streaming: true,
|
|
517
570
|
systemMessage: { content: systemMessageContent },
|
|
@@ -523,12 +576,12 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
523
576
|
});
|
|
524
577
|
log.info({ sessionKey, sessionId: session.sessionId.slice(0, 8) }, "Session created");
|
|
525
578
|
resetCheckpointSessionState(sessionKey);
|
|
526
|
-
upsertCopilotSession(sessionKey,
|
|
579
|
+
upsertCopilotSession(sessionKey, sessionMode, session.sessionId, projectRoot, model);
|
|
527
580
|
if (sessionKey === "default")
|
|
528
581
|
setState(ORCHESTRATOR_SESSION_KEY, session.sessionId);
|
|
529
582
|
const mgr = registry?.get(sessionKey);
|
|
530
583
|
if (mgr)
|
|
531
|
-
mgr.currentModel =
|
|
584
|
+
mgr.currentModel = model;
|
|
532
585
|
return session;
|
|
533
586
|
}
|
|
534
587
|
export async function initOrchestrator(client) {
|
|
@@ -564,6 +617,9 @@ export async function initOrchestrator(client) {
|
|
|
564
617
|
try {
|
|
565
618
|
const defaultManager = registry.getOrCreate("default");
|
|
566
619
|
await defaultManager.ensureSession();
|
|
620
|
+
await Promise.allSettled(agents
|
|
621
|
+
.filter((agent) => agent.persistent && agent.scope)
|
|
622
|
+
.map((agent) => registry.getOrCreate(`agent:${agent.slug}`).ensureSession()));
|
|
567
623
|
}
|
|
568
624
|
catch (err) {
|
|
569
625
|
log.error({ err: err instanceof Error ? err.message : err }, "Failed to create initial session (will retry on first message)");
|
|
@@ -603,7 +659,7 @@ async function executeOnSession(manager, item) {
|
|
|
603
659
|
// Update last-seen globals (backwards compat — for callers that inspect after a turn ends)
|
|
604
660
|
currentAuthenticatedUser = item.authUser;
|
|
605
661
|
currentAuthorizationHeader = item.authHeader;
|
|
606
|
-
|
|
662
|
+
const runTurn = () => turnContextStorage.run({
|
|
607
663
|
sessionKey,
|
|
608
664
|
sourceChannel: item.sourceChannel,
|
|
609
665
|
channelKey: item.channelKey,
|
|
@@ -1003,6 +1059,14 @@ async function executeOnSession(manager, item) {
|
|
|
1003
1059
|
unsubTurnReasoning();
|
|
1004
1060
|
}
|
|
1005
1061
|
});
|
|
1062
|
+
const persistentAgent = getPersistentAgentForSessionKey(sessionKey);
|
|
1063
|
+
const scopedRunTurn = () => item.taskId
|
|
1064
|
+
? withToolTaskContext(item.taskId, runTurn)
|
|
1065
|
+
: runTurn();
|
|
1066
|
+
if (persistentAgent?.scope) {
|
|
1067
|
+
return withActiveScope(persistentAgent.scope, scopedRunTurn);
|
|
1068
|
+
}
|
|
1069
|
+
return scopedRunTurn();
|
|
1006
1070
|
}
|
|
1007
1071
|
/**
|
|
1008
1072
|
* Process a single queued item: route model, handle @mentions, execute.
|
|
@@ -1010,6 +1074,10 @@ async function executeOnSession(manager, item) {
|
|
|
1010
1074
|
*/
|
|
1011
1075
|
async function processItem(item, manager) {
|
|
1012
1076
|
const { sessionKey } = manager;
|
|
1077
|
+
const persistentAgent = getPersistentAgentForSessionKey(sessionKey);
|
|
1078
|
+
if (persistentAgent) {
|
|
1079
|
+
return executeOnSession(manager, item);
|
|
1080
|
+
}
|
|
1013
1081
|
if (item.targetAgent && item.targetAgent !== "chapterhouse") {
|
|
1014
1082
|
setActiveAgent(item.channelKey || "default", item.targetAgent);
|
|
1015
1083
|
return executeOnSession(manager, item);
|
|
@@ -1040,6 +1108,24 @@ async function processItem(item, manager) {
|
|
|
1040
1108
|
lastRouteResult = routeResult;
|
|
1041
1109
|
return executeOnSession(manager, item);
|
|
1042
1110
|
}
|
|
1111
|
+
export async function sendToAgentSession(slug, prompt, taskId) {
|
|
1112
|
+
const agent = getAgent(slug);
|
|
1113
|
+
if (!agent?.persistent) {
|
|
1114
|
+
throw new Error(`Agent '${slug}' is not a persistent agent.`);
|
|
1115
|
+
}
|
|
1116
|
+
const sessionKey = `agent:${agent.slug}`;
|
|
1117
|
+
return await new Promise((resolve) => {
|
|
1118
|
+
sendToOrchestrator(prompt, {
|
|
1119
|
+
type: "sse-web",
|
|
1120
|
+
sessionKey,
|
|
1121
|
+
user: getCurrentAuthenticatedUser(),
|
|
1122
|
+
authorizationHeader: getCurrentAuthorizationHeader(),
|
|
1123
|
+
}, (text, done) => {
|
|
1124
|
+
if (done)
|
|
1125
|
+
resolve(text);
|
|
1126
|
+
}, undefined, undefined, undefined, undefined, undefined, { logSource: "delegated", taskId, viaLabel: "@chapterhouse" });
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1043
1129
|
function getActiveProjectRules(prompt, projectPath) {
|
|
1044
1130
|
const registry = loadRegistry();
|
|
1045
1131
|
const project = resolveProject(prompt, { projectPath }, registry);
|
|
@@ -1077,6 +1163,7 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1077
1163
|
// returned to the client to match every emitted event — Fix 1 root cause).
|
|
1078
1164
|
const turnId = externalTurnId ?? randomUUID();
|
|
1079
1165
|
const sourceLabel = source.type === "background" ? "background" : "web";
|
|
1166
|
+
const logSource = options?.logSource ?? sourceLabel;
|
|
1080
1167
|
logMessage("in", sourceLabel, prompt);
|
|
1081
1168
|
let sessionKey;
|
|
1082
1169
|
if ((source.type === "background" || source.type === "sse-web") && source.sessionKey) {
|
|
@@ -1086,12 +1173,13 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1086
1173
|
sessionKey = "default";
|
|
1087
1174
|
}
|
|
1088
1175
|
const channelKey = source.type === "web" ? source.connectionId : "default";
|
|
1089
|
-
const
|
|
1176
|
+
const isPersistentAgentSession = sessionKey.startsWith("agent:");
|
|
1177
|
+
const mention = isPersistentAgentSession ? undefined : parseAtMention(prompt);
|
|
1090
1178
|
const targetAgent = mention?.agentSlug;
|
|
1091
1179
|
const routedPrompt = mention ? mention.message : prompt;
|
|
1092
1180
|
const taggedPrompt = source.type === "background"
|
|
1093
1181
|
? routedPrompt
|
|
1094
|
-
: `[via ${sourceLabel}] ${routedPrompt}`;
|
|
1182
|
+
: `[via ${options?.viaLabel ?? sourceLabel}] ${routedPrompt}`;
|
|
1095
1183
|
const logRole = source.type === "background" ? "agent_completion" : "user";
|
|
1096
1184
|
const sourceChannel = source.type === "web" ? "web" : undefined;
|
|
1097
1185
|
// Capture auth context at enqueue time — prevents cross-session contamination
|
|
@@ -1125,6 +1213,7 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1125
1213
|
targetAgent,
|
|
1126
1214
|
channelKey,
|
|
1127
1215
|
sessionKey,
|
|
1216
|
+
taskId: options?.taskId,
|
|
1128
1217
|
authUser,
|
|
1129
1218
|
authHeader,
|
|
1130
1219
|
resolve,
|
|
@@ -1141,12 +1230,12 @@ export async function sendToOrchestrator(prompt, source, callback, attachments,
|
|
|
1141
1230
|
catch { /* best-effort */ }
|
|
1142
1231
|
if (!options?.suppressPromptLog) {
|
|
1143
1232
|
try {
|
|
1144
|
-
logConversation(logRole, prompt,
|
|
1233
|
+
logConversation(logRole, prompt, logSource, sessionKey, { turnId });
|
|
1145
1234
|
}
|
|
1146
1235
|
catch { /* best-effort */ }
|
|
1147
1236
|
}
|
|
1148
1237
|
try {
|
|
1149
|
-
logConversation("assistant", finalContent,
|
|
1238
|
+
logConversation("assistant", finalContent, logSource, sessionKey, { turnId });
|
|
1150
1239
|
}
|
|
1151
1240
|
catch { /* best-effort */ }
|
|
1152
1241
|
scheduleCheckpointExtraction(sessionKey, prompt, finalContent, source);
|
|
@@ -1350,6 +1439,20 @@ export async function cancelCurrentMessage() {
|
|
|
1350
1439
|
}
|
|
1351
1440
|
return aborted || drained > 0;
|
|
1352
1441
|
}
|
|
1442
|
+
/** Cancel the active turn for a single session key. */
|
|
1443
|
+
export async function interruptSessionTurn(sessionKey) {
|
|
1444
|
+
const manager = registry?.get(sessionKey);
|
|
1445
|
+
if (!manager?.isProcessing)
|
|
1446
|
+
return false;
|
|
1447
|
+
const turnId = manager.currentTurnId;
|
|
1448
|
+
const aborted = await manager.abortCurrentTurn();
|
|
1449
|
+
if (aborted && turnId) {
|
|
1450
|
+
emitTurnEvent(sessionKey, { type: "turn:interrupted", turnId, sessionKey });
|
|
1451
|
+
persistTurnEvents(turnId, sessionKey);
|
|
1452
|
+
scheduleClearTurnLog(turnId);
|
|
1453
|
+
}
|
|
1454
|
+
return aborted;
|
|
1455
|
+
}
|
|
1353
1456
|
/** Switch the model on the live default orchestrator session without destroying it. */
|
|
1354
1457
|
export function switchSessionModel(newModel) {
|
|
1355
1458
|
const manager = registry?.get("default");
|