chapterhouse 0.6.0 → 0.8.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/korg.agent.md +65 -0
- package/dist/api/agent-edit-access.js +11 -0
- package/dist/api/agents.api.test.js +48 -0
- package/dist/api/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +420 -13
- package/dist/api/server.test.js +533 -3
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agent-event-bus.js +1 -0
- package/dist/copilot/agents.js +117 -50
- package/dist/copilot/agents.mcp-servers.test.js +87 -0
- package/dist/copilot/agents.parse.test.js +69 -0
- package/dist/copilot/agents.test.js +137 -2
- package/dist/copilot/orchestrator.js +62 -13
- package/dist/copilot/orchestrator.test.js +130 -8
- package/dist/copilot/session-manager.js +34 -0
- package/dist/copilot/system-message.js +11 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +184 -376
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +53 -59
- package/dist/daemon.js +9 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/hooks.js +151 -0
- package/dist/memory/hooks.test.js +325 -0
- package/dist/memory/hot-tier.js +37 -0
- package/dist/memory/hot-tier.test.js +30 -0
- package/dist/memory/housekeeping-scheduler.js +35 -0
- package/dist/memory/housekeeping-scheduler.test.js +50 -0
- package/dist/memory/inbox.js +10 -0
- package/dist/memory/index.js +3 -1
- package/dist/memory/migration.js +244 -0
- package/dist/memory/migration.test.js +100 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/store/db.js +119 -4
- package/dist/store/db.test.js +19 -1
- package/dist/test/setup-env.js +3 -1
- package/dist/test/setup-env.test.js +8 -1
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +140 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/index-manager.js +246 -330
- package/dist/wiki/index-manager.test.js +138 -145
- package/dist/wiki/ingest.js +347 -0
- package/dist/wiki/ingest.test.js +111 -0
- package/dist/wiki/links.js +151 -0
- package/dist/wiki/links.test.js +176 -0
- package/dist/wiki/migrate-topics.test.js +16 -6
- package/dist/wiki/scheduler.js +118 -0
- package/dist/wiki/scheduler.test.js +64 -0
- package/dist/wiki/timeline.js +51 -0
- package/dist/wiki/timeline.test.js +65 -0
- package/dist/wiki/topic-structure.js +1 -1
- package/package.json +3 -1
- package/skills/pkb-ideas/SKILL.md +78 -0
- package/skills/pkb-ideas/_meta.json +4 -0
- package/skills/pkb-org/SKILL.md +82 -0
- package/skills/pkb-org/_meta.json +4 -0
- package/skills/pkb-people/SKILL.md +74 -0
- package/skills/pkb-people/_meta.json +4 -0
- package/skills/pkb-research/SKILL.md +83 -0
- package/skills/pkb-research/_meta.json +4 -0
- package/skills/pkb-source/SKILL.md +38 -0
- package/skills/pkb-source/_meta.json +4 -0
- package/skills/wiki-conventions/SKILL.md +5 -5
- package/web/dist/assets/index-5kz9aRU9.css +10 -0
- package/web/dist/assets/{index-B5oDsQ5y.js → index-BbX9RKf3.js} +101 -99
- package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/dist/wiki/context.js +0 -138
- package/dist/wiki/fix.js +0 -335
- package/dist/wiki/fix.test.js +0 -350
- package/dist/wiki/lint.js +0 -451
- package/dist/wiki/lint.test.js +0 -329
- package/web/dist/assets/index-B5oDsQ5y.js.map +0 -1
- package/web/dist/assets/index-DknKAtDS.css +0 -10
|
@@ -13,10 +13,9 @@ import { getSkillDirectories } from "./skills.js";
|
|
|
13
13
|
import { resetClient } from "./client.js";
|
|
14
14
|
import { logConversation, getState, setState, deleteState, getCopilotSession, upsertCopilotSession, getTaskSessionKey, getDb, appendTaskEvent } from "../store/db.js";
|
|
15
15
|
import { maybeWriteEpisode } from "./episode-writer.js";
|
|
16
|
-
import { getWikiSummary } from "../wiki/context.js";
|
|
17
16
|
import { SESSIONS_DIR } from "../paths.js";
|
|
18
17
|
import { resolveModel } from "./router.js";
|
|
19
|
-
import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, setActiveAgent, parseAtMention, buildAgentRoster, getActiveTasks, getAgent, composeAgentSystemMessage, filterToolsForAgent, withToolTaskContext, } from "./agents.js";
|
|
18
|
+
import { loadAgents, ensureDefaultAgents, clearActiveTasks, getAgentRegistry, setActiveAgent, parseAtMention, buildAgentRoster, getActiveTasks, getAgent, composeAgentSystemMessage, filterMcpServersForAgent, filterToolsForAgent, withToolTaskContext, } from "./agents.js";
|
|
20
19
|
import * as agentsModule from "./agents.js";
|
|
21
20
|
import { childLogger } from "../util/logger.js";
|
|
22
21
|
import { agentEventBus } from "./agent-event-bus.js";
|
|
@@ -36,6 +35,18 @@ const AGENT_REPLY_CHUNK_SIZE = 500;
|
|
|
36
35
|
const AGENT_REPLY_CHUNK_THRESHOLD = 8 * 1024;
|
|
37
36
|
const ORCHESTRATOR_SESSION_KEY = "orchestrator_session_id";
|
|
38
37
|
const LAST_AUTHENTICATED_USER_KEY = "last_authenticated_user";
|
|
38
|
+
function getWikiSummary() {
|
|
39
|
+
try {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
const pages = db.prepare(`SELECT title, summary, last_updated FROM wiki_pages ORDER BY last_updated DESC LIMIT 50`).all();
|
|
42
|
+
if (pages.length === 0)
|
|
43
|
+
return "";
|
|
44
|
+
return pages.map((page) => `${page.title}: ${page.summary || ""}`.trim()).join("\n");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
39
50
|
let logMessage = () => { };
|
|
40
51
|
export function setMessageLogger(fn) {
|
|
41
52
|
logMessage = fn;
|
|
@@ -179,6 +190,13 @@ function getSessionConfig() {
|
|
|
179
190
|
function agentSlugFromSessionKey(sessionKey) {
|
|
180
191
|
return sessionKey.startsWith("agent:") ? sessionKey.slice("agent:".length) : undefined;
|
|
181
192
|
}
|
|
193
|
+
function persistentAgentSessionKey(slug) {
|
|
194
|
+
const agent = getAgent(slug);
|
|
195
|
+
if (!agent?.persistent) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
return `agent:${agent.slug}`;
|
|
199
|
+
}
|
|
182
200
|
function getPersistentAgentForSessionKey(sessionKey) {
|
|
183
201
|
const slug = agentSlugFromSessionKey(sessionKey);
|
|
184
202
|
if (!slug)
|
|
@@ -344,6 +362,7 @@ async function createOrResumeSession(sessionKey, projectRoot) {
|
|
|
344
362
|
client: copilotClient,
|
|
345
363
|
onAgentTaskComplete: feedAgentResult,
|
|
346
364
|
})));
|
|
365
|
+
mcpServers = filterMcpServersForAgent(persistentAgent, mcpServers);
|
|
347
366
|
const scopedHotTier = (await memoryCoordinator?.buildHotTierContext(sessionKey)) || undefined;
|
|
348
367
|
const channelNote = `You are in your persistent Chapterhouse channel (${sessionKey}). Your memory scope is ${persistentAgent.scope}.`;
|
|
349
368
|
systemMessageContent = [
|
|
@@ -1209,19 +1228,18 @@ export async function interruptCurrentTurn(sessionKey, newPrompt, source, callba
|
|
|
1209
1228
|
await manager.abortCurrentTurn();
|
|
1210
1229
|
log.info({ sessionKey, replacementTurnId: turnId }, "turn.interrupted");
|
|
1211
1230
|
}
|
|
1212
|
-
/**
|
|
1213
|
-
* Enqueue a turn for the new POST→SSE chat path (#130).
|
|
1214
|
-
*
|
|
1215
|
-
* Unlike `sendToOrchestrator`, this function:
|
|
1216
|
-
* - Returns the `turnId` immediately without waiting for the turn to complete.
|
|
1217
|
-
* - Routes through the shared lifecycle emitter in sendToOrchestrator.
|
|
1218
|
-
* - Does NOT write to sseClients — the SSE channel delivers events via subscribeSession().
|
|
1219
|
-
* - Supports interrupt: true which calls interruptCurrentTurn under the hood.
|
|
1220
|
-
*
|
|
1221
|
-
* @returns turnId (UUID)
|
|
1222
|
-
*/
|
|
1223
1231
|
export function enqueueForSse(opts) {
|
|
1232
|
+
if (opts.type === "agent_saved") {
|
|
1233
|
+
agentEventBus.emit({
|
|
1234
|
+
type: "agent_saved",
|
|
1235
|
+
payload: { slug: opts.slug ?? "" },
|
|
1236
|
+
});
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1224
1239
|
const { sessionKey, prompt, attachments, authUser, authHeader, interrupt } = opts;
|
|
1240
|
+
if (!sessionKey || !prompt) {
|
|
1241
|
+
throw new Error("Missing sessionKey or prompt for SSE turn enqueue");
|
|
1242
|
+
}
|
|
1225
1243
|
const turnId = randomUUID();
|
|
1226
1244
|
// sse-web carries auth and enables onQueued — unlike "background" (Fixes 2 & 3).
|
|
1227
1245
|
const source = { type: "sse-web", sessionKey, user: authUser, authorizationHeader: authHeader };
|
|
@@ -1288,6 +1306,37 @@ export async function interruptSessionTurn(sessionKey) {
|
|
|
1288
1306
|
}
|
|
1289
1307
|
return aborted;
|
|
1290
1308
|
}
|
|
1309
|
+
export function getPersistentAgentSessionState(slug) {
|
|
1310
|
+
const sessionKey = persistentAgentSessionKey(slug);
|
|
1311
|
+
if (!sessionKey) {
|
|
1312
|
+
return "none";
|
|
1313
|
+
}
|
|
1314
|
+
const manager = registry?.get(sessionKey);
|
|
1315
|
+
if (!manager) {
|
|
1316
|
+
return "none";
|
|
1317
|
+
}
|
|
1318
|
+
return manager.isProcessing ? "in_flight" : "idle";
|
|
1319
|
+
}
|
|
1320
|
+
export async function reloadPersistentAgent(slug, onReloaded) {
|
|
1321
|
+
const sessionKey = persistentAgentSessionKey(slug);
|
|
1322
|
+
if (!sessionKey || !registry) {
|
|
1323
|
+
return "none";
|
|
1324
|
+
}
|
|
1325
|
+
let manager = registry.get(sessionKey);
|
|
1326
|
+
if (!manager) {
|
|
1327
|
+
manager = registry.getOrCreate(sessionKey);
|
|
1328
|
+
await manager.ensureSession();
|
|
1329
|
+
onReloaded?.();
|
|
1330
|
+
return "reloaded";
|
|
1331
|
+
}
|
|
1332
|
+
if (manager.isProcessing) {
|
|
1333
|
+
manager.requestSessionReload(onReloaded);
|
|
1334
|
+
return "scheduled";
|
|
1335
|
+
}
|
|
1336
|
+
await manager.restartSession();
|
|
1337
|
+
onReloaded?.();
|
|
1338
|
+
return "reloaded";
|
|
1339
|
+
}
|
|
1291
1340
|
/** Switch the model on the live default orchestrator session without destroying it. */
|
|
1292
1341
|
export function switchSessionModel(newModel) {
|
|
1293
1342
|
const manager = registry?.get("default");
|
|
@@ -130,6 +130,7 @@ async function loadOrchestratorModule(t, overrides = {}) {
|
|
|
130
130
|
housekeepingRuns: [],
|
|
131
131
|
housekeepingInFlight: false,
|
|
132
132
|
activeScope: makeScope(1, "chapterhouse", "Chapterhouse", "Core Chapterhouse work."),
|
|
133
|
+
wikiPages: [{ title: "Chapterhouse", summary: "wiki summary", last_updated: "2026-05-13" }],
|
|
133
134
|
...overrides,
|
|
134
135
|
};
|
|
135
136
|
const client = createFakeClient(state);
|
|
@@ -382,7 +383,10 @@ async function loadOrchestratorModule(t, overrides = {}) {
|
|
|
382
383
|
});
|
|
383
384
|
t.mock.module("./mcp-config.js", {
|
|
384
385
|
namedExports: {
|
|
385
|
-
loadMcpConfig: () => ({
|
|
386
|
+
loadMcpConfig: () => ({
|
|
387
|
+
filesystem: { command: "filesystem" },
|
|
388
|
+
truenas: { command: "truenas" },
|
|
389
|
+
}),
|
|
386
390
|
},
|
|
387
391
|
});
|
|
388
392
|
t.mock.module("./skills.js", {
|
|
@@ -423,7 +427,7 @@ async function loadOrchestratorModule(t, overrides = {}) {
|
|
|
423
427
|
return {};
|
|
424
428
|
},
|
|
425
429
|
get: () => undefined,
|
|
426
|
-
all: () => [],
|
|
430
|
+
all: () => (sql.includes("FROM wiki_pages") ? (state.wikiPages ?? []) : []),
|
|
427
431
|
}),
|
|
428
432
|
transaction: (fn) => fn,
|
|
429
433
|
}),
|
|
@@ -444,11 +448,6 @@ async function loadOrchestratorModule(t, overrides = {}) {
|
|
|
444
448
|
},
|
|
445
449
|
},
|
|
446
450
|
});
|
|
447
|
-
t.mock.module("../wiki/context.js", {
|
|
448
|
-
namedExports: {
|
|
449
|
-
getWikiSummary: () => "wiki summary",
|
|
450
|
-
},
|
|
451
|
-
});
|
|
452
451
|
t.mock.module("../wiki/project-registry.js", {
|
|
453
452
|
namedExports: {
|
|
454
453
|
loadRegistry: () => state.projectRegistry,
|
|
@@ -513,6 +512,13 @@ async function loadOrchestratorModule(t, overrides = {}) {
|
|
|
513
512
|
return "@coder @designer";
|
|
514
513
|
},
|
|
515
514
|
composeAgentSystemMessage: (agent) => agent.systemMessage ?? `You are ${agent.slug}.`,
|
|
515
|
+
filterMcpServersForAgent: (agent, mcpServers) => {
|
|
516
|
+
if (agent.mcpServers && agent.mcpServers.length > 0) {
|
|
517
|
+
const allowed = new Set(agent.mcpServers);
|
|
518
|
+
return Object.fromEntries(Object.entries(mcpServers).filter(([name]) => allowed.has(name)));
|
|
519
|
+
}
|
|
520
|
+
return mcpServers;
|
|
521
|
+
},
|
|
516
522
|
filterToolsForAgent: (_agent, tools) => tools,
|
|
517
523
|
bindToolsToAgent: (_agentSlug, tools) => tools,
|
|
518
524
|
withToolTaskContext: (_taskId, fn) => fn(),
|
|
@@ -627,7 +633,7 @@ test("initOrchestrator falls back to an available model and eagerly creates a se
|
|
|
627
633
|
assert.equal(state.loadAgentsCalls, 1);
|
|
628
634
|
assert.equal(state.createSessionCalls.length, 1);
|
|
629
635
|
assert.equal(state.healthCheckIntervalMs, 30_000);
|
|
630
|
-
assert.equal(state.systemOptions?.memorySummary, "wiki summary");
|
|
636
|
+
assert.equal(state.systemOptions?.memorySummary, "Chapterhouse: wiki summary");
|
|
631
637
|
assert.equal(state.store.get("orchestrator_session_id"), "session-123");
|
|
632
638
|
});
|
|
633
639
|
test("initOrchestrator passes hot-tier XML into the orchestrator system prompt when injection is enabled", async (t) => {
|
|
@@ -728,6 +734,28 @@ test("initOrchestrator prewarms persistent agent sessions with scoped hot-tier c
|
|
|
728
734
|
assert.equal(persistentCall.systemMessage.content.includes("scope=\"infra\""), true);
|
|
729
735
|
assert.deepEqual(state.dbWrites.filter((write) => write.sql.includes("copilot_sessions")), []);
|
|
730
736
|
});
|
|
737
|
+
test("initOrchestrator prewarms persistent agent sessions with only allowed MCP servers", async (t) => {
|
|
738
|
+
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
739
|
+
registry: [
|
|
740
|
+
{ slug: "coder", name: "Kaylee", model: "claude-sonnet-4.6", systemMessage: "You are Kaylee." },
|
|
741
|
+
{
|
|
742
|
+
slug: "bellonda",
|
|
743
|
+
name: "Bellonda",
|
|
744
|
+
model: "claude-sonnet-4.6",
|
|
745
|
+
systemMessage: "You are Bellonda.",
|
|
746
|
+
persistent: true,
|
|
747
|
+
scope: "infra",
|
|
748
|
+
mcpServers: ["truenas"],
|
|
749
|
+
},
|
|
750
|
+
],
|
|
751
|
+
});
|
|
752
|
+
await orchestrator.initOrchestrator(client);
|
|
753
|
+
const persistentCall = state.createSessionCalls.find((call) => String(call.systemMessage?.content ?? "").includes("Bellonda"));
|
|
754
|
+
assert.ok(persistentCall, "expected a prewarmed Bellonda session");
|
|
755
|
+
assert.deepEqual(persistentCall.mcpServers, {
|
|
756
|
+
truenas: { command: "truenas" },
|
|
757
|
+
});
|
|
758
|
+
});
|
|
731
759
|
test("sendToOrchestrator routes agent session keys directly to persistent agent sessions", async (t) => {
|
|
732
760
|
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
733
761
|
config: {
|
|
@@ -1517,6 +1545,100 @@ test("cancelCurrentMessage aborts the active request and agent helpers expose ru
|
|
|
1517
1545
|
await orchestrator.shutdownAgents();
|
|
1518
1546
|
assert.equal(state.clearActiveTasksCalls, 1);
|
|
1519
1547
|
});
|
|
1548
|
+
test("persistent agent helpers report session state and reload idle sessions", async (t) => {
|
|
1549
|
+
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
1550
|
+
config: {
|
|
1551
|
+
copilotModel: "claude-sonnet-4.6",
|
|
1552
|
+
selfEditEnabled: true,
|
|
1553
|
+
},
|
|
1554
|
+
registry: [
|
|
1555
|
+
{
|
|
1556
|
+
slug: "bellonda",
|
|
1557
|
+
name: "Bellonda",
|
|
1558
|
+
model: "claude-sonnet-4.6",
|
|
1559
|
+
systemMessage: "You are Bellonda.",
|
|
1560
|
+
persistent: true,
|
|
1561
|
+
scope: "infra",
|
|
1562
|
+
},
|
|
1563
|
+
],
|
|
1564
|
+
});
|
|
1565
|
+
await orchestrator.initOrchestrator(client);
|
|
1566
|
+
const createCallsBeforeReload = state.createSessionCalls.length;
|
|
1567
|
+
assert.equal(orchestrator.getPersistentAgentSessionState("bellonda"), "idle");
|
|
1568
|
+
const reloaded = await orchestrator.reloadPersistentAgent("bellonda");
|
|
1569
|
+
assert.equal(reloaded, "reloaded");
|
|
1570
|
+
assert.equal(orchestrator.getPersistentAgentSessionState("bellonda"), "idle");
|
|
1571
|
+
assert.equal(state.disconnectCalls, 1);
|
|
1572
|
+
assert.equal(state.createSessionCalls.length, createCallsBeforeReload + 1);
|
|
1573
|
+
});
|
|
1574
|
+
test("persistent agent helpers detect in-flight turns", async (t) => {
|
|
1575
|
+
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
1576
|
+
config: {
|
|
1577
|
+
copilotModel: "claude-sonnet-4.6",
|
|
1578
|
+
selfEditEnabled: true,
|
|
1579
|
+
},
|
|
1580
|
+
registry: [
|
|
1581
|
+
{
|
|
1582
|
+
slug: "bellonda",
|
|
1583
|
+
name: "Bellonda",
|
|
1584
|
+
model: "claude-sonnet-4.6",
|
|
1585
|
+
systemMessage: "You are Bellonda.",
|
|
1586
|
+
persistent: true,
|
|
1587
|
+
scope: "infra",
|
|
1588
|
+
},
|
|
1589
|
+
],
|
|
1590
|
+
sendResult: "__PENDING__",
|
|
1591
|
+
});
|
|
1592
|
+
await orchestrator.initOrchestrator(client);
|
|
1593
|
+
orchestrator.sendToOrchestrator("check deploy health", { type: "background", sessionKey: "agent:bellonda" }, () => { });
|
|
1594
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1595
|
+
assert.equal(orchestrator.getPersistentAgentSessionState("bellonda"), "in_flight");
|
|
1596
|
+
state.pendingReject?.(new Error("test teardown"));
|
|
1597
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1598
|
+
});
|
|
1599
|
+
test("reloadPersistentAgent defers reloads until the in-flight turn finishes and preserves queued turns", async (t) => {
|
|
1600
|
+
const { orchestrator, state, client } = await loadOrchestratorModule(t, {
|
|
1601
|
+
config: {
|
|
1602
|
+
copilotModel: "claude-sonnet-4.6",
|
|
1603
|
+
selfEditEnabled: true,
|
|
1604
|
+
},
|
|
1605
|
+
registry: [
|
|
1606
|
+
{
|
|
1607
|
+
slug: "bellonda",
|
|
1608
|
+
name: "Bellonda",
|
|
1609
|
+
model: "claude-sonnet-4.6",
|
|
1610
|
+
systemMessage: "You are Bellonda.",
|
|
1611
|
+
persistent: true,
|
|
1612
|
+
scope: "infra",
|
|
1613
|
+
},
|
|
1614
|
+
],
|
|
1615
|
+
sendResult: "__PENDING__",
|
|
1616
|
+
});
|
|
1617
|
+
await orchestrator.initOrchestrator(client);
|
|
1618
|
+
const createCallsBeforeReload = state.createSessionCalls.length;
|
|
1619
|
+
const reloadEvents = [];
|
|
1620
|
+
orchestrator.sendToOrchestrator("check deploy health", { type: "background", sessionKey: "agent:bellonda" }, () => { });
|
|
1621
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1622
|
+
const queuedTurn = new Promise((resolve) => {
|
|
1623
|
+
orchestrator.sendToOrchestrator("summarize the queued follow-up", { type: "background", sessionKey: "agent:bellonda" }, (text, done) => {
|
|
1624
|
+
if (done)
|
|
1625
|
+
resolve(text);
|
|
1626
|
+
});
|
|
1627
|
+
});
|
|
1628
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1629
|
+
const reloadResult = await orchestrator.reloadPersistentAgent("bellonda", () => {
|
|
1630
|
+
reloadEvents.push("reloaded");
|
|
1631
|
+
});
|
|
1632
|
+
assert.equal(reloadResult, "scheduled");
|
|
1633
|
+
assert.equal(state.disconnectCalls, 0, "deferred reload should wait for the active turn to finish");
|
|
1634
|
+
state.sendResult = "Queued follow-up complete";
|
|
1635
|
+
state.pendingReject?.(new Error("forced restart"));
|
|
1636
|
+
assert.equal(await queuedTurn, "Queued follow-up complete");
|
|
1637
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1638
|
+
assert.deepEqual(reloadEvents, ["reloaded"]);
|
|
1639
|
+
assert.equal(state.disconnectCalls, 1, "deferred reload should restart the session once the turn finishes");
|
|
1640
|
+
assert.equal(state.createSessionCalls.length, createCallsBeforeReload + 1);
|
|
1641
|
+
});
|
|
1520
1642
|
// ---------------------------------------------------------------------------
|
|
1521
1643
|
// REGRESSION: #35 — per-session isolation
|
|
1522
1644
|
// This test would have caught the original bug. With a global shared queue,
|
|
@@ -60,6 +60,9 @@ export class SessionManager {
|
|
|
60
60
|
* never-evict-mid-turn invariant. */
|
|
61
61
|
_pendingClose = false;
|
|
62
62
|
_onPendingCloseEvict;
|
|
63
|
+
/** Set when a persistent session should be recreated before the next queued turn runs. */
|
|
64
|
+
_pendingReload = false;
|
|
65
|
+
_onPendingReload = [];
|
|
63
66
|
constructor(sessionKey, worker, sessionFactory) {
|
|
64
67
|
this.worker = worker;
|
|
65
68
|
this.sessionFactory = sessionFactory;
|
|
@@ -178,6 +181,19 @@ export class SessionManager {
|
|
|
178
181
|
item.reject(err);
|
|
179
182
|
}
|
|
180
183
|
this._lastActivityAt = Date.now();
|
|
184
|
+
if (this._pendingReload) {
|
|
185
|
+
await this.restartSession();
|
|
186
|
+
const callbacks = this._onPendingReload.splice(0);
|
|
187
|
+
this._pendingReload = false;
|
|
188
|
+
for (const callback of callbacks) {
|
|
189
|
+
try {
|
|
190
|
+
callback();
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
log.warn({ sessionKey: this.sessionKey, err: err instanceof Error ? err.message : String(err) }, "session.reload.callback.failed");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
181
197
|
}
|
|
182
198
|
this._currentTurnId = undefined;
|
|
183
199
|
this._processing = false;
|
|
@@ -213,6 +229,24 @@ export class SessionManager {
|
|
|
213
229
|
this._session = undefined;
|
|
214
230
|
this._sessionCreatePromise = undefined;
|
|
215
231
|
}
|
|
232
|
+
async restartSession() {
|
|
233
|
+
if (this._session) {
|
|
234
|
+
try {
|
|
235
|
+
await this._session.disconnect();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// best effort
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this.invalidateSession();
|
|
242
|
+
await this.ensureSession();
|
|
243
|
+
}
|
|
244
|
+
requestSessionReload(onReloaded) {
|
|
245
|
+
this._pendingReload = true;
|
|
246
|
+
if (onReloaded) {
|
|
247
|
+
this._onPendingReload.push(onReloaded);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
216
250
|
/** Reject all queued messages without evicting the session. Returns count drained. */
|
|
217
251
|
cancelQueued() {
|
|
218
252
|
const count = this._queue.length;
|
|
@@ -4,7 +4,7 @@ export function getOrchestratorSystemMessage(opts) {
|
|
|
4
4
|
const versionBanner = opts?.version ? `\nYou are running inside chapterhouse v${opts.version}.\n` : "";
|
|
5
5
|
const memoryBlock = opts?.memorySummary
|
|
6
6
|
? `\n## Memory\nYou have a persistent memory store. Here's what you currently remember:\n\n${opts.memorySummary}\n`
|
|
7
|
-
: "\n## Memory\nYou have a persistent memory store. It's currently empty — use `
|
|
7
|
+
: "\n## Memory\nYou have a persistent memory store. It's currently empty — use `memory_remember` for agent memory or `wiki_update` for wiki knowledge.\n";
|
|
8
8
|
const selfEditBlock = opts?.selfEditEnabled
|
|
9
9
|
? ""
|
|
10
10
|
: `\n## Self-Edit Protection
|
|
@@ -115,22 +115,23 @@ You can delegate **multiple tasks simultaneously**. Different agents can work in
|
|
|
115
115
|
- \`restart_chapterhouse\`: Restart the Chapterhouse daemon.
|
|
116
116
|
|
|
117
117
|
### Memory
|
|
118
|
-
- \`
|
|
119
|
-
- \`
|
|
120
|
-
- \`
|
|
118
|
+
- \`memory_remember\`: Save something durable to scoped agent memory.
|
|
119
|
+
- \`memory_recall\`: Search scoped agent memory for stored facts, decisions, and observations.
|
|
120
|
+
- \`memory_reflect\`: Synthesize durable patterns from repeated observations in the scoped memory store.
|
|
121
|
+
- \`wiki_update\`: Create or update wiki pages when knowledge belongs in the shared wiki.
|
|
121
122
|
|
|
122
123
|
Subagent proposals from \`memory_propose\` are processed automatically at end-of-task, so you do not need to manually review them mid-conversation.
|
|
123
124
|
|
|
124
|
-
**Past conversations**: Daily conversation summaries are auto-written to \`pages/conversations/YYYY-MM-DD.md\`. When the user references something from earlier ("what did we decide about X", "remember when we…", "the thing we discussed yesterday"), call \`wiki_search\`
|
|
125
|
+
**Past conversations**: Daily conversation summaries are auto-written to \`pages/conversations/YYYY-MM-DD.md\`. When the user references something from earlier ("what did we decide about X", "remember when we…", "the thing we discussed yesterday"), call \`wiki_search\` or \`memory_recall\` — don't guess from your own context, since older turns may have been compacted out.
|
|
125
126
|
|
|
126
127
|
**Wiki structure** — the wiki enforces a topic layout, so put things in the right place:
|
|
127
128
|
- **Entity categories** (\`projects\`, \`people\`, \`orgs\`, \`tools\`, \`topics\`, \`areas\`): every named thing gets its own directory \`pages/<category>/<topic-slug>/\`. The topic's overview lives at \`pages/<category>/<topic-slug>/index.md\`; related sub-pages ("facets") go alongside it, e.g. \`pages/projects/chapterhouse/decisions.md\`, \`pages/projects/chapterhouse/feature-ideas.md\`. Exactly one topic level deep; lowercase-slug names only.
|
|
128
129
|
- **Flat categories** (\`preferences\`, \`facts\`, \`routines\`): a single file each, e.g. \`pages/preferences.md\`.
|
|
129
|
-
- **Decisions** are always recorded against an entity, never as a standalone page: a decision about a project goes to \`pages/projects/<topic>/decisions.md\`.
|
|
130
|
-
-
|
|
131
|
-
-
|
|
130
|
+
- **Decisions** are always recorded against an entity, never as a standalone page: a decision about a project goes to \`pages/projects/<topic>/decisions.md\`. Use \`wiki_update\` for shared wiki records or \`memory_remember\` for scoped agent memory.
|
|
131
|
+
- For entity pages and facet pages, use \`wiki_update\` with the full canonical path — bad paths are rejected with a suggested correction; just retry with the suggestion.
|
|
132
|
+
- Source material that should be preserved before synthesis belongs in \`wiki_ingest_source\`.
|
|
132
133
|
|
|
133
|
-
**Wiki writes and restructuring**: Before writing or restructuring wiki content, invoke the \`wiki-conventions\` skill. Treat \`wiki_update
|
|
134
|
+
**Wiki writes and restructuring**: Before writing or restructuring wiki content, invoke the \`wiki-conventions\` skill. Treat \`wiki_update\` and \`wiki_ingest_source\` as write-sensitive workflows, and use \`memory_remember\` / \`memory_recall\` for scoped agent memory. 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.
|
|
134
135
|
|
|
135
136
|
**Learning workflow**: When the user asks you to do something you don't have a skill for:
|
|
136
137
|
1. **Search skills.sh first**: Use the find-skills skill to search for existing community skills.
|
|
@@ -154,7 +155,7 @@ Subagent proposals from \`memory_propose\` are processed automatically at end-of
|
|
|
154
155
|
10. Expand shorthand paths: "${getExampleProjectPath()}" is the expanded form of the user's home-directory project path.
|
|
155
156
|
11. Be conversational and human. You're Chapterhouse.
|
|
156
157
|
12. When using skills, follow the skill's instructions precisely.
|
|
157
|
-
13. **Proactive knowledge building**: When the user shares preferences, project details, etc., proactively use \`
|
|
158
|
+
13. **Proactive knowledge building**: When the user shares preferences, project details, etc., proactively use \`memory_remember\` for scoped agent memory or \`wiki_update\` for shared wiki knowledge.
|
|
158
159
|
14. When a user mentions completing work or shipping something, proactively suggest logging it as OKR progress with \`log_okr_progress\`.
|
|
159
160
|
${selfEditBlock}${memoryBlock}`;
|
|
160
161
|
}
|
|
@@ -86,7 +86,8 @@ test("orchestrator prompt omits memory_context when hot-tier XML is not provided
|
|
|
86
86
|
});
|
|
87
87
|
test("orchestrator prompt requires wiki-conventions before write-sensitive wiki work", () => {
|
|
88
88
|
const message = getOrchestratorSystemMessage();
|
|
89
|
-
assert.match(message, /wiki-conventions[\s\S]{0,500}wiki_update[\s\S]{0,200}
|
|
89
|
+
assert.match(message, /wiki-conventions[\s\S]{0,500}wiki_update[\s\S]{0,200}wiki_ingest_source[\s\S]{0,200}memory_remember[\s\S]{0,200}memory_recall/i);
|
|
90
|
+
assert.doesNotMatch(message, /`remember`|`recall`|`forget`|`wiki_ingest`(?!_source)|`wiki_lint`|`wiki_rebuild_index`|`wiki_fix`/);
|
|
90
91
|
assert.match(message, /before writing or restructuring wiki content/i);
|
|
91
92
|
});
|
|
92
93
|
test("orchestrator prompt describes the wiki orientation ritual", () => {
|
|
@@ -101,4 +102,8 @@ test("orchestrator prompt explains that subagent memory proposals are processed
|
|
|
101
102
|
assert.match(message, /processed automatically at end-of-task|processed automatically at the end of the task/i);
|
|
102
103
|
assert.match(message, /do not need to manually review them mid-conversation|don't need to manually review them mid-conversation/i);
|
|
103
104
|
});
|
|
105
|
+
test("orchestrator prompt includes the memory_reflect tool in memory guidance", () => {
|
|
106
|
+
const message = getOrchestratorSystemMessage();
|
|
107
|
+
assert.match(message, /memory_reflect/);
|
|
108
|
+
});
|
|
104
109
|
//# sourceMappingURL=system-message.test.js.map
|