chapterhouse 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/bellonda.agent.md +11 -0
- package/agents/hwi-noree.agent.md +12 -0
- package/dist/api/server.js +39 -2
- package/dist/api/server.test.js +20 -0
- package/dist/api/turn-sse.integration.test.js +12 -0
- package/dist/copilot/agents.js +16 -4
- package/dist/copilot/agents.test.js +43 -1
- package/dist/copilot/orchestrator.js +173 -32
- package/dist/copilot/orchestrator.test.js +236 -20
- package/dist/copilot/session-manager.js +11 -2
- package/dist/copilot/session-manager.test.js +25 -0
- package/dist/copilot/tools.agent.test.js +52 -4
- package/dist/copilot/tools.js +265 -18
- package/dist/copilot/tools.memory.test.js +175 -2
- package/dist/daemon.js +6 -0
- package/dist/memory/action-items.js +100 -0
- package/dist/memory/action-items.test.js +83 -0
- package/dist/memory/active-scope.js +9 -0
- package/dist/memory/eot.js +28 -3
- package/dist/memory/eot.test.js +108 -0
- package/dist/memory/hot-tier.js +60 -1
- package/dist/memory/hot-tier.test.js +38 -0
- package/dist/memory/housekeeping-scheduler.js +152 -0
- package/dist/memory/housekeeping-scheduler.test.js +187 -0
- package/dist/memory/index.js +2 -1
- package/dist/memory/recall.js +59 -0
- package/dist/memory/recall.test.js +27 -0
- package/dist/memory/tiering.js +33 -3
- package/dist/store/db.js +130 -17
- package/dist/store/db.test.js +61 -5
- package/package.json +1 -1
- package/web/dist/assets/{index-B_cCSHan.js → index-BfHqP3-C.js} +87 -87
- package/web/dist/assets/{index-B_cCSHan.js.map → index-BfHqP3-C.js.map} +1 -1
- package/web/dist/assets/index-_O6AoWOS.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DhY5yWmC.css +0 -10
package/dist/copilot/tools.js
CHANGED
|
@@ -7,7 +7,7 @@ import { homedir } from "os";
|
|
|
7
7
|
import { listSkills, createSkill, removeSkill } from "./skills.js";
|
|
8
8
|
import { config, persistModel } from "../config.js";
|
|
9
9
|
import { agentEventBus } from "./agent-event-bus.js";
|
|
10
|
-
import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentActiveProjectRules, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, invalidateOrchestratorSession, maybeScheduleScopeChangeCheckpoint, resetCheckpointSessionState, switchSessionModel, } from "./orchestrator.js";
|
|
10
|
+
import { getCurrentSourceChannel, getCurrentActivityCallback, getCurrentActiveProjectRules, getCurrentAuthenticatedUser, getLastAuthenticatedUser, getCurrentAuthorizationHeader, getCurrentSessionKey, sendToAgentSession, invalidateOrchestratorSession, maybeScheduleScopeChangeCheckpoint, resetCheckpointSessionState, switchSessionModel, } from "./orchestrator.js";
|
|
11
11
|
import { getRouterConfig, updateRouterConfig } from "./router.js";
|
|
12
12
|
import { ensureWikiStructure, readPage, writePage, deletePage, writeRawSource, assertPagePath } from "../wiki/fs.js";
|
|
13
13
|
import { searchIndex, addToIndex, removeFromIndex, buildIndexEntryForPage } from "../wiki/index-manager.js";
|
|
@@ -29,7 +29,7 @@ import { TeamsNotifier } from "../integrations/teams-notify.js";
|
|
|
29
29
|
import { TeamPushClient } from "../integrations/team-push.js";
|
|
30
30
|
import { OKRMapper, parseOKRPageContent } from "./okr-mapper.js";
|
|
31
31
|
import { childLogger } from "../util/logger.js";
|
|
32
|
-
import { getActiveScope as getMemoryActiveScope, getScope as getMemoryScope, inferScopeFromText, demoteToCold, demoteToWarm, queueMemoryProposal, recall as recallMemory, recordDecision, recordObservation, runHousekeeping, setActiveScope as setMemoryActiveScope, promoteToHot, upsertEntity, } from "../memory/index.js";
|
|
32
|
+
import { getActiveScope as getMemoryActiveScope, getScope as getMemoryScope, inferScopeFromText, completeActionItem, demoteToCold, demoteToWarm, dropActionItem, queueMemoryProposal, recall as recallMemory, listActionItems, recordDecision, recordActionItem, recordObservation, runHousekeeping, setActiveScope as setMemoryActiveScope, snoozeActionItem, promoteToHot, upsertEntity, } from "../memory/index.js";
|
|
33
33
|
const log = childLogger("tools");
|
|
34
34
|
/** Escape a string for safe inclusion as a single-line YAML scalar value. */
|
|
35
35
|
function yamlEscape(value) {
|
|
@@ -65,6 +65,21 @@ function requireOrchestratorMemoryWrite() {
|
|
|
65
65
|
}
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
|
+
function resolveProposalScopeSlug(requestedScopeSlug) {
|
|
69
|
+
const agentSlug = agentsModule.getCurrentToolAgentSlug?.();
|
|
70
|
+
if (!agentSlug || agentSlug === "chapterhouse") {
|
|
71
|
+
return requestedScopeSlug;
|
|
72
|
+
}
|
|
73
|
+
const activeScope = getMemoryActiveScope();
|
|
74
|
+
if (activeScope) {
|
|
75
|
+
return activeScope.slug;
|
|
76
|
+
}
|
|
77
|
+
const agent = getAgent(agentSlug);
|
|
78
|
+
if (agent?.persistent && agent.scope) {
|
|
79
|
+
return agent.scope;
|
|
80
|
+
}
|
|
81
|
+
return requestedScopeSlug;
|
|
82
|
+
}
|
|
68
83
|
function resolveMemoryScopeForWrite(explicitScope, content) {
|
|
69
84
|
const explicit = explicitScope ? getMemoryScope(explicitScope) : undefined;
|
|
70
85
|
if (explicitScope && !explicit) {
|
|
@@ -106,11 +121,31 @@ const decisionProposalPayloadSchema = z.object({
|
|
|
106
121
|
});
|
|
107
122
|
const entityProposalPayloadSchema = z.object({
|
|
108
123
|
name: z.string(),
|
|
109
|
-
|
|
124
|
+
entity_kind: z.string().optional(),
|
|
125
|
+
kind: z.string().optional(),
|
|
110
126
|
summary: z.string().optional(),
|
|
127
|
+
}).superRefine((value, context) => {
|
|
128
|
+
if (!value.entity_kind && !value.kind) {
|
|
129
|
+
context.addIssue({
|
|
130
|
+
code: z.ZodIssueCode.custom,
|
|
131
|
+
message: "entity_kind is required for entity proposals.",
|
|
132
|
+
path: ["entity_kind"],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}).transform((value) => ({
|
|
136
|
+
name: value.name,
|
|
137
|
+
entity_kind: value.entity_kind ?? value.kind,
|
|
138
|
+
summary: value.summary,
|
|
139
|
+
}));
|
|
140
|
+
const actionItemProposalPayloadSchema = z.object({
|
|
141
|
+
title: z.string(),
|
|
142
|
+
detail: z.string().optional(),
|
|
143
|
+
due_at: z.string().optional(),
|
|
144
|
+
source: z.string().optional(),
|
|
145
|
+
entity_id: z.number().int().positive().optional(),
|
|
111
146
|
});
|
|
112
147
|
const memoryProposeArgsSchema = z.object({
|
|
113
|
-
kind: z.enum(["observation", "decision", "entity"]),
|
|
148
|
+
kind: z.enum(["observation", "decision", "entity", "action_item"]),
|
|
114
149
|
scope_slug: z.string().optional(),
|
|
115
150
|
payload: z.record(z.string(), z.unknown()),
|
|
116
151
|
confidence: z.number().min(0).max(1).optional(),
|
|
@@ -120,7 +155,9 @@ const memoryProposeArgsSchema = z.object({
|
|
|
120
155
|
? observationProposalPayloadSchema
|
|
121
156
|
: value.kind === "decision"
|
|
122
157
|
? decisionProposalPayloadSchema
|
|
123
|
-
:
|
|
158
|
+
: value.kind === "entity"
|
|
159
|
+
? entityProposalPayloadSchema
|
|
160
|
+
: actionItemProposalPayloadSchema;
|
|
124
161
|
const parsed = schema.safeParse(value.payload);
|
|
125
162
|
if (!parsed.success) {
|
|
126
163
|
for (const issue of parsed.error.issues) {
|
|
@@ -132,7 +169,7 @@ const memoryProposeArgsSchema = z.object({
|
|
|
132
169
|
}
|
|
133
170
|
}
|
|
134
171
|
});
|
|
135
|
-
const memoryTierTableSchema = z.enum(["observation", "decision", "entity"]);
|
|
172
|
+
const memoryTierTableSchema = z.enum(["observation", "decision", "entity", "action_item"]);
|
|
136
173
|
function getCurrentQuarter(now = new Date()) {
|
|
137
174
|
return `${now.getUTCFullYear()}-Q${Math.floor(now.getUTCMonth() / 3) + 1}`;
|
|
138
175
|
}
|
|
@@ -188,15 +225,6 @@ export function createTools(deps) {
|
|
|
188
225
|
}
|
|
189
226
|
const delegatedSlug = agent.slug;
|
|
190
227
|
const taskId = createTaskId();
|
|
191
|
-
let session;
|
|
192
|
-
try {
|
|
193
|
-
const allTools = createTools(deps);
|
|
194
|
-
session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override, undefined, taskId);
|
|
195
|
-
}
|
|
196
|
-
catch (err) {
|
|
197
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
198
|
-
return `Failed to create session for @${delegatedSlug}: ${msg}`;
|
|
199
|
-
}
|
|
200
228
|
const task = registerTask(delegatedSlug, args.summary, getCurrentSourceChannel(), taskId);
|
|
201
229
|
const activeProjectRules = getCurrentActiveProjectRules();
|
|
202
230
|
const warningLines = activeProjectRules
|
|
@@ -210,6 +238,71 @@ export function createTools(deps) {
|
|
|
210
238
|
const db = getDb();
|
|
211
239
|
db.prepare(`INSERT INTO agent_tasks (task_id, agent_slug, description, prompt, status, origin_channel, session_key, source)
|
|
212
240
|
VALUES (?, ?, ?, ?, 'running', ?, ?, 'adhoc')`).run(task.taskId, delegatedSlug, args.summary, args.task, task.originChannel || null, getCurrentSessionKey());
|
|
241
|
+
if (agent.persistent) {
|
|
242
|
+
(async () => {
|
|
243
|
+
try {
|
|
244
|
+
const output = await sendToAgentSession(delegatedSlug, taskPrompt, task.taskId);
|
|
245
|
+
completeTask(task.taskId, output);
|
|
246
|
+
updateTaskResult(task.taskId, "completed", output);
|
|
247
|
+
const statusEvent = appendTaskStatusEvent(task.taskId, "completed");
|
|
248
|
+
if (statusEvent) {
|
|
249
|
+
void agentEventBus.emit({
|
|
250
|
+
type: "session:tool_call",
|
|
251
|
+
sessionId: task.taskId,
|
|
252
|
+
payload: {
|
|
253
|
+
toolName: "",
|
|
254
|
+
toolArgs: {},
|
|
255
|
+
_kind: statusEvent.kind,
|
|
256
|
+
_seq: statusEvent.seq,
|
|
257
|
+
_ts: statusEvent.ts,
|
|
258
|
+
_summary: statusEvent.summary,
|
|
259
|
+
_text: statusEvent.text,
|
|
260
|
+
_status: statusEvent.status,
|
|
261
|
+
},
|
|
262
|
+
timestamp: new Date(statusEvent.ts),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
deps.onAgentTaskComplete(task.taskId, delegatedSlug, output);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
269
|
+
failTask(task.taskId, msg);
|
|
270
|
+
updateTaskResult(task.taskId, "error", msg);
|
|
271
|
+
const statusEvent = appendTaskStatusEvent(task.taskId, "error", msg);
|
|
272
|
+
if (statusEvent) {
|
|
273
|
+
void agentEventBus.emit({
|
|
274
|
+
type: "session:tool_call",
|
|
275
|
+
sessionId: task.taskId,
|
|
276
|
+
payload: {
|
|
277
|
+
toolName: "",
|
|
278
|
+
toolArgs: {},
|
|
279
|
+
_kind: statusEvent.kind,
|
|
280
|
+
_seq: statusEvent.seq,
|
|
281
|
+
_ts: statusEvent.ts,
|
|
282
|
+
_summary: statusEvent.summary,
|
|
283
|
+
_text: statusEvent.text,
|
|
284
|
+
_status: statusEvent.status,
|
|
285
|
+
},
|
|
286
|
+
timestamp: new Date(statusEvent.ts),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
deps.onAgentTaskComplete(task.taskId, delegatedSlug, `Error: ${msg}`);
|
|
290
|
+
}
|
|
291
|
+
})();
|
|
292
|
+
const model = (args.model_override && args.model_override.length > 0)
|
|
293
|
+
? args.model_override
|
|
294
|
+
: (agent.model === "auto" ? "claude-sonnet-4.6" : agent.model || "claude-sonnet-4.6");
|
|
295
|
+
return `Task delegated to @${delegatedSlug} (${model}). Task ID: ${task.taskId}. I'll notify you when it's done.`;
|
|
296
|
+
}
|
|
297
|
+
let session;
|
|
298
|
+
try {
|
|
299
|
+
const allTools = createTools(deps);
|
|
300
|
+
session = await createEphemeralAgentSession(agent.slug, deps.client, allTools, args.model_override, undefined, taskId);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
304
|
+
return `Failed to create session for @${delegatedSlug}: ${msg}`;
|
|
305
|
+
}
|
|
213
306
|
// Capture the parent's activity callback so the child session can stream
|
|
214
307
|
// its events back to the originating SSE connection. This survives past
|
|
215
308
|
// the parent assistant turn — the child runs long after the parent's
|
|
@@ -849,10 +942,12 @@ export function createTools(deps) {
|
|
|
849
942
|
? observationProposalPayloadSchema.parse(parsedArgs.payload)
|
|
850
943
|
: parsedArgs.kind === "decision"
|
|
851
944
|
? decisionProposalPayloadSchema.parse(parsedArgs.payload)
|
|
852
|
-
:
|
|
945
|
+
: parsedArgs.kind === "entity"
|
|
946
|
+
? entityProposalPayloadSchema.parse(parsedArgs.payload)
|
|
947
|
+
: actionItemProposalPayloadSchema.parse(parsedArgs.payload);
|
|
853
948
|
const proposal = queueMemoryProposal({
|
|
854
949
|
kind: parsedArgs.kind,
|
|
855
|
-
scopeSlug: parsedArgs.scope_slug,
|
|
950
|
+
scopeSlug: resolveProposalScopeSlug(parsedArgs.scope_slug),
|
|
856
951
|
payload,
|
|
857
952
|
confidence: parsedArgs.confidence ?? 0.5,
|
|
858
953
|
reason: parsedArgs.reason,
|
|
@@ -872,12 +967,164 @@ export function createTools(deps) {
|
|
|
872
967
|
}
|
|
873
968
|
},
|
|
874
969
|
}),
|
|
970
|
+
defineTool("memory_add_action_item", {
|
|
971
|
+
description: "Add a scoped memory action item/reminder. Orchestrator-only write tool for follow-ups and proactive reminders.",
|
|
972
|
+
parameters: z.object({
|
|
973
|
+
scope: z.string().optional().describe("Optional memory scope slug. Defaults to active scope, then keyword inference."),
|
|
974
|
+
title: z.string().min(1).describe("Short action item title."),
|
|
975
|
+
detail: z.string().optional().describe("Longer action item detail."),
|
|
976
|
+
due_at: z.string().optional().describe("Optional ISO due timestamp."),
|
|
977
|
+
entity_name: z.string().optional().describe("Optional entity name to attach. Auto-upserts the entity if provided."),
|
|
978
|
+
entity_kind: z.string().optional().describe("Required when entity_name is provided."),
|
|
979
|
+
source: z.string().optional().describe("Action item source, e.g. manual, subagent_proposal, external."),
|
|
980
|
+
}),
|
|
981
|
+
handler: async (args) => {
|
|
982
|
+
const denied = requireOrchestratorMemoryWrite();
|
|
983
|
+
if (denied)
|
|
984
|
+
return denied;
|
|
985
|
+
if (args.entity_name && !args.entity_kind) {
|
|
986
|
+
return "entity_kind is required when entity_name is provided.";
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
const { scopeId, scopeSlug } = resolveMemoryScopeForWrite(args.scope, `${args.title}\n${args.detail ?? ""}`);
|
|
990
|
+
const entity = args.entity_name
|
|
991
|
+
? upsertEntity({
|
|
992
|
+
scope_id: scopeId,
|
|
993
|
+
kind: args.entity_kind,
|
|
994
|
+
name: args.entity_name,
|
|
995
|
+
})
|
|
996
|
+
: undefined;
|
|
997
|
+
const actionItem = recordActionItem({
|
|
998
|
+
scope_id: scopeId,
|
|
999
|
+
entity_id: entity?.id,
|
|
1000
|
+
title: args.title,
|
|
1001
|
+
detail: args.detail,
|
|
1002
|
+
due_at: args.due_at,
|
|
1003
|
+
source: args.source ?? "manual",
|
|
1004
|
+
});
|
|
1005
|
+
return {
|
|
1006
|
+
ok: true,
|
|
1007
|
+
id: actionItem.id,
|
|
1008
|
+
scope: scopeSlug,
|
|
1009
|
+
status: actionItem.status,
|
|
1010
|
+
entity_id: entity?.id,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
return err instanceof Error ? err.message : String(err);
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
}),
|
|
1018
|
+
defineTool("memory_complete_action_item", {
|
|
1019
|
+
description: "Complete a memory action item. Orchestrator-only write tool.",
|
|
1020
|
+
parameters: z.object({
|
|
1021
|
+
id: z.number().int().positive(),
|
|
1022
|
+
resolution_reason: z.string().optional(),
|
|
1023
|
+
}),
|
|
1024
|
+
handler: async (args) => {
|
|
1025
|
+
const denied = requireOrchestratorMemoryWrite();
|
|
1026
|
+
if (denied)
|
|
1027
|
+
return denied;
|
|
1028
|
+
try {
|
|
1029
|
+
const actionItem = completeActionItem(args.id, args.resolution_reason);
|
|
1030
|
+
return { ok: true, id: actionItem.id, status: actionItem.status, resolved_at: actionItem.resolvedAt };
|
|
1031
|
+
}
|
|
1032
|
+
catch (err) {
|
|
1033
|
+
return err instanceof Error ? err.message : String(err);
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
}),
|
|
1037
|
+
defineTool("memory_drop_action_item", {
|
|
1038
|
+
description: "Drop a memory action item with a reason. Orchestrator-only write tool.",
|
|
1039
|
+
parameters: z.object({
|
|
1040
|
+
id: z.number().int().positive(),
|
|
1041
|
+
reason: z.string().min(1),
|
|
1042
|
+
}),
|
|
1043
|
+
handler: async (args) => {
|
|
1044
|
+
const denied = requireOrchestratorMemoryWrite();
|
|
1045
|
+
if (denied)
|
|
1046
|
+
return denied;
|
|
1047
|
+
try {
|
|
1048
|
+
const actionItem = dropActionItem(args.id, args.reason);
|
|
1049
|
+
return { ok: true, id: actionItem.id, status: actionItem.status, resolved_at: actionItem.resolvedAt };
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
return err instanceof Error ? err.message : String(err);
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
}),
|
|
1056
|
+
defineTool("memory_snooze_action_item", {
|
|
1057
|
+
description: "Snooze a memory action item until an ISO timestamp. Orchestrator-only write tool.",
|
|
1058
|
+
parameters: z.object({
|
|
1059
|
+
id: z.number().int().positive(),
|
|
1060
|
+
snooze_until: z.string().min(1),
|
|
1061
|
+
}),
|
|
1062
|
+
handler: async (args) => {
|
|
1063
|
+
const denied = requireOrchestratorMemoryWrite();
|
|
1064
|
+
if (denied)
|
|
1065
|
+
return denied;
|
|
1066
|
+
try {
|
|
1067
|
+
const actionItem = snoozeActionItem(args.id, args.snooze_until);
|
|
1068
|
+
return {
|
|
1069
|
+
ok: true,
|
|
1070
|
+
id: actionItem.id,
|
|
1071
|
+
status: actionItem.status,
|
|
1072
|
+
snooze_until: actionItem.snoozeUntil,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
catch (err) {
|
|
1076
|
+
return err instanceof Error ? err.message : String(err);
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
}),
|
|
1080
|
+
defineTool("memory_list_action_items", {
|
|
1081
|
+
description: "List scoped memory action items/reminders. Defaults to currently actionable open items.",
|
|
1082
|
+
parameters: z.object({
|
|
1083
|
+
scope: z.string().optional().describe("Optional memory scope slug. Defaults to active scope when available."),
|
|
1084
|
+
status: z.enum(["open", "done", "dropped", "snoozed"]).optional().describe("Optional status filter."),
|
|
1085
|
+
due_before: z.string().optional().describe("Optional ISO timestamp; only include items due at or before this time."),
|
|
1086
|
+
includeArchived: z.boolean().optional().describe("Include cold-tier items. Defaults to false."),
|
|
1087
|
+
}),
|
|
1088
|
+
handler: async (args) => {
|
|
1089
|
+
const requestedScope = args.scope ? getMemoryScope(args.scope) : undefined;
|
|
1090
|
+
if (args.scope && !requestedScope) {
|
|
1091
|
+
return `Unknown memory scope '${args.scope}'.`;
|
|
1092
|
+
}
|
|
1093
|
+
const activeScope = getMemoryActiveScope();
|
|
1094
|
+
const scope = requestedScope ?? activeScope ?? undefined;
|
|
1095
|
+
const actionItems = listActionItems({
|
|
1096
|
+
scope_id: scope?.id,
|
|
1097
|
+
status: args.status,
|
|
1098
|
+
due_before: args.due_before,
|
|
1099
|
+
includeArchived: args.includeArchived,
|
|
1100
|
+
});
|
|
1101
|
+
return {
|
|
1102
|
+
active_scope: activeScope ? { slug: activeScope.slug, title: activeScope.title } : null,
|
|
1103
|
+
scope: scope ? { slug: scope.slug, title: scope.title } : null,
|
|
1104
|
+
action_items: actionItems.map((item) => ({
|
|
1105
|
+
id: item.id,
|
|
1106
|
+
scope_id: item.scopeId,
|
|
1107
|
+
entity_id: item.entityId,
|
|
1108
|
+
title: item.title,
|
|
1109
|
+
detail: item.detail,
|
|
1110
|
+
status: item.status,
|
|
1111
|
+
due_at: item.dueAt,
|
|
1112
|
+
snooze_until: item.snoozeUntil,
|
|
1113
|
+
source: item.source,
|
|
1114
|
+
created_at: item.createdAt,
|
|
1115
|
+
updated_at: item.updatedAt,
|
|
1116
|
+
resolved_at: item.resolvedAt,
|
|
1117
|
+
resolution_reason: item.resolutionReason,
|
|
1118
|
+
})),
|
|
1119
|
+
};
|
|
1120
|
+
},
|
|
1121
|
+
}),
|
|
875
1122
|
defineTool("memory_recall", {
|
|
876
1123
|
description: "Search scoped agent memory with FTS-backed recall. Use this for the new agent-memory store, not the wiki-backed recall tool.",
|
|
877
1124
|
parameters: z.object({
|
|
878
1125
|
query: z.string().describe("Query text to search for."),
|
|
879
1126
|
scope: z.string().optional().describe("Optional scope slug. Defaults to the active scope when available."),
|
|
880
|
-
kinds: z.array(z.enum(["observation", "decision", "entity"])).optional()
|
|
1127
|
+
kinds: z.array(z.enum(["observation", "decision", "entity", "action_item"])).optional()
|
|
881
1128
|
.describe("Optional filter for memory entry kinds."),
|
|
882
1129
|
limit: z.number().int().positive().optional().describe("Maximum number of ranked hits to return. Defaults to 10."),
|
|
883
1130
|
includeSuperseded: z.boolean().optional().describe("Include superseded observations and decisions. Defaults to false."),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import test from "node:test";
|
|
@@ -46,6 +46,7 @@ test("memory_set_scope invalidates the orchestrator session after scheduling the
|
|
|
46
46
|
invalidateOrchestratorSession: (sessionKey) => {
|
|
47
47
|
events.push(`invalidate:${sessionKey}`);
|
|
48
48
|
},
|
|
49
|
+
sendToAgentSession: async () => "",
|
|
49
50
|
switchSessionModel: async () => { },
|
|
50
51
|
},
|
|
51
52
|
});
|
|
@@ -194,6 +195,178 @@ test("memory_propose queues pending proposals, defaults scope from the active sc
|
|
|
194
195
|
assert.equal(payload.reason, "The user explicitly asked for the new proposal path.");
|
|
195
196
|
assert.equal(payload.payload.content, "Subagents can queue durable observations for orchestrator review.");
|
|
196
197
|
});
|
|
198
|
+
test("memory_propose from a persistent agent is bound to that agent's scope", async () => {
|
|
199
|
+
const home = process.env.CHAPTERHOUSE_HOME;
|
|
200
|
+
assert.ok(home, "test home should be set");
|
|
201
|
+
const chapterhouseHome = home.endsWith(".chapterhouse") ? home : join(home, ".chapterhouse");
|
|
202
|
+
const agentsDir = join(chapterhouseHome, "agents");
|
|
203
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
204
|
+
writeFileSync(join(agentsDir, "bellonda.agent.md"), [
|
|
205
|
+
"---",
|
|
206
|
+
"name: Bellonda",
|
|
207
|
+
"description: Mentat of the infrastructure domain",
|
|
208
|
+
"model: claude-sonnet-4.6",
|
|
209
|
+
"persistent: true",
|
|
210
|
+
"scope: infra",
|
|
211
|
+
"---",
|
|
212
|
+
"",
|
|
213
|
+
"You are Bellonda.",
|
|
214
|
+
].join("\n"));
|
|
215
|
+
const { toolsModule, agentsModule, dbModule } = await loadModules();
|
|
216
|
+
agentsModule.loadAgents();
|
|
217
|
+
const tools = toolsModule.createTools({
|
|
218
|
+
client: { async listModels() { return []; } },
|
|
219
|
+
onAgentTaskComplete: () => { },
|
|
220
|
+
});
|
|
221
|
+
const bindToolsToAgent = agentsModule.bindToolsToAgent;
|
|
222
|
+
assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
|
|
223
|
+
const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
|
|
224
|
+
const bellondaTools = bindToolsToAgent("bellonda", tools, "task-persistent-scope-001");
|
|
225
|
+
await findTool(chapterhouseTools, "memory_set_scope").handler({ slug: "chapterhouse" }, {});
|
|
226
|
+
const memoryModule = await import("../memory/index.js");
|
|
227
|
+
const proposed = await memoryModule.withActiveScope("infra", () => findTool(bellondaTools, "memory_propose").handler({
|
|
228
|
+
kind: "observation",
|
|
229
|
+
scope_slug: "chapterhouse",
|
|
230
|
+
payload: {
|
|
231
|
+
content: "Persistent agents should not be able to write proposals outside their bound scope.",
|
|
232
|
+
},
|
|
233
|
+
}, {}));
|
|
234
|
+
assert.equal(proposed.status, "queued");
|
|
235
|
+
const row = dbModule.getDb().prepare(`
|
|
236
|
+
SELECT source_agent, source_task_id, payload
|
|
237
|
+
FROM mem_inbox
|
|
238
|
+
WHERE id = ?
|
|
239
|
+
`).get(proposed.proposal_id);
|
|
240
|
+
assert.ok(row, "memory_propose should insert a mem_inbox row");
|
|
241
|
+
assert.equal(row.source_agent, "bellonda");
|
|
242
|
+
assert.equal(row.source_task_id, "task-persistent-scope-001");
|
|
243
|
+
const payload = JSON.parse(row.payload);
|
|
244
|
+
assert.equal(payload.scope_slug, "infra");
|
|
245
|
+
});
|
|
246
|
+
test("memory_propose accepts entity proposals with entity_kind and queues the full payload", async () => {
|
|
247
|
+
const { toolsModule, agentsModule, dbModule } = await loadModules();
|
|
248
|
+
const tools = toolsModule.createTools({
|
|
249
|
+
client: { async listModels() { return []; } },
|
|
250
|
+
onAgentTaskComplete: () => { },
|
|
251
|
+
});
|
|
252
|
+
const bindToolsToAgent = agentsModule.bindToolsToAgent;
|
|
253
|
+
assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
|
|
254
|
+
const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
|
|
255
|
+
const coderTools = bindToolsToAgent("coder", tools, "task-entity-propose");
|
|
256
|
+
await findTool(chapterhouseTools, "memory_set_scope").handler({ slug: "chapterhouse" }, {});
|
|
257
|
+
const proposed = await findTool(coderTools, "memory_propose").handler({
|
|
258
|
+
kind: "entity",
|
|
259
|
+
payload: {
|
|
260
|
+
name: "truenas",
|
|
261
|
+
entity_kind: "host",
|
|
262
|
+
summary: "NAS host used by Bellonda.",
|
|
263
|
+
},
|
|
264
|
+
confidence: 0.8,
|
|
265
|
+
}, {});
|
|
266
|
+
assert.equal(proposed.status, "queued");
|
|
267
|
+
const row = dbModule.getDb().prepare(`
|
|
268
|
+
SELECT payload
|
|
269
|
+
FROM mem_inbox
|
|
270
|
+
WHERE id = ?
|
|
271
|
+
`).get(proposed.proposal_id);
|
|
272
|
+
assert.ok(row, "memory_propose should insert a mem_inbox row");
|
|
273
|
+
const payload = JSON.parse(row.payload);
|
|
274
|
+
assert.equal(payload.kind, "entity");
|
|
275
|
+
assert.equal(payload.scope_slug, "chapterhouse");
|
|
276
|
+
assert.deepEqual(payload.payload, {
|
|
277
|
+
name: "truenas",
|
|
278
|
+
entity_kind: "host",
|
|
279
|
+
summary: "NAS host used by Bellonda.",
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
test("action item memory tools add, list, complete, drop, and snooze action items", async () => {
|
|
283
|
+
const { toolsModule, agentsModule } = await loadModules();
|
|
284
|
+
const tools = toolsModule.createTools({
|
|
285
|
+
client: { async listModels() { return []; } },
|
|
286
|
+
onAgentTaskComplete: () => { },
|
|
287
|
+
});
|
|
288
|
+
const bindToolsToAgent = agentsModule.bindToolsToAgent;
|
|
289
|
+
assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
|
|
290
|
+
const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
|
|
291
|
+
const coderTools = bindToolsToAgent("coder", tools);
|
|
292
|
+
const memoryAddActionItem = findTool(chapterhouseTools, "memory_add_action_item");
|
|
293
|
+
const memoryListActionItems = findTool(coderTools, "memory_list_action_items");
|
|
294
|
+
const memoryCompleteActionItem = findTool(chapterhouseTools, "memory_complete_action_item");
|
|
295
|
+
const memoryDropActionItem = findTool(chapterhouseTools, "memory_drop_action_item");
|
|
296
|
+
const memorySnoozeActionItem = findTool(chapterhouseTools, "memory_snooze_action_item");
|
|
297
|
+
const visibleToCoder = agentsModule.filterToolsForAgent({
|
|
298
|
+
slug: "coder",
|
|
299
|
+
name: "Coder",
|
|
300
|
+
description: "Software engineer",
|
|
301
|
+
model: "gpt-5.4",
|
|
302
|
+
systemMessage: "test",
|
|
303
|
+
}, tools);
|
|
304
|
+
assert.equal(visibleToCoder.some((tool) => tool.name === "memory_list_action_items"), true);
|
|
305
|
+
assert.equal(visibleToCoder.some((tool) => tool.name === "memory_add_action_item"), false);
|
|
306
|
+
const added = await memoryAddActionItem.handler({
|
|
307
|
+
scope: "chapterhouse",
|
|
308
|
+
title: "Migrate feature ideas",
|
|
309
|
+
detail: "Move the parked feature-ideas.md page into mem_action_items.",
|
|
310
|
+
due_at: "2026-05-15T12:00:00.000Z",
|
|
311
|
+
entity_name: "Chapterhouse",
|
|
312
|
+
entity_kind: "project",
|
|
313
|
+
source: "test",
|
|
314
|
+
}, {});
|
|
315
|
+
assert.equal(added.ok, true);
|
|
316
|
+
const addedId = added.id;
|
|
317
|
+
const listed = await memoryListActionItems.handler({ scope: "chapterhouse" }, {});
|
|
318
|
+
assert.equal((listed.action_items).some((item) => item.id === addedId), true);
|
|
319
|
+
const completed = await memoryCompleteActionItem.handler({ id: addedId, resolution_reason: "Done." }, {});
|
|
320
|
+
assert.equal(completed.ok, true);
|
|
321
|
+
assert.equal(completed.status, "done");
|
|
322
|
+
const afterComplete = await memoryListActionItems.handler({ scope: "chapterhouse" }, {});
|
|
323
|
+
assert.equal((afterComplete.action_items).some((item) => item.id === addedId), false);
|
|
324
|
+
const dropped = await memoryAddActionItem.handler({ scope: "chapterhouse", title: "Drop me" }, {});
|
|
325
|
+
const droppedResult = await memoryDropActionItem.handler({
|
|
326
|
+
id: dropped.id,
|
|
327
|
+
reason: "No longer needed.",
|
|
328
|
+
}, {});
|
|
329
|
+
assert.equal(droppedResult.status, "dropped");
|
|
330
|
+
const snoozed = await memoryAddActionItem.handler({ scope: "chapterhouse", title: "Snooze me" }, {});
|
|
331
|
+
const snoozedResult = await memorySnoozeActionItem.handler({
|
|
332
|
+
id: snoozed.id,
|
|
333
|
+
snooze_until: "2999-01-01T00:00:00.000Z",
|
|
334
|
+
}, {});
|
|
335
|
+
assert.equal(snoozedResult.status, "snoozed");
|
|
336
|
+
});
|
|
337
|
+
test("memory_propose accepts action_item proposals with a resolvable payload shape", async () => {
|
|
338
|
+
const { toolsModule, agentsModule, dbModule } = await loadModules();
|
|
339
|
+
const tools = toolsModule.createTools({
|
|
340
|
+
client: { async listModels() { return []; } },
|
|
341
|
+
onAgentTaskComplete: () => { },
|
|
342
|
+
});
|
|
343
|
+
const bindToolsToAgent = agentsModule.bindToolsToAgent;
|
|
344
|
+
assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
|
|
345
|
+
const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
|
|
346
|
+
const coderTools = bindToolsToAgent("coder", tools, "task-action-propose");
|
|
347
|
+
await findTool(chapterhouseTools, "memory_set_scope").handler({ slug: "chapterhouse" }, {});
|
|
348
|
+
const proposed = await findTool(coderTools, "memory_propose").handler({
|
|
349
|
+
kind: "action_item",
|
|
350
|
+
payload: {
|
|
351
|
+
title: "Remind infra about high disk usage",
|
|
352
|
+
detail: "Next time disk exceeds 85%, notify Bellonda.",
|
|
353
|
+
due_at: "2026-05-15T12:00:00.000Z",
|
|
354
|
+
source: "test",
|
|
355
|
+
},
|
|
356
|
+
confidence: 0.8,
|
|
357
|
+
}, {});
|
|
358
|
+
assert.equal(proposed.status, "queued");
|
|
359
|
+
const row = dbModule.getDb().prepare(`
|
|
360
|
+
SELECT payload
|
|
361
|
+
FROM mem_inbox
|
|
362
|
+
WHERE id = ?
|
|
363
|
+
`).get(proposed.proposal_id);
|
|
364
|
+
const payload = JSON.parse(row.payload);
|
|
365
|
+
assert.equal(payload.kind, "action_item");
|
|
366
|
+
assert.equal(payload.scope_slug, "chapterhouse");
|
|
367
|
+
assert.equal(payload.payload.title, "Remind infra about high disk usage");
|
|
368
|
+
assert.equal(payload.payload.detail, "Next time disk exceeds 85%, notify Bellonda.");
|
|
369
|
+
});
|
|
197
370
|
test("memory_propose rejects invalid proposal kinds", async () => {
|
|
198
371
|
const { toolsModule } = await loadModules();
|
|
199
372
|
const tools = toolsModule.createTools({
|
|
@@ -205,7 +378,7 @@ test("memory_propose rejects invalid proposal kinds", async () => {
|
|
|
205
378
|
kind: "pattern",
|
|
206
379
|
payload: { content: "invalid kind" },
|
|
207
380
|
}, {});
|
|
208
|
-
assert.match(String(result), /observation|decision|entity/i);
|
|
381
|
+
assert.match(String(result), /observation|decision|entity|action_item/i);
|
|
209
382
|
});
|
|
210
383
|
test("memory_housekeep is orchestrator-only and returns housekeeping summaries", async () => {
|
|
211
384
|
const { toolsModule, agentsModule, dbModule } = await loadModules();
|
package/dist/daemon.js
CHANGED
|
@@ -19,7 +19,9 @@ import { registerShutdownSignals } from "./shutdown-signals.js";
|
|
|
19
19
|
import { logger } from "./util/logger.js";
|
|
20
20
|
import { CHAPTERHOUSE_VERSION } from "./version.js";
|
|
21
21
|
import { isWorkiqAutoInstallEnabled, ensureWorkiqMcpEntry } from "./copilot/workiq-installer.js";
|
|
22
|
+
import { MemoryHousekeepingScheduler } from "./memory/housekeeping-scheduler.js";
|
|
22
23
|
const log = logger.child({ module: "daemon" });
|
|
24
|
+
let memoryHousekeepingScheduler;
|
|
23
25
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
24
26
|
/**
|
|
25
27
|
* How long the daemon waits for in-flight work to finish before forcing an exit.
|
|
@@ -149,6 +151,8 @@ async function main() {
|
|
|
149
151
|
});
|
|
150
152
|
// Start HTTP API + serve the web UI
|
|
151
153
|
await startApiServer();
|
|
154
|
+
memoryHousekeepingScheduler = new MemoryHousekeepingScheduler();
|
|
155
|
+
memoryHousekeepingScheduler.start();
|
|
152
156
|
if (config.chapterhouseMode === "personal" && (config.adoPat || config.teamChapterhouseUrl)) {
|
|
153
157
|
new StandupScheduler().schedule();
|
|
154
158
|
}
|
|
@@ -202,6 +206,7 @@ async function shutdown() {
|
|
|
202
206
|
forceTimer.unref();
|
|
203
207
|
// Destroy all active agent sessions
|
|
204
208
|
await shutdownAgents();
|
|
209
|
+
await memoryHousekeepingScheduler?.stop();
|
|
205
210
|
try {
|
|
206
211
|
stopEpisodeWriter();
|
|
207
212
|
}
|
|
@@ -223,6 +228,7 @@ export async function restartDaemon() {
|
|
|
223
228
|
}
|
|
224
229
|
// Destroy all active agent sessions
|
|
225
230
|
await shutdownAgents();
|
|
231
|
+
await memoryHousekeepingScheduler?.stop();
|
|
226
232
|
try {
|
|
227
233
|
stopEpisodeWriter();
|
|
228
234
|
}
|