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
package/dist/api/server.test.js
CHANGED
|
@@ -124,6 +124,22 @@ function readProjectRegistryRows(testRoot) {
|
|
|
124
124
|
db.close();
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
function getAgentFilePath(testRoot, slug) {
|
|
128
|
+
return join(testRoot, ".chapterhouse", "agents", `${slug}.agent.md`);
|
|
129
|
+
}
|
|
130
|
+
function writeAgentFile(testRoot, slug, content) {
|
|
131
|
+
mkdirSync(join(testRoot, ".chapterhouse", "agents"), { recursive: true });
|
|
132
|
+
writeFileSync(getAgentFilePath(testRoot, slug), content, "utf-8");
|
|
133
|
+
}
|
|
134
|
+
function markBundledAgent(testRoot, slug, hash = "bundled-hash") {
|
|
135
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
136
|
+
try {
|
|
137
|
+
db.prepare(`INSERT OR REPLACE INTO max_state (key, value) VALUES (?, ?)`).run(`bundled_agent_hash:${slug}.agent.md`, hash);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
db.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
127
143
|
function setMemoryActiveScope(testRoot, slug) {
|
|
128
144
|
const db = new Database(getProjectDbPath(testRoot));
|
|
129
145
|
try {
|
|
@@ -192,6 +208,15 @@ async function withStartedServer(run, extraEnv = {}, timeoutMs = DEFAULT_API_SER
|
|
|
192
208
|
rmSync(testRoot, { recursive: true, force: true });
|
|
193
209
|
}
|
|
194
210
|
}
|
|
211
|
+
test("agent edits require a team lead when Entra auth is enabled", async () => {
|
|
212
|
+
const accessModule = await import("./agent-edit-access.js");
|
|
213
|
+
assert.equal(typeof accessModule.assertAgentEditAccess, "function", "assertAgentEditAccess should be exported");
|
|
214
|
+
const assertAgentEditAccess = accessModule.assertAgentEditAccess;
|
|
215
|
+
assert.doesNotThrow(() => assertAgentEditAccess({ entraAuthEnabled: false }, { role: "engineer" }));
|
|
216
|
+
assert.throws(() => assertAgentEditAccess({ entraAuthEnabled: true }, { role: "engineer" }), /Admin access required/);
|
|
217
|
+
assert.throws(() => assertAgentEditAccess({ entraAuthEnabled: true }), /Admin access required/);
|
|
218
|
+
assert.doesNotThrow(() => assertAgentEditAccess({ entraAuthEnabled: true }, { role: "team-lead" }));
|
|
219
|
+
});
|
|
195
220
|
test("server routes expose bootstrap and public config without auth", async () => {
|
|
196
221
|
await withStartedServer(async ({ baseUrl }) => {
|
|
197
222
|
const bootstrap = await fetch(`${baseUrl}/api/bootstrap`);
|
|
@@ -216,10 +241,319 @@ test("server channels route returns chapterhouse plus persistent agents in chann
|
|
|
216
241
|
const channels = await response.json();
|
|
217
242
|
assert.deepEqual(channels.map((channel) => channel.key), [
|
|
218
243
|
"default",
|
|
244
|
+
"agent:korg",
|
|
219
245
|
]);
|
|
220
246
|
assert.deepEqual(channels.map((channel) => channel.label), [
|
|
221
247
|
"# chapterhouse",
|
|
248
|
+
"# korg",
|
|
222
249
|
]);
|
|
250
|
+
assert.equal(channels[1]?.scope, "pkb");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
test("server agents routes return source-aware charter summaries and details", async () => {
|
|
254
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
255
|
+
writeAgentFile(testRoot, "alpha-custom", [
|
|
256
|
+
"---",
|
|
257
|
+
"name: Alpha Custom",
|
|
258
|
+
"description: First custom charter",
|
|
259
|
+
"model: gpt-5.4",
|
|
260
|
+
"scope: frontend",
|
|
261
|
+
"skills:",
|
|
262
|
+
" - ui-copy",
|
|
263
|
+
" - wireframes",
|
|
264
|
+
"---",
|
|
265
|
+
"",
|
|
266
|
+
"You are Alpha Custom.",
|
|
267
|
+
].join("\n"));
|
|
268
|
+
writeAgentFile(testRoot, "zulu-custom", [
|
|
269
|
+
"---",
|
|
270
|
+
"name: Zulu Custom",
|
|
271
|
+
"description: Last custom charter",
|
|
272
|
+
"model: claude-sonnet-4.6",
|
|
273
|
+
"persistent: true",
|
|
274
|
+
"---",
|
|
275
|
+
"",
|
|
276
|
+
"You are Zulu Custom.",
|
|
277
|
+
].join("\n"));
|
|
278
|
+
markBundledAgent(testRoot, "designer");
|
|
279
|
+
const listResponse = await fetch(`${baseUrl}/api/agents`, {
|
|
280
|
+
headers: { authorization: authHeader },
|
|
281
|
+
});
|
|
282
|
+
assert.equal(listResponse.status, 200);
|
|
283
|
+
const agents = await listResponse.json();
|
|
284
|
+
const firstCustomIndex = agents.findIndex((agent) => agent.type === "custom");
|
|
285
|
+
assert.notEqual(firstCustomIndex, -1);
|
|
286
|
+
assert.ok(agents.slice(0, firstCustomIndex).every((agent) => agent.type === "builtin"));
|
|
287
|
+
const customAgents = agents.filter((agent) => agent.type === "custom");
|
|
288
|
+
assert.deepEqual(customAgents.map((agent) => agent.name), ["Alpha Custom", "Zulu Custom"]);
|
|
289
|
+
const designer = agents.find((agent) => agent.slug === "designer");
|
|
290
|
+
assert.ok(designer);
|
|
291
|
+
assert.equal(designer.type, "builtin");
|
|
292
|
+
assert.equal(typeof designer.lastEdited, "string");
|
|
293
|
+
const alpha = agents.find((agent) => agent.slug === "alpha-custom");
|
|
294
|
+
assert.deepEqual(alpha, {
|
|
295
|
+
name: "Alpha Custom",
|
|
296
|
+
slug: "alpha-custom",
|
|
297
|
+
description: "First custom charter",
|
|
298
|
+
model: "gpt-5.4",
|
|
299
|
+
scope: "frontend",
|
|
300
|
+
type: "custom",
|
|
301
|
+
lastEdited: alpha?.lastEdited ?? null,
|
|
302
|
+
});
|
|
303
|
+
assert.equal(typeof alpha?.lastEdited, "string");
|
|
304
|
+
const detailResponse = await fetch(`${baseUrl}/api/agents/${encodeURIComponent("alpha-custom")}`, {
|
|
305
|
+
headers: { authorization: authHeader },
|
|
306
|
+
});
|
|
307
|
+
assert.equal(detailResponse.status, 200);
|
|
308
|
+
assert.deepEqual(await detailResponse.json(), {
|
|
309
|
+
name: "Alpha Custom",
|
|
310
|
+
slug: "alpha-custom",
|
|
311
|
+
description: "First custom charter",
|
|
312
|
+
model: "gpt-5.4",
|
|
313
|
+
scope: "frontend",
|
|
314
|
+
persistent: false,
|
|
315
|
+
skills: ["ui-copy", "wireframes"],
|
|
316
|
+
type: "custom",
|
|
317
|
+
editable: true,
|
|
318
|
+
systemPrompt: "You are Alpha Custom.",
|
|
319
|
+
});
|
|
320
|
+
const builtinDetailResponse = await fetch(`${baseUrl}/api/agents/${encodeURIComponent("designer")}`, {
|
|
321
|
+
headers: { authorization: authHeader },
|
|
322
|
+
});
|
|
323
|
+
assert.equal(builtinDetailResponse.status, 200);
|
|
324
|
+
const builtinDetail = await builtinDetailResponse.json();
|
|
325
|
+
assert.equal(builtinDetail.slug, "designer");
|
|
326
|
+
assert.equal(builtinDetail.type, "builtin");
|
|
327
|
+
assert.equal(builtinDetail.editable, false);
|
|
328
|
+
assert.equal(typeof builtinDetail.systemPrompt, "string");
|
|
329
|
+
assert.ok(builtinDetail.systemPrompt.length > 0);
|
|
330
|
+
const missingResponse = await fetch(`${baseUrl}/api/agents/${encodeURIComponent("missing-agent")}`, {
|
|
331
|
+
headers: { authorization: authHeader },
|
|
332
|
+
});
|
|
333
|
+
assert.equal(missingResponse.status, 404);
|
|
334
|
+
assert.deepEqual(await missingResponse.json(), { error: "Agent 'missing-agent' not found" });
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
test("server rejects invalid agent slugs before reading agent files", async () => {
|
|
338
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
339
|
+
writeFileSync(join(testRoot, "some-path.agent.md"), [
|
|
340
|
+
"---",
|
|
341
|
+
"name: Escaped Agent",
|
|
342
|
+
"description: Should never be readable",
|
|
343
|
+
"model: gpt-5.4",
|
|
344
|
+
"---",
|
|
345
|
+
"",
|
|
346
|
+
"You should not see this.",
|
|
347
|
+
].join("\n"), "utf-8");
|
|
348
|
+
const response = await fetch(`${baseUrl}/api/agents/..%2F..%2Fsome-path`, {
|
|
349
|
+
headers: { authorization: authHeader },
|
|
350
|
+
});
|
|
351
|
+
assert.equal(response.status, 400);
|
|
352
|
+
assert.deepEqual(await response.json(), { error: "Invalid slug" });
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
test("server patches a custom agent file and reloads the persistent agent registry", async () => {
|
|
356
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
357
|
+
writeAgentFile(testRoot, "scribe", [
|
|
358
|
+
"---",
|
|
359
|
+
"name: Scribe",
|
|
360
|
+
"description: Drafts release notes",
|
|
361
|
+
"model: claude-sonnet-4.6",
|
|
362
|
+
"persistent: true",
|
|
363
|
+
"scope: docs",
|
|
364
|
+
"skills:",
|
|
365
|
+
" - writing",
|
|
366
|
+
"tools:",
|
|
367
|
+
" - bash",
|
|
368
|
+
"---",
|
|
369
|
+
"",
|
|
370
|
+
"Original system prompt.",
|
|
371
|
+
].join("\n"));
|
|
372
|
+
const before = await fetch(`${baseUrl}/api/channels`, {
|
|
373
|
+
headers: { authorization: authHeader },
|
|
374
|
+
});
|
|
375
|
+
assert.equal(before.status, 200);
|
|
376
|
+
const response = await fetch(`${baseUrl}/api/agents/scribe`, {
|
|
377
|
+
method: "PATCH",
|
|
378
|
+
headers: {
|
|
379
|
+
authorization: authHeader,
|
|
380
|
+
"content-type": "application/json",
|
|
381
|
+
},
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
name: "Scribe Prime",
|
|
384
|
+
description: "Publishes saved agent edits",
|
|
385
|
+
systemPrompt: "# Updated Prompt\n\nShip it.",
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
assert.equal(response.status, 200);
|
|
389
|
+
assert.deepEqual(await response.json(), {
|
|
390
|
+
name: "Scribe Prime",
|
|
391
|
+
slug: "scribe",
|
|
392
|
+
description: "Publishes saved agent edits",
|
|
393
|
+
model: "claude-sonnet-4.6",
|
|
394
|
+
scope: "docs",
|
|
395
|
+
persistent: true,
|
|
396
|
+
skills: ["writing"],
|
|
397
|
+
type: "custom",
|
|
398
|
+
editable: true,
|
|
399
|
+
systemPrompt: "# Updated Prompt\n\nShip it.",
|
|
400
|
+
});
|
|
401
|
+
const saved = readFileSync(getAgentFilePath(testRoot, "scribe"), "utf-8");
|
|
402
|
+
assert.match(saved, /^---\n[\s\S]*\n---\n\n# Updated Prompt\n\nShip it\.\n?$/);
|
|
403
|
+
assert.match(saved, /name: Scribe Prime/);
|
|
404
|
+
assert.match(saved, /description: Publishes saved agent edits/);
|
|
405
|
+
assert.match(saved, /model: claude-sonnet-4.6/);
|
|
406
|
+
assert.match(saved, /scope: docs/);
|
|
407
|
+
assert.match(saved, /tools:\n - bash/);
|
|
408
|
+
assert.match(saved, /skills:\n - writing/);
|
|
409
|
+
const after = await fetch(`${baseUrl}/api/channels`, {
|
|
410
|
+
headers: { authorization: authHeader },
|
|
411
|
+
});
|
|
412
|
+
assert.equal(after.status, 200);
|
|
413
|
+
const channels = await after.json();
|
|
414
|
+
assert.deepEqual(channels.find((channel) => channel.slug === "scribe"), {
|
|
415
|
+
key: "agent:scribe",
|
|
416
|
+
label: "# scribe",
|
|
417
|
+
slug: "scribe",
|
|
418
|
+
name: "Scribe Prime",
|
|
419
|
+
description: "Publishes saved agent edits",
|
|
420
|
+
scope: "docs",
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
test("server rejects PATCH /api/agents/:slug when the request changes skills", async () => {
|
|
425
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
426
|
+
writeAgentFile(testRoot, "scribe", [
|
|
427
|
+
"---",
|
|
428
|
+
"name: Scribe",
|
|
429
|
+
"description: Drafts release notes",
|
|
430
|
+
"model: claude-sonnet-4.6",
|
|
431
|
+
"skills:",
|
|
432
|
+
" - writing",
|
|
433
|
+
"---",
|
|
434
|
+
"",
|
|
435
|
+
"Original system prompt.",
|
|
436
|
+
].join("\n"));
|
|
437
|
+
const response = await fetch(`${baseUrl}/api/agents/scribe`, {
|
|
438
|
+
method: "PATCH",
|
|
439
|
+
headers: {
|
|
440
|
+
authorization: authHeader,
|
|
441
|
+
"content-type": "application/json",
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify({ skills: ["editing", "review"] }),
|
|
444
|
+
});
|
|
445
|
+
assert.equal(response.status, 400);
|
|
446
|
+
assert.match((await response.json()).error, /skills/i);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
test("server rejects PATCH /api/agents/:slug when the request changes read-only fields", async () => {
|
|
450
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
451
|
+
writeAgentFile(testRoot, "scribe", [
|
|
452
|
+
"---",
|
|
453
|
+
"name: Scribe",
|
|
454
|
+
"description: Drafts release notes",
|
|
455
|
+
"model: claude-sonnet-4.6",
|
|
456
|
+
"---",
|
|
457
|
+
"",
|
|
458
|
+
"Original system prompt.",
|
|
459
|
+
].join("\n"));
|
|
460
|
+
const response = await fetch(`${baseUrl}/api/agents/scribe`, {
|
|
461
|
+
method: "PATCH",
|
|
462
|
+
headers: {
|
|
463
|
+
authorization: authHeader,
|
|
464
|
+
"content-type": "application/json",
|
|
465
|
+
},
|
|
466
|
+
body: JSON.stringify({ slug: "other-scribe" }),
|
|
467
|
+
});
|
|
468
|
+
assert.equal(response.status, 400);
|
|
469
|
+
assert.match((await response.json()).error, /slug/i);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
test("server rejects PATCH /api/agents/:slug for bundled agents", async () => {
|
|
473
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
474
|
+
const response = await fetch(`${baseUrl}/api/agents/designer`, {
|
|
475
|
+
method: "PATCH",
|
|
476
|
+
headers: {
|
|
477
|
+
authorization: authHeader,
|
|
478
|
+
"content-type": "application/json",
|
|
479
|
+
},
|
|
480
|
+
body: JSON.stringify({ description: "Should not save" }),
|
|
481
|
+
});
|
|
482
|
+
assert.equal(response.status, 403);
|
|
483
|
+
assert.deepEqual(await response.json(), { error: "Built-in agents are read-only" });
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
test("server rejects PATCH /api/agents/:slug for non-team-leads in team mode", async () => {
|
|
487
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
488
|
+
writeAgentFile(testRoot, "scribe", [
|
|
489
|
+
"---",
|
|
490
|
+
"name: Scribe",
|
|
491
|
+
"description: Drafts release notes",
|
|
492
|
+
"model: claude-sonnet-4.6",
|
|
493
|
+
"---",
|
|
494
|
+
"",
|
|
495
|
+
"Original system prompt.",
|
|
496
|
+
].join("\n"));
|
|
497
|
+
const response = await fetch(`${baseUrl}/api/agents/scribe`, {
|
|
498
|
+
method: "PATCH",
|
|
499
|
+
headers: {
|
|
500
|
+
authorization: authHeader,
|
|
501
|
+
"content-type": "application/json",
|
|
502
|
+
},
|
|
503
|
+
body: JSON.stringify({ description: "Should not save" }),
|
|
504
|
+
});
|
|
505
|
+
assert.equal(response.status, 403);
|
|
506
|
+
assert.deepEqual(await response.json(), { error: "Forbidden" });
|
|
507
|
+
}, {
|
|
508
|
+
CHAPTERHOUSE_MODE: "team",
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
test("server returns 404 when PATCH /api/agents/:slug targets a missing file", async () => {
|
|
512
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
513
|
+
const response = await fetch(`${baseUrl}/api/agents/missing-agent`, {
|
|
514
|
+
method: "PATCH",
|
|
515
|
+
headers: {
|
|
516
|
+
authorization: authHeader,
|
|
517
|
+
"content-type": "application/json",
|
|
518
|
+
},
|
|
519
|
+
body: JSON.stringify({ description: "Still missing" }),
|
|
520
|
+
});
|
|
521
|
+
assert.equal(response.status, 404);
|
|
522
|
+
assert.deepEqual(await response.json(), { error: "Agent not found" });
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
test("server returns 400 when PATCH /api/agents/:slug cannot parse malformed agent content", async () => {
|
|
526
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
527
|
+
writeAgentFile(testRoot, "scribe", [
|
|
528
|
+
"---",
|
|
529
|
+
"name: Scribe",
|
|
530
|
+
"description: Drafts release notes",
|
|
531
|
+
"model: claude-sonnet-4.6",
|
|
532
|
+
"skills: [writing",
|
|
533
|
+
"---",
|
|
534
|
+
"",
|
|
535
|
+
"Original system prompt.",
|
|
536
|
+
].join("\n"));
|
|
537
|
+
const response = await fetch(`${baseUrl}/api/agents/scribe`, {
|
|
538
|
+
method: "PATCH",
|
|
539
|
+
headers: {
|
|
540
|
+
authorization: authHeader,
|
|
541
|
+
"content-type": "application/json",
|
|
542
|
+
},
|
|
543
|
+
body: JSON.stringify({ description: "Broken input stays broken" }),
|
|
544
|
+
});
|
|
545
|
+
assert.equal(response.status, 400);
|
|
546
|
+
assert.match((await response.json()).error, /^Invalid content:/);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
test("server returns 404 when confirming reload for an unknown agent", async () => {
|
|
550
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
551
|
+
const response = await fetch(`${baseUrl}/api/agents/missing/reload-confirm`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: { authorization: authHeader },
|
|
554
|
+
});
|
|
555
|
+
assert.equal(response.status, 404);
|
|
556
|
+
assert.match(await response.text(), /Agent not found/i);
|
|
223
557
|
});
|
|
224
558
|
});
|
|
225
559
|
test("server runs in standalone mode without auth", async () => {
|
|
@@ -916,17 +1250,17 @@ test("server caps concurrent SSE connections per IP", async () => {
|
|
|
916
1250
|
let secondResponse;
|
|
917
1251
|
try {
|
|
918
1252
|
firstResponse = await fetch(`${baseUrl}/stream`, {
|
|
919
|
-
headers: { authorization: authHeader },
|
|
1253
|
+
headers: { authorization: authHeader, connection: "close" },
|
|
920
1254
|
signal: firstController.signal,
|
|
921
1255
|
});
|
|
922
1256
|
secondResponse = await fetch(`${baseUrl}/stream`, {
|
|
923
|
-
headers: { authorization: authHeader },
|
|
1257
|
+
headers: { authorization: authHeader, connection: "close" },
|
|
924
1258
|
signal: secondController.signal,
|
|
925
1259
|
});
|
|
926
1260
|
assert.equal(firstResponse.status, 200);
|
|
927
1261
|
assert.equal(secondResponse.status, 200);
|
|
928
1262
|
const rejected = await fetch(`${baseUrl}/stream`, {
|
|
929
|
-
headers: { authorization: authHeader },
|
|
1263
|
+
headers: { authorization: authHeader, connection: "close" },
|
|
930
1264
|
});
|
|
931
1265
|
assert.equal(rejected.status, 429);
|
|
932
1266
|
assert.equal(rejected.headers.get("retry-after"), "60");
|
|
@@ -946,4 +1280,200 @@ test("server caps concurrent SSE connections per IP", async () => {
|
|
|
946
1280
|
API_RATE_LIMIT_SSE_MAX_CONNECTIONS: "2",
|
|
947
1281
|
});
|
|
948
1282
|
});
|
|
1283
|
+
test("POST /api/memory/active-scope sets and clears the active scope", async () => {
|
|
1284
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1285
|
+
const unauthorized = await fetch(`${baseUrl}/api/memory/active-scope`, {
|
|
1286
|
+
method: "POST",
|
|
1287
|
+
headers: { "content-type": "application/json" },
|
|
1288
|
+
body: JSON.stringify({ scope: "chapterhouse" }),
|
|
1289
|
+
});
|
|
1290
|
+
assert.equal(unauthorized.status, 401);
|
|
1291
|
+
const set = await fetch(`${baseUrl}/api/memory/active-scope`, {
|
|
1292
|
+
method: "POST",
|
|
1293
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1294
|
+
body: JSON.stringify({ scope: "chapterhouse" }),
|
|
1295
|
+
});
|
|
1296
|
+
assert.equal(set.status, 200);
|
|
1297
|
+
assert.deepEqual(await set.json(), { ok: true, scope: "chapterhouse" });
|
|
1298
|
+
const verify = await fetch(`${baseUrl}/api/memory/active-scope`, {
|
|
1299
|
+
headers: { authorization: authHeader },
|
|
1300
|
+
});
|
|
1301
|
+
assert.deepEqual(await verify.json(), { slug: "chapterhouse", title: "Chapterhouse" });
|
|
1302
|
+
const clear = await fetch(`${baseUrl}/api/memory/active-scope`, {
|
|
1303
|
+
method: "POST",
|
|
1304
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1305
|
+
body: JSON.stringify({ scope: null }),
|
|
1306
|
+
});
|
|
1307
|
+
assert.equal(clear.status, 200);
|
|
1308
|
+
assert.deepEqual(await clear.json(), { ok: true, scope: null });
|
|
1309
|
+
const unknownScope = await fetch(`${baseUrl}/api/memory/active-scope`, {
|
|
1310
|
+
method: "POST",
|
|
1311
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1312
|
+
body: JSON.stringify({ scope: "nonexistent-scope" }),
|
|
1313
|
+
});
|
|
1314
|
+
assert.equal(unknownScope.status, 404);
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
test("GET /api/memory/scopes lists all scopes with entry counts", async () => {
|
|
1318
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1319
|
+
const unauthorized = await fetch(`${baseUrl}/api/memory/scopes`);
|
|
1320
|
+
assert.equal(unauthorized.status, 401);
|
|
1321
|
+
const res = await fetch(`${baseUrl}/api/memory/scopes`, {
|
|
1322
|
+
headers: { authorization: authHeader },
|
|
1323
|
+
});
|
|
1324
|
+
assert.equal(res.status, 200);
|
|
1325
|
+
const body = await res.json();
|
|
1326
|
+
assert.ok(Array.isArray(body.scopes));
|
|
1327
|
+
assert.ok(body.scopes.length >= 2, "expected at least 2 seeded scopes");
|
|
1328
|
+
const chapterhouse = body.scopes.find((s) => s.slug === "chapterhouse");
|
|
1329
|
+
assert.ok(chapterhouse, "expected chapterhouse scope");
|
|
1330
|
+
assert.ok(typeof chapterhouse.counts.observations === "number");
|
|
1331
|
+
assert.ok(typeof chapterhouse.counts.decisions === "number");
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
test("GET /api/memory/:scope returns entries for a scope and 404 for unknown", async () => {
|
|
1335
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1336
|
+
const unauthorized = await fetch(`${baseUrl}/api/memory/chapterhouse`);
|
|
1337
|
+
assert.equal(unauthorized.status, 401);
|
|
1338
|
+
const res = await fetch(`${baseUrl}/api/memory/chapterhouse`, {
|
|
1339
|
+
headers: { authorization: authHeader },
|
|
1340
|
+
});
|
|
1341
|
+
assert.equal(res.status, 200);
|
|
1342
|
+
const body = await res.json();
|
|
1343
|
+
assert.ok(Array.isArray(body.entries));
|
|
1344
|
+
assert.ok(typeof body.total === "number");
|
|
1345
|
+
const withStore = await fetch(`${baseUrl}/api/memory/chapterhouse?store=decisions`, {
|
|
1346
|
+
headers: { authorization: authHeader },
|
|
1347
|
+
});
|
|
1348
|
+
assert.equal(withStore.status, 200);
|
|
1349
|
+
const withTier = await fetch(`${baseUrl}/api/memory/chapterhouse?store=observations&tier=hot`, {
|
|
1350
|
+
headers: { authorization: authHeader },
|
|
1351
|
+
});
|
|
1352
|
+
assert.equal(withTier.status, 200);
|
|
1353
|
+
const notFound = await fetch(`${baseUrl}/api/memory/no-such-scope`, {
|
|
1354
|
+
headers: { authorization: authHeader },
|
|
1355
|
+
});
|
|
1356
|
+
assert.equal(notFound.status, 404);
|
|
1357
|
+
assert.deepEqual(await notFound.json(), { error: "Memory scope 'no-such-scope' not found" });
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
test("POST /api/memory/:scope/remember writes an observation or decision", async () => {
|
|
1361
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1362
|
+
const unauthorized = await fetch(`${baseUrl}/api/memory/chapterhouse/remember`, {
|
|
1363
|
+
method: "POST",
|
|
1364
|
+
headers: { "content-type": "application/json" },
|
|
1365
|
+
body: JSON.stringify({ content: "Test observation" }),
|
|
1366
|
+
});
|
|
1367
|
+
assert.equal(unauthorized.status, 401);
|
|
1368
|
+
const obs = await fetch(`${baseUrl}/api/memory/chapterhouse/remember`, {
|
|
1369
|
+
method: "POST",
|
|
1370
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1371
|
+
body: JSON.stringify({ content: "Chapterhouse uses SQLite for memory." }),
|
|
1372
|
+
});
|
|
1373
|
+
assert.equal(obs.status, 200);
|
|
1374
|
+
const obsBody = await obs.json();
|
|
1375
|
+
assert.equal(obsBody.ok, true);
|
|
1376
|
+
assert.ok(typeof obsBody.id === "string");
|
|
1377
|
+
const dec = await fetch(`${baseUrl}/api/memory/chapterhouse/remember`, {
|
|
1378
|
+
method: "POST",
|
|
1379
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1380
|
+
body: JSON.stringify({ kind: "decision", title: "Use SQLite", content: "SQLite chosen for persistence." }),
|
|
1381
|
+
});
|
|
1382
|
+
assert.equal(dec.status, 200);
|
|
1383
|
+
const decBody = await dec.json();
|
|
1384
|
+
assert.equal(decBody.ok, true);
|
|
1385
|
+
const noTitle = await fetch(`${baseUrl}/api/memory/chapterhouse/remember`, {
|
|
1386
|
+
method: "POST",
|
|
1387
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1388
|
+
body: JSON.stringify({ kind: "decision", content: "Missing title." }),
|
|
1389
|
+
});
|
|
1390
|
+
assert.equal(noTitle.status, 400);
|
|
1391
|
+
const notFound = await fetch(`${baseUrl}/api/memory/missing-scope/remember`, {
|
|
1392
|
+
method: "POST",
|
|
1393
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1394
|
+
body: JSON.stringify({ content: "Will not land." }),
|
|
1395
|
+
});
|
|
1396
|
+
assert.equal(notFound.status, 404);
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
test("GET /api/memory/inbox lists pending proposals", async () => {
|
|
1400
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1401
|
+
const unauthorized = await fetch(`${baseUrl}/api/memory/inbox`);
|
|
1402
|
+
assert.equal(unauthorized.status, 401);
|
|
1403
|
+
const empty = await fetch(`${baseUrl}/api/memory/inbox`, {
|
|
1404
|
+
headers: { authorization: authHeader },
|
|
1405
|
+
});
|
|
1406
|
+
assert.equal(empty.status, 200);
|
|
1407
|
+
const body = await empty.json();
|
|
1408
|
+
assert.ok(Array.isArray(body.items));
|
|
1409
|
+
assert.ok(typeof body.total === "number");
|
|
1410
|
+
assert.equal(body.items.length, body.total);
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
test("POST /api/memory/inbox/:id/route accepts and rejects proposals", async () => {
|
|
1414
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1415
|
+
// First queue a proposal via the remember endpoint so we have something to route
|
|
1416
|
+
const rememberRes = await fetch(`${baseUrl}/api/memory/chapterhouse/remember`, {
|
|
1417
|
+
method: "POST",
|
|
1418
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1419
|
+
body: JSON.stringify({ content: "Proposal for inbox routing test." }),
|
|
1420
|
+
});
|
|
1421
|
+
assert.equal(rememberRes.status, 200);
|
|
1422
|
+
const notFound = await fetch(`${baseUrl}/api/memory/inbox/99999/route`, {
|
|
1423
|
+
method: "POST",
|
|
1424
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1425
|
+
body: JSON.stringify({ action: "accept" }),
|
|
1426
|
+
});
|
|
1427
|
+
assert.equal(notFound.status, 404);
|
|
1428
|
+
const badId = await fetch(`${baseUrl}/api/memory/inbox/not-a-number/route`, {
|
|
1429
|
+
method: "POST",
|
|
1430
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
1431
|
+
body: JSON.stringify({ action: "accept" }),
|
|
1432
|
+
});
|
|
1433
|
+
assert.equal(badId.status, 400);
|
|
1434
|
+
const unauthorized = await fetch(`${baseUrl}/api/memory/inbox/1/route`, {
|
|
1435
|
+
method: "POST",
|
|
1436
|
+
headers: { "content-type": "application/json" },
|
|
1437
|
+
body: JSON.stringify({ action: "accept" }),
|
|
1438
|
+
});
|
|
1439
|
+
assert.equal(unauthorized.status, 401);
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
test("GET /api/wiki/korg/sessions returns grouped active research sessions", async () => {
|
|
1443
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
1444
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
1445
|
+
try {
|
|
1446
|
+
db.prepare(`
|
|
1447
|
+
INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
|
|
1448
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1449
|
+
`).run("src-1", "text", "session-one", "First source", "2026-05-14T21:00:00.000Z", "sources/src-1.md", "open questions remain", "[]", "active", "compiler-research", "Compiler research");
|
|
1450
|
+
db.prepare(`
|
|
1451
|
+
INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
|
|
1452
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1453
|
+
`).run("src-2", "text", "session-one-b", "Second source", "2026-05-14T22:00:00.000Z", "sources/src-2.md", "follow-up questions", "[]", "active", "compiler-research", "Compiler research");
|
|
1454
|
+
db.prepare(`
|
|
1455
|
+
INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
|
|
1456
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1457
|
+
`).run("src-3", "text", "archived-session", "Archived source", "2026-05-13T20:00:00.000Z", "sources/src-3.md", "done", "[]", "archived", "old-session", "Old session");
|
|
1458
|
+
}
|
|
1459
|
+
finally {
|
|
1460
|
+
db.close();
|
|
1461
|
+
}
|
|
1462
|
+
const response = await fetch(`${baseUrl}/api/wiki/korg/sessions`, {
|
|
1463
|
+
headers: { authorization: authHeader },
|
|
1464
|
+
});
|
|
1465
|
+
assert.equal(response.status, 200);
|
|
1466
|
+
assert.deepEqual(await response.json(), {
|
|
1467
|
+
sessions: [
|
|
1468
|
+
{
|
|
1469
|
+
id: "compiler-research",
|
|
1470
|
+
name: "Compiler research",
|
|
1471
|
+
source_count: 2,
|
|
1472
|
+
open_questions: 0,
|
|
1473
|
+
last_activity: "2026-05-14T22:00:00.000Z",
|
|
1474
|
+
},
|
|
1475
|
+
],
|
|
1476
|
+
});
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
949
1479
|
//# sourceMappingURL=server.test.js.map
|
package/dist/config.js
CHANGED
|
@@ -58,6 +58,7 @@ const configSchema = z.object({
|
|
|
58
58
|
CHAPTERHOUSE_MEMORY_INJECT: z.string().optional(),
|
|
59
59
|
CHAPTERHOUSE_MEMORY_AUTO_ACCEPT: z.string().optional(),
|
|
60
60
|
CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED: z.string().optional(),
|
|
61
|
+
CHAPTERHOUSE_MEMORY_HOOKS_ENABLED: z.string().optional(),
|
|
61
62
|
CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED: z.string().optional(),
|
|
62
63
|
CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS: z.string().optional(),
|
|
63
64
|
CHAPTERHOUSE_MEMORY_DECAY_DAYS: z.string().optional(),
|
|
@@ -65,6 +66,8 @@ const configSchema = z.object({
|
|
|
65
66
|
CHAPTERHOUSE_MEMORY_TIERING_ENABLED: z.string().optional(),
|
|
66
67
|
CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST: z.string().optional(),
|
|
67
68
|
CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS: z.string().optional(),
|
|
69
|
+
CHAPTERHOUSE_PKB_CONSOLIDATION_ENABLED: z.string().optional(),
|
|
70
|
+
CHAPTERHOUSE_PKB_CONSOLIDATION_HOUR: z.string().optional(),
|
|
68
71
|
});
|
|
69
72
|
export const DEFAULT_MODEL = "claude-sonnet-4.6";
|
|
70
73
|
export const DEFAULT_TEAM_WIKI_CACHE_TTL_MINUTES = 60;
|
|
@@ -140,6 +143,17 @@ function parsePositiveNumberEnv(name, rawValue, defaultValue) {
|
|
|
140
143
|
}
|
|
141
144
|
return parsed;
|
|
142
145
|
}
|
|
146
|
+
function parseHourEnv(name, rawValue, defaultValue) {
|
|
147
|
+
const normalized = rawValue?.trim();
|
|
148
|
+
if (!normalized) {
|
|
149
|
+
return defaultValue;
|
|
150
|
+
}
|
|
151
|
+
const parsed = Number(normalized);
|
|
152
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 23) {
|
|
153
|
+
throw new Error(`${name} must be an integer between 0 and 23, got: "${rawValue}"`);
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
143
157
|
function parseCorsAllowedOrigins(rawValue) {
|
|
144
158
|
const normalized = rawValue?.trim();
|
|
145
159
|
if (!normalized) {
|
|
@@ -298,6 +312,8 @@ export function parseRuntimeConfig(env, options = {}) {
|
|
|
298
312
|
const memoryInboxRetentionDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS", raw.CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS, 7);
|
|
299
313
|
const memoryHotRecallBoost = parsePositiveNumberEnv("CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST", raw.CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST, 1.5);
|
|
300
314
|
const memoryHotAgeDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS", raw.CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS, 30);
|
|
315
|
+
const pkbConsolidationEnabled = parseBooleanEnv("CHAPTERHOUSE_PKB_CONSOLIDATION_ENABLED", raw.CHAPTERHOUSE_PKB_CONSOLIDATION_ENABLED, true);
|
|
316
|
+
const pkbConsolidationHour = parseHourEnv("CHAPTERHOUSE_PKB_CONSOLIDATION_HOUR", raw.CHAPTERHOUSE_PKB_CONSOLIDATION_HOUR, 3);
|
|
301
317
|
if (effectiveEntraAuthEnabled && (!effectiveEntraTenantId || !effectiveEntraClientId)) {
|
|
302
318
|
throw new Error("ENTRA_AUTH_ENABLED=true requires ENTRA_TENANT_ID and ENTRA_CLIENT_ID");
|
|
303
319
|
}
|
|
@@ -348,6 +364,7 @@ export function parseRuntimeConfig(env, options = {}) {
|
|
|
348
364
|
memoryInjectEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_INJECT", raw.CHAPTERHOUSE_MEMORY_INJECT, true),
|
|
349
365
|
memoryAutoAcceptEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_AUTO_ACCEPT", raw.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT, true),
|
|
350
366
|
memoryEndOfTaskHookEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED", raw.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED, true),
|
|
367
|
+
memoryHooksEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_HOOKS_ENABLED", raw.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED, true),
|
|
351
368
|
memoryHousekeepingEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED", raw.CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED, true),
|
|
352
369
|
memoryHousekeepingTurns,
|
|
353
370
|
memoryDecayDays,
|
|
@@ -355,6 +372,8 @@ export function parseRuntimeConfig(env, options = {}) {
|
|
|
355
372
|
memoryTieringEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_TIERING_ENABLED", raw.CHAPTERHOUSE_MEMORY_TIERING_ENABLED, true),
|
|
356
373
|
memoryHotRecallBoost,
|
|
357
374
|
memoryHotAgeDays,
|
|
375
|
+
pkbConsolidationEnabled,
|
|
376
|
+
pkbConsolidationHour,
|
|
358
377
|
modeCompatibilityWarnings,
|
|
359
378
|
};
|
|
360
379
|
}
|
|
@@ -406,6 +425,7 @@ export const config = {
|
|
|
406
425
|
memoryInjectEnabled: runtimeConfig.memoryInjectEnabled,
|
|
407
426
|
memoryAutoAcceptEnabled: runtimeConfig.memoryAutoAcceptEnabled,
|
|
408
427
|
memoryEndOfTaskHookEnabled: runtimeConfig.memoryEndOfTaskHookEnabled,
|
|
428
|
+
memoryHooksEnabled: runtimeConfig.memoryHooksEnabled,
|
|
409
429
|
memoryHousekeepingEnabled: runtimeConfig.memoryHousekeepingEnabled,
|
|
410
430
|
memoryHousekeepingTurns: runtimeConfig.memoryHousekeepingTurns,
|
|
411
431
|
memoryDecayDays: runtimeConfig.memoryDecayDays,
|
|
@@ -413,6 +433,14 @@ export const config = {
|
|
|
413
433
|
memoryTieringEnabled: runtimeConfig.memoryTieringEnabled,
|
|
414
434
|
memoryHotRecallBoost: runtimeConfig.memoryHotRecallBoost,
|
|
415
435
|
memoryHotAgeDays: runtimeConfig.memoryHotAgeDays,
|
|
436
|
+
pkbConsolidationEnabled: runtimeConfig.pkbConsolidationEnabled,
|
|
437
|
+
pkbConsolidationHour: runtimeConfig.pkbConsolidationHour,
|
|
438
|
+
get PKB_CONSOLIDATION_ENABLED() {
|
|
439
|
+
return runtimeConfig.pkbConsolidationEnabled;
|
|
440
|
+
},
|
|
441
|
+
get PKB_CONSOLIDATION_HOUR() {
|
|
442
|
+
return runtimeConfig.pkbConsolidationHour;
|
|
443
|
+
},
|
|
416
444
|
modeCompatibilityWarnings: runtimeConfig.modeCompatibilityWarnings,
|
|
417
445
|
copilotAuthToken: runtimeConfig.copilotAuthToken,
|
|
418
446
|
get copilotModel() {
|