chapterhouse 0.3.26 → 0.4.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/dist/api/server.js +12 -0
- package/dist/api/server.test.js +39 -0
- package/dist/config.js +70 -0
- package/dist/config.test.js +109 -0
- package/dist/copilot/agents.js +27 -4
- package/dist/copilot/agents.test.js +7 -0
- package/dist/copilot/oneshot.js +54 -0
- package/dist/copilot/orchestrator.js +227 -3
- package/dist/copilot/orchestrator.test.js +372 -0
- package/dist/copilot/system-message.js +4 -0
- package/dist/copilot/system-message.test.js +24 -0
- package/dist/copilot/tools.agent.test.js +23 -0
- package/dist/copilot/tools.js +350 -4
- package/dist/copilot/tools.memory.test.js +248 -0
- package/dist/copilot/turn-event-log-env.test.js +19 -0
- package/dist/copilot/turn-event-log.js +22 -23
- package/dist/copilot/turn-event-log.test.js +61 -2
- package/dist/memory/active-scope.js +69 -0
- package/dist/memory/active-scope.test.js +76 -0
- package/dist/memory/checkpoint-prompt.js +71 -0
- package/dist/memory/checkpoint.js +257 -0
- package/dist/memory/checkpoint.test.js +255 -0
- package/dist/memory/decisions.js +53 -0
- package/dist/memory/decisions.test.js +92 -0
- package/dist/memory/entities.js +59 -0
- package/dist/memory/entities.test.js +65 -0
- package/dist/memory/eot.js +219 -0
- package/dist/memory/eot.test.js +263 -0
- package/dist/memory/hot-tier.js +187 -0
- package/dist/memory/hot-tier.test.js +197 -0
- package/dist/memory/housekeeping.js +352 -0
- package/dist/memory/housekeeping.test.js +280 -0
- package/dist/memory/inbox.js +73 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/observations.js +46 -0
- package/dist/memory/observations.test.js +86 -0
- package/dist/memory/recall.js +197 -0
- package/dist/memory/recall.test.js +196 -0
- package/dist/memory/scopes.js +89 -0
- package/dist/memory/scopes.test.js +201 -0
- package/dist/memory/tiering.js +193 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.js +7 -1
- package/dist/store/db.js +412 -8
- package/dist/store/db.test.js +83 -0
- package/dist/test/setup-env.js +16 -0
- package/dist/test/setup-env.test.js +4 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-BRPJa1DK.js → index-DmYLALt0.js} +70 -70
- package/web/dist/assets/index-DmYLALt0.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-BRPJa1DK.js.map +0 -1
package/dist/api/server.js
CHANGED
|
@@ -31,6 +31,7 @@ import { formatSseData, formatSseEvent } from "./sse.js";
|
|
|
31
31
|
import { assertAuthenticationConfigured, createHealthPayload, createPublicConfigPayload, getDisplayHost, resolveApiToken, shouldServeSpaPath, } from "./server-runtime.js";
|
|
32
32
|
import { BadRequestError, ForbiddenError, InternalServerError, NotFoundError, apiNotFoundHandler, asBadRequest, createApiErrorHandler, parseRequest, } from "./errors.js";
|
|
33
33
|
import { childLogger } from "../util/logger.js";
|
|
34
|
+
import { getActiveScope } from "../memory/active-scope.js";
|
|
34
35
|
const log = childLogger("server");
|
|
35
36
|
void searchIndex; // re-exported by index-manager; reference here documents the dep
|
|
36
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -838,6 +839,17 @@ app.get("/api/auto", (_req, res) => {
|
|
|
838
839
|
lastRoute: lastRoute || null,
|
|
839
840
|
});
|
|
840
841
|
});
|
|
842
|
+
app.get("/api/memory/active-scope", (_req, res) => {
|
|
843
|
+
const activeScope = getActiveScope();
|
|
844
|
+
if (!activeScope) {
|
|
845
|
+
res.json(null);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
res.json({
|
|
849
|
+
slug: activeScope.slug,
|
|
850
|
+
title: activeScope.title,
|
|
851
|
+
});
|
|
852
|
+
});
|
|
841
853
|
app.post("/api/auto", (req, res) => {
|
|
842
854
|
const body = parseRequest(autoRequestSchema, req.body ?? {});
|
|
843
855
|
const updated = updateRouterConfig(body);
|
package/dist/api/server.test.js
CHANGED
|
@@ -124,6 +124,25 @@ function readProjectRegistryRows(testRoot) {
|
|
|
124
124
|
db.close();
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
function setMemoryActiveScope(testRoot, slug) {
|
|
128
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
129
|
+
try {
|
|
130
|
+
if (slug === null) {
|
|
131
|
+
db.prepare(`DELETE FROM mem_settings WHERE key = ?`).run("current_scope_slug");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const scope = db.prepare(`SELECT slug FROM mem_scopes WHERE slug = ?`).get(slug);
|
|
135
|
+
assert.ok(scope, `expected seeded memory scope '${slug}' to exist`);
|
|
136
|
+
db.prepare(`
|
|
137
|
+
INSERT INTO mem_settings (key, value)
|
|
138
|
+
VALUES (?, ?)
|
|
139
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
140
|
+
`).run("current_scope_slug", slug);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
db.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
127
146
|
// Standalone mode triggers a full daemon init via the server→daemon circular import
|
|
128
147
|
// (server.ts imports restartDaemon from daemon.ts, which has module-level main()).
|
|
129
148
|
// The Copilot SDK's client.start() then blocks the event loop while authenticating,
|
|
@@ -208,6 +227,26 @@ test("server runs in standalone mode without auth", async () => {
|
|
|
208
227
|
API_TOKEN: "",
|
|
209
228
|
}, STANDALONE_API_SERVER_STARTUP_TIMEOUT_MS);
|
|
210
229
|
});
|
|
230
|
+
test("server exposes the active memory scope API and requires auth", async () => {
|
|
231
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
232
|
+
const unauthorized = await fetch(`${baseUrl}/api/memory/active-scope`);
|
|
233
|
+
assert.equal(unauthorized.status, 401);
|
|
234
|
+
const noScope = await fetch(`${baseUrl}/api/memory/active-scope`, {
|
|
235
|
+
headers: { authorization: authHeader },
|
|
236
|
+
});
|
|
237
|
+
assert.equal(noScope.status, 200);
|
|
238
|
+
assert.equal(await noScope.json(), null);
|
|
239
|
+
setMemoryActiveScope(testRoot, "chapterhouse");
|
|
240
|
+
const activeScope = await fetch(`${baseUrl}/api/memory/active-scope`, {
|
|
241
|
+
headers: { authorization: authHeader },
|
|
242
|
+
});
|
|
243
|
+
assert.equal(activeScope.status, 200);
|
|
244
|
+
assert.deepEqual(await activeScope.json(), {
|
|
245
|
+
slug: "chapterhouse",
|
|
246
|
+
title: "Chapterhouse",
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
211
250
|
test("server bootstrap rejects non-loopback origins", async () => {
|
|
212
251
|
await withStartedServer(async ({ baseUrl }) => {
|
|
213
252
|
const response = await fetch(`${baseUrl}/api/bootstrap`, {
|
package/dist/config.js
CHANGED
|
@@ -46,8 +46,24 @@ const configSchema = z.object({
|
|
|
46
46
|
API_RATE_LIMIT_GENERAL_MAX: z.string().optional(),
|
|
47
47
|
API_RATE_LIMIT_AUTH_MAX: z.string().optional(),
|
|
48
48
|
API_RATE_LIMIT_SSE_MAX_CONNECTIONS: z.string().optional(),
|
|
49
|
+
CHAPTERHOUSE_SSE_BUFFER_CAPACITY: z.string().optional(),
|
|
50
|
+
CHAPTERHOUSE_SSE_REPLAY_LIMIT: z.string().optional(),
|
|
49
51
|
CHAPTERHOUSE_WORKIQ_AUTO_INSTALL: z.string().optional(),
|
|
50
52
|
CHAPTERHOUSE_CHAT_SSE: z.string().optional(),
|
|
53
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED: z.string().optional(),
|
|
54
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS: z.string().optional(),
|
|
55
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE: z.string().optional(),
|
|
56
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_MIN_TURNS_FOR_SCOPE_FIRE: z.string().optional(),
|
|
57
|
+
CHAPTERHOUSE_MEMORY_INJECT: z.string().optional(),
|
|
58
|
+
CHAPTERHOUSE_MEMORY_AUTO_ACCEPT: z.string().optional(),
|
|
59
|
+
CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED: z.string().optional(),
|
|
60
|
+
CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED: z.string().optional(),
|
|
61
|
+
CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS: z.string().optional(),
|
|
62
|
+
CHAPTERHOUSE_MEMORY_DECAY_DAYS: z.string().optional(),
|
|
63
|
+
CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS: z.string().optional(),
|
|
64
|
+
CHAPTERHOUSE_MEMORY_TIERING_ENABLED: z.string().optional(),
|
|
65
|
+
CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST: z.string().optional(),
|
|
66
|
+
CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS: z.string().optional(),
|
|
51
67
|
});
|
|
52
68
|
export const DEFAULT_MODEL = "claude-sonnet-4.6";
|
|
53
69
|
export const DEFAULT_TEAM_WIKI_CACHE_TTL_MINUTES = 60;
|
|
@@ -60,6 +76,8 @@ export const DEFAULT_API_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
|
60
76
|
export const DEFAULT_API_RATE_LIMIT_GENERAL_MAX = 100;
|
|
61
77
|
export const DEFAULT_API_RATE_LIMIT_AUTH_MAX = 10;
|
|
62
78
|
export const DEFAULT_API_RATE_LIMIT_SSE_MAX_CONNECTIONS = 5;
|
|
79
|
+
export const DEFAULT_SSE_BUFFER_CAPACITY = 2_000;
|
|
80
|
+
export const DEFAULT_SSE_REPLAY_LIMIT = 10_000;
|
|
63
81
|
function parseZeroOneEnv(name, rawValue, defaultValue) {
|
|
64
82
|
const normalized = rawValue?.trim();
|
|
65
83
|
if (!normalized) {
|
|
@@ -110,6 +128,17 @@ function parsePositiveIntegerEnv(name, rawValue, defaultValue) {
|
|
|
110
128
|
}
|
|
111
129
|
return parsed;
|
|
112
130
|
}
|
|
131
|
+
function parsePositiveNumberEnv(name, rawValue, defaultValue) {
|
|
132
|
+
const normalized = rawValue?.trim();
|
|
133
|
+
if (!normalized) {
|
|
134
|
+
return defaultValue;
|
|
135
|
+
}
|
|
136
|
+
const parsed = Number(normalized);
|
|
137
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
138
|
+
throw new Error(`${name} must be a positive number, got: "${rawValue}"`);
|
|
139
|
+
}
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
113
142
|
function parseCorsAllowedOrigins(rawValue) {
|
|
114
143
|
const normalized = rawValue?.trim();
|
|
115
144
|
if (!normalized) {
|
|
@@ -200,6 +229,15 @@ export function parseRuntimeConfig(env, options = {}) {
|
|
|
200
229
|
const apiRateLimitGeneralMax = parsePositiveIntegerEnv("API_RATE_LIMIT_GENERAL_MAX", raw.API_RATE_LIMIT_GENERAL_MAX, DEFAULT_API_RATE_LIMIT_GENERAL_MAX);
|
|
201
230
|
const apiRateLimitAuthMax = parsePositiveIntegerEnv("API_RATE_LIMIT_AUTH_MAX", raw.API_RATE_LIMIT_AUTH_MAX, DEFAULT_API_RATE_LIMIT_AUTH_MAX);
|
|
202
231
|
const apiRateLimitSseMaxConnections = parsePositiveIntegerEnv("API_RATE_LIMIT_SSE_MAX_CONNECTIONS", raw.API_RATE_LIMIT_SSE_MAX_CONNECTIONS, DEFAULT_API_RATE_LIMIT_SSE_MAX_CONNECTIONS);
|
|
232
|
+
const sseBufferCapacity = parsePositiveIntegerEnv("CHAPTERHOUSE_SSE_BUFFER_CAPACITY", raw.CHAPTERHOUSE_SSE_BUFFER_CAPACITY, DEFAULT_SSE_BUFFER_CAPACITY);
|
|
233
|
+
const sseReplayLimit = parsePositiveIntegerEnv("CHAPTERHOUSE_SSE_REPLAY_LIMIT", raw.CHAPTERHOUSE_SSE_REPLAY_LIMIT, DEFAULT_SSE_REPLAY_LIMIT);
|
|
234
|
+
const memoryCheckpointTurns = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS, 5);
|
|
235
|
+
const memoryCheckpointMinTurnsForScopeFire = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_MIN_TURNS_FOR_SCOPE_FIRE", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_MIN_TURNS_FOR_SCOPE_FIRE, 2);
|
|
236
|
+
const memoryHousekeepingTurns = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS", raw.CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS, 50);
|
|
237
|
+
const memoryDecayDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_DECAY_DAYS", raw.CHAPTERHOUSE_MEMORY_DECAY_DAYS, 30);
|
|
238
|
+
const memoryInboxRetentionDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS", raw.CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS, 7);
|
|
239
|
+
const memoryHotRecallBoost = parsePositiveNumberEnv("CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST", raw.CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST, 1.5);
|
|
240
|
+
const memoryHotAgeDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS", raw.CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS, 30);
|
|
203
241
|
if (entraAuthEnabled && (!entraTenantId || !entraClientId)) {
|
|
204
242
|
throw new Error("ENTRA_AUTH_ENABLED=true requires ENTRA_TENANT_ID and ENTRA_CLIENT_ID");
|
|
205
243
|
}
|
|
@@ -239,8 +277,24 @@ export function parseRuntimeConfig(env, options = {}) {
|
|
|
239
277
|
apiRateLimitGeneralMax,
|
|
240
278
|
apiRateLimitAuthMax,
|
|
241
279
|
apiRateLimitSseMaxConnections,
|
|
280
|
+
sseBufferCapacity,
|
|
281
|
+
sseReplayLimit,
|
|
242
282
|
workiqAutoInstall: parseBooleanEnv("CHAPTERHOUSE_WORKIQ_AUTO_INSTALL", raw.CHAPTERHOUSE_WORKIQ_AUTO_INSTALL, true),
|
|
243
283
|
chatSseEnabled: parseZeroOneEnv("CHAPTERHOUSE_CHAT_SSE", raw.CHAPTERHOUSE_CHAT_SSE, true),
|
|
284
|
+
memoryCheckpointEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED, true),
|
|
285
|
+
memoryCheckpointTurns,
|
|
286
|
+
memoryCheckpointOnScopeChange: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE, true),
|
|
287
|
+
memoryCheckpointMinTurnsForScopeFire,
|
|
288
|
+
memoryInjectEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_INJECT", raw.CHAPTERHOUSE_MEMORY_INJECT, true),
|
|
289
|
+
memoryAutoAcceptEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_AUTO_ACCEPT", raw.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT, true),
|
|
290
|
+
memoryEndOfTaskHookEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED", raw.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED, true),
|
|
291
|
+
memoryHousekeepingEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED", raw.CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED, true),
|
|
292
|
+
memoryHousekeepingTurns,
|
|
293
|
+
memoryDecayDays,
|
|
294
|
+
memoryInboxRetentionDays,
|
|
295
|
+
memoryTieringEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_TIERING_ENABLED", raw.CHAPTERHOUSE_MEMORY_TIERING_ENABLED, true),
|
|
296
|
+
memoryHotRecallBoost,
|
|
297
|
+
memoryHotAgeDays,
|
|
244
298
|
};
|
|
245
299
|
}
|
|
246
300
|
const runtimeConfig = parseRuntimeConfig(process.env);
|
|
@@ -280,8 +334,24 @@ export const config = {
|
|
|
280
334
|
apiRateLimitGeneralMax: runtimeConfig.apiRateLimitGeneralMax,
|
|
281
335
|
apiRateLimitAuthMax: runtimeConfig.apiRateLimitAuthMax,
|
|
282
336
|
apiRateLimitSseMaxConnections: runtimeConfig.apiRateLimitSseMaxConnections,
|
|
337
|
+
sseBufferCapacity: runtimeConfig.sseBufferCapacity,
|
|
338
|
+
sseReplayLimit: runtimeConfig.sseReplayLimit,
|
|
283
339
|
workiqAutoInstall: runtimeConfig.workiqAutoInstall,
|
|
284
340
|
chatSseEnabled: runtimeConfig.chatSseEnabled,
|
|
341
|
+
memoryCheckpointEnabled: runtimeConfig.memoryCheckpointEnabled,
|
|
342
|
+
memoryCheckpointTurns: runtimeConfig.memoryCheckpointTurns,
|
|
343
|
+
memoryCheckpointOnScopeChange: runtimeConfig.memoryCheckpointOnScopeChange,
|
|
344
|
+
memoryCheckpointMinTurnsForScopeFire: runtimeConfig.memoryCheckpointMinTurnsForScopeFire,
|
|
345
|
+
memoryInjectEnabled: runtimeConfig.memoryInjectEnabled,
|
|
346
|
+
memoryAutoAcceptEnabled: runtimeConfig.memoryAutoAcceptEnabled,
|
|
347
|
+
memoryEndOfTaskHookEnabled: runtimeConfig.memoryEndOfTaskHookEnabled,
|
|
348
|
+
memoryHousekeepingEnabled: runtimeConfig.memoryHousekeepingEnabled,
|
|
349
|
+
memoryHousekeepingTurns: runtimeConfig.memoryHousekeepingTurns,
|
|
350
|
+
memoryDecayDays: runtimeConfig.memoryDecayDays,
|
|
351
|
+
memoryInboxRetentionDays: runtimeConfig.memoryInboxRetentionDays,
|
|
352
|
+
memoryTieringEnabled: runtimeConfig.memoryTieringEnabled,
|
|
353
|
+
memoryHotRecallBoost: runtimeConfig.memoryHotRecallBoost,
|
|
354
|
+
memoryHotAgeDays: runtimeConfig.memoryHotAgeDays,
|
|
285
355
|
copilotAuthToken: runtimeConfig.copilotAuthToken,
|
|
286
356
|
get copilotModel() {
|
|
287
357
|
return _copilotModel;
|
package/dist/config.test.js
CHANGED
|
@@ -109,6 +109,95 @@ test("defaults chat SSE on and still honors explicit CHAPTERHOUSE_CHAT_SSE overr
|
|
|
109
109
|
assert.equal(parsedDisabled.chatSseEnabled, false);
|
|
110
110
|
assert.equal(parsedEnabled.chatSseEnabled, true);
|
|
111
111
|
});
|
|
112
|
+
test("defaults memory checkpoint turns to 5 and parses integer overrides", async () => {
|
|
113
|
+
const configModule = await import("./config.js");
|
|
114
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
115
|
+
const parsedDefault = configModule.parseRuntimeConfig({});
|
|
116
|
+
const parsedThree = configModule.parseRuntimeConfig({
|
|
117
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS: "3",
|
|
118
|
+
});
|
|
119
|
+
const parsedTen = configModule.parseRuntimeConfig({
|
|
120
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS: "10",
|
|
121
|
+
});
|
|
122
|
+
assert.equal(parsedDefault.memoryCheckpointTurns, 5);
|
|
123
|
+
assert.equal(parsedThree.memoryCheckpointTurns, 3);
|
|
124
|
+
assert.equal(parsedTen.memoryCheckpointTurns, 10);
|
|
125
|
+
});
|
|
126
|
+
test("defaults memory injection on and still honors explicit CHAPTERHOUSE_MEMORY_INJECT overrides", async () => {
|
|
127
|
+
const configModule = await import("./config.js");
|
|
128
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
129
|
+
const parsedDefault = configModule.parseRuntimeConfig({});
|
|
130
|
+
const parsedDisabled = configModule.parseRuntimeConfig({
|
|
131
|
+
CHAPTERHOUSE_MEMORY_INJECT: "0",
|
|
132
|
+
});
|
|
133
|
+
const parsedEnabled = configModule.parseRuntimeConfig({
|
|
134
|
+
CHAPTERHOUSE_MEMORY_INJECT: "1",
|
|
135
|
+
});
|
|
136
|
+
assert.equal(parsedDefault.memoryInjectEnabled, true);
|
|
137
|
+
assert.equal(parsedDisabled.memoryInjectEnabled, false);
|
|
138
|
+
assert.equal(parsedEnabled.memoryInjectEnabled, true);
|
|
139
|
+
});
|
|
140
|
+
test("defaults memory checkpoint extraction on and still honors explicit CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED overrides", async () => {
|
|
141
|
+
const configModule = await import("./config.js");
|
|
142
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
143
|
+
const parsedDefault = configModule.parseRuntimeConfig({});
|
|
144
|
+
const parsedDisabled = configModule.parseRuntimeConfig({
|
|
145
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED: "0",
|
|
146
|
+
});
|
|
147
|
+
const parsedEnabled = configModule.parseRuntimeConfig({
|
|
148
|
+
CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED: "1",
|
|
149
|
+
});
|
|
150
|
+
assert.equal(parsedDefault.memoryCheckpointEnabled, true);
|
|
151
|
+
assert.equal(parsedDisabled.memoryCheckpointEnabled, false);
|
|
152
|
+
assert.equal(parsedEnabled.memoryCheckpointEnabled, true);
|
|
153
|
+
});
|
|
154
|
+
test("defaults end-of-task memory processing on and still honors explicit CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED overrides", async () => {
|
|
155
|
+
const configModule = await import("./config.js");
|
|
156
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
157
|
+
const parsedDefault = configModule.parseRuntimeConfig({});
|
|
158
|
+
const parsedDisabled = configModule.parseRuntimeConfig({
|
|
159
|
+
CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED: "0",
|
|
160
|
+
});
|
|
161
|
+
const parsedEnabled = configModule.parseRuntimeConfig({
|
|
162
|
+
CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED: "1",
|
|
163
|
+
});
|
|
164
|
+
assert.equal(parsedDefault.memoryEndOfTaskHookEnabled, true);
|
|
165
|
+
assert.equal(parsedDisabled.memoryEndOfTaskHookEnabled, false);
|
|
166
|
+
assert.equal(parsedEnabled.memoryEndOfTaskHookEnabled, true);
|
|
167
|
+
});
|
|
168
|
+
test("parses memory housekeeping config defaults and overrides", async () => {
|
|
169
|
+
const configModule = await import("./config.js");
|
|
170
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
171
|
+
const parsedDefault = configModule.parseRuntimeConfig({});
|
|
172
|
+
const parsedOverride = configModule.parseRuntimeConfig({
|
|
173
|
+
CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED: "0",
|
|
174
|
+
CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS: "12",
|
|
175
|
+
CHAPTERHOUSE_MEMORY_DECAY_DAYS: "45",
|
|
176
|
+
CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS: "14",
|
|
177
|
+
});
|
|
178
|
+
assert.equal(parsedDefault.memoryHousekeepingEnabled, true);
|
|
179
|
+
assert.equal(parsedDefault.memoryHousekeepingTurns, 50);
|
|
180
|
+
assert.equal(parsedDefault.memoryDecayDays, 30);
|
|
181
|
+
assert.equal(parsedDefault.memoryInboxRetentionDays, 7);
|
|
182
|
+
assert.equal(parsedOverride.memoryHousekeepingEnabled, false);
|
|
183
|
+
assert.equal(parsedOverride.memoryHousekeepingTurns, 12);
|
|
184
|
+
assert.equal(parsedOverride.memoryDecayDays, 45);
|
|
185
|
+
assert.equal(parsedOverride.memoryInboxRetentionDays, 14);
|
|
186
|
+
});
|
|
187
|
+
test("defaults automatic proposal acceptance on and still honors explicit CHAPTERHOUSE_MEMORY_AUTO_ACCEPT overrides", async () => {
|
|
188
|
+
const configModule = await import("./config.js");
|
|
189
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
190
|
+
const parsedDefault = configModule.parseRuntimeConfig({});
|
|
191
|
+
const parsedDisabled = configModule.parseRuntimeConfig({
|
|
192
|
+
CHAPTERHOUSE_MEMORY_AUTO_ACCEPT: "0",
|
|
193
|
+
});
|
|
194
|
+
const parsedEnabled = configModule.parseRuntimeConfig({
|
|
195
|
+
CHAPTERHOUSE_MEMORY_AUTO_ACCEPT: "1",
|
|
196
|
+
});
|
|
197
|
+
assert.equal(parsedDefault.memoryAutoAcceptEnabled, true);
|
|
198
|
+
assert.equal(parsedDisabled.memoryAutoAcceptEnabled, false);
|
|
199
|
+
assert.equal(parsedEnabled.memoryAutoAcceptEnabled, true);
|
|
200
|
+
});
|
|
112
201
|
test("prefers COPILOT_TOKEN over GITHUB_TOKEN for Copilot SDK auth", async () => {
|
|
113
202
|
const configModule = await import("./config.js");
|
|
114
203
|
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
@@ -159,4 +248,24 @@ test("rejects invalid rate limiting settings", async () => {
|
|
|
159
248
|
API_RATE_LIMIT_GENERAL_MAX: "0",
|
|
160
249
|
}), /API_RATE_LIMIT_GENERAL_MAX must be a positive integer/);
|
|
161
250
|
});
|
|
251
|
+
test("parses SSE replay settings and defaults", async () => {
|
|
252
|
+
const configModule = await import("./config.js");
|
|
253
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
254
|
+
const parsedDefault = configModule.parseRuntimeConfig({});
|
|
255
|
+
const parsedExplicit = configModule.parseRuntimeConfig({
|
|
256
|
+
CHAPTERHOUSE_SSE_BUFFER_CAPACITY: "2500",
|
|
257
|
+
CHAPTERHOUSE_SSE_REPLAY_LIMIT: "20000",
|
|
258
|
+
});
|
|
259
|
+
assert.equal(parsedDefault.sseBufferCapacity, 2000);
|
|
260
|
+
assert.equal(parsedDefault.sseReplayLimit, 10000);
|
|
261
|
+
assert.equal(parsedExplicit.sseBufferCapacity, 2500);
|
|
262
|
+
assert.equal(parsedExplicit.sseReplayLimit, 20000);
|
|
263
|
+
});
|
|
264
|
+
test("rejects invalid SSE replay settings", async () => {
|
|
265
|
+
const configModule = await import("./config.js");
|
|
266
|
+
assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
|
|
267
|
+
assert.throws(() => configModule.parseRuntimeConfig({
|
|
268
|
+
CHAPTERHOUSE_SSE_BUFFER_CAPACITY: "0",
|
|
269
|
+
}), /CHAPTERHOUSE_SSE_BUFFER_CAPACITY must be a positive integer/);
|
|
270
|
+
});
|
|
162
271
|
//# sourceMappingURL=config.test.js.map
|
package/dist/copilot/agents.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { readdirSync, readFileSync, mkdirSync, writeFileSync, existsSync, rmSync, copyFileSync } from "fs";
|
|
2
3
|
import { createHash } from "crypto";
|
|
3
4
|
import { join, dirname, sep } from "path";
|
|
@@ -10,6 +11,8 @@ import { loadMcpConfig } from "./mcp-config.js";
|
|
|
10
11
|
import { getSkillDirectories } from "./skills.js";
|
|
11
12
|
import { childLogger } from "../util/logger.js";
|
|
12
13
|
const log = childLogger("agents");
|
|
14
|
+
const toolAgentContext = new AsyncLocalStorage();
|
|
15
|
+
const toolTaskContext = new AsyncLocalStorage();
|
|
13
16
|
// Frontmatter schema
|
|
14
17
|
const agentFrontmatterSchema = z.object({
|
|
15
18
|
name: z.string().min(1),
|
|
@@ -213,12 +216,18 @@ let taskCounter = 0;
|
|
|
213
216
|
function nextTaskId() {
|
|
214
217
|
return `task-${++taskCounter}-${Date.now().toString(36)}`;
|
|
215
218
|
}
|
|
219
|
+
export function createTaskId() {
|
|
220
|
+
return nextTaskId();
|
|
221
|
+
}
|
|
216
222
|
/** Shared base prompt injected into all agent sessions. */
|
|
217
223
|
function getAgentBasePrompt() {
|
|
218
224
|
return `## Runtime Context
|
|
219
225
|
|
|
220
226
|
You are an agent within Chapterhouse, a team-level AI assistant for engineering teams. You run on the user's local machine.
|
|
221
227
|
|
|
228
|
+
### Agent Memory
|
|
229
|
+
Chapterhouse agent memory follows a three-tier memory model: **read** with \`memory_recall\`, **propose** with \`memory_propose\`, and **write** with orchestrator-only tools. Do not call \`memory_remember\` directly; when you discover something memory-worthy, use \`memory_propose\` instead. Proposals are processed automatically at end-of-task, so do not wait for confirmation. Examples: a durable fact is an \`observation\`, a settled implementation choice is a \`decision\`, and a named system/tool/person can be proposed as an \`entity\`.
|
|
230
|
+
|
|
222
231
|
### Shared Wiki
|
|
223
232
|
All agents share a wiki knowledge base for persistent memory. Use \`wiki_read\` and \`wiki_search\` to find existing knowledge, and \`wiki_update\` to save important findings.
|
|
224
233
|
|
|
@@ -263,6 +272,7 @@ export function buildAgentRoster() {
|
|
|
263
272
|
const WIKI_TOOL_NAMES = new Set([
|
|
264
273
|
"wiki_search", "wiki_read", "wiki_update", "remember", "recall", "forget",
|
|
265
274
|
"wiki_ingest", "wiki_lint", "wiki_rebuild_index",
|
|
275
|
+
"memory_recall", "memory_propose",
|
|
266
276
|
]);
|
|
267
277
|
// Management tools that only @chapterhouse should have
|
|
268
278
|
const MANAGEMENT_TOOL_NAMES = new Set([
|
|
@@ -271,7 +281,20 @@ const MANAGEMENT_TOOL_NAMES = new Set([
|
|
|
271
281
|
"switch_model", "toggle_auto", "list_models",
|
|
272
282
|
"restart_chapterhouse", "list_skills", "learn_skill", "uninstall_skill",
|
|
273
283
|
"list_machine_sessions", "attach_machine_session",
|
|
284
|
+
"memory_remember", "memory_set_scope", "memory_housekeep", "memory_promote", "memory_demote",
|
|
274
285
|
]);
|
|
286
|
+
export function getCurrentToolAgentSlug() {
|
|
287
|
+
return toolAgentContext.getStore();
|
|
288
|
+
}
|
|
289
|
+
export function getCurrentToolTaskId() {
|
|
290
|
+
return toolTaskContext.getStore();
|
|
291
|
+
}
|
|
292
|
+
export function bindToolsToAgent(agentSlug, allTools, taskId) {
|
|
293
|
+
return allTools.map((tool) => ({
|
|
294
|
+
...tool,
|
|
295
|
+
handler: (args, invocation) => toolAgentContext.run(agentSlug, () => toolTaskContext.run(taskId, () => tool.handler(args, invocation))),
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
275
298
|
/** Filter tools based on agent config. */
|
|
276
299
|
export function filterToolsForAgent(agent, allTools) {
|
|
277
300
|
if (agent.tools && agent.tools.length > 0) {
|
|
@@ -286,7 +309,7 @@ export function filterToolsForAgent(agent, allTools) {
|
|
|
286
309
|
return allTools.filter((t) => !MANAGEMENT_TOOL_NAMES.has(t.name));
|
|
287
310
|
}
|
|
288
311
|
/** Create an ephemeral session for an agent. Always creates a fresh session — caller is responsible for destroying it. */
|
|
289
|
-
export async function createEphemeralAgentSession(slug, client, allTools, modelOverride, systemMessagePrefix) {
|
|
312
|
+
export async function createEphemeralAgentSession(slug, client, allTools, modelOverride, systemMessagePrefix, taskId) {
|
|
290
313
|
const agent = getAgent(slug);
|
|
291
314
|
if (!agent)
|
|
292
315
|
throw new Error(`Agent '${slug}' not found in registry.`);
|
|
@@ -295,7 +318,7 @@ export async function createEphemeralAgentSession(slug, client, allTools, modelO
|
|
|
295
318
|
const model = (modelOverride && modelOverride.length > 0)
|
|
296
319
|
? modelOverride
|
|
297
320
|
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model);
|
|
298
|
-
const tools = filterToolsForAgent(agent, allTools);
|
|
321
|
+
const tools = bindToolsToAgent(agent.slug, filterToolsForAgent(agent, allTools), taskId);
|
|
299
322
|
const mcpServers = loadMcpConfig();
|
|
300
323
|
const skillDirectories = getSkillDirectories();
|
|
301
324
|
const baseSystemMessage = composeAgentSystemMessage(agent);
|
|
@@ -342,9 +365,9 @@ export function getTask(taskId) {
|
|
|
342
365
|
return activeTasks.get(taskId);
|
|
343
366
|
}
|
|
344
367
|
/** Register a new task. */
|
|
345
|
-
export function registerTask(agentSlug, description, originChannel) {
|
|
368
|
+
export function registerTask(agentSlug, description, originChannel, taskId = nextTaskId()) {
|
|
346
369
|
const task = {
|
|
347
|
-
taskId
|
|
370
|
+
taskId,
|
|
348
371
|
agentSlug,
|
|
349
372
|
description,
|
|
350
373
|
status: "running",
|
|
@@ -19,4 +19,11 @@ test("composeAgentSystemMessage steers wiki-capable agents to wiki-conventions",
|
|
|
19
19
|
assert.match(message, /scan the last 20-30 entries of `pages\/_meta\/log\.md`/i);
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
|
+
test("composeAgentSystemMessage teaches subagents the three-tier memory model and directs them to memory_propose", () => {
|
|
23
|
+
const message = composeAgentSystemMessage(makeAgent("coder"));
|
|
24
|
+
assert.match(message, /three-tier memory model|read, propose, write/i);
|
|
25
|
+
assert.match(message, /memory_recall/i);
|
|
26
|
+
assert.match(message, /memory_propose/i);
|
|
27
|
+
assert.match(message, /do not call `memory_remember` directly|should not call `memory_remember` directly/i);
|
|
28
|
+
});
|
|
22
29
|
//# sourceMappingURL=agents.test.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { approveAll } from "@github/copilot-sdk";
|
|
2
|
+
import { config } from "../config.js";
|
|
3
|
+
import { SESSIONS_DIR } from "../paths.js";
|
|
4
|
+
import { childLogger } from "../util/logger.js";
|
|
5
|
+
const log = childLogger("copilot.oneshot");
|
|
6
|
+
const DEFAULT_ONE_SHOT_TIMEOUT_MS = 60_000;
|
|
7
|
+
export async function runOneShotPrompt(input) {
|
|
8
|
+
const model = input.model ?? config.copilotModel;
|
|
9
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_ONE_SHOT_TIMEOUT_MS;
|
|
10
|
+
const maxAttempts = input.expectJson ? 2 : 1;
|
|
11
|
+
let prompt = input.user;
|
|
12
|
+
let lastContent = "";
|
|
13
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
14
|
+
const session = await input.client.createSession({
|
|
15
|
+
model,
|
|
16
|
+
configDir: SESSIONS_DIR,
|
|
17
|
+
streaming: false,
|
|
18
|
+
systemMessage: { content: input.system },
|
|
19
|
+
onPermissionRequest: approveAll,
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
const response = await session.sendAndWait({ prompt }, timeoutMs);
|
|
23
|
+
lastContent = response?.data?.content?.trim() ?? "";
|
|
24
|
+
if (!input.expectJson) {
|
|
25
|
+
return { content: lastContent, model, attempts: attempt };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
JSON.parse(lastContent);
|
|
29
|
+
return { content: lastContent, model, attempts: attempt };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (attempt >= maxAttempts) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
prompt = [
|
|
36
|
+
input.user,
|
|
37
|
+
"",
|
|
38
|
+
"Your previous reply was not valid JSON.",
|
|
39
|
+
"Return only valid JSON matching the requested schema. Do not wrap it in markdown fences.",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
try {
|
|
45
|
+
await session.disconnect();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
log.warn({ err: error instanceof Error ? error.message : error, model }, "one-shot disconnect failed");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { content: lastContent, model, attempts: maxAttempts };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=oneshot.js.map
|