chapterhouse 0.7.0 → 0.8.1
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/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +238 -2
- package/dist/api/server.test.js +199 -0
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agents.js +3 -4
- package/dist/copilot/agents.test.js +12 -1
- package/dist/copilot/orchestrator.js +12 -1
- package/dist/copilot/orchestrator.test.js +3 -7
- package/dist/copilot/system-message.js +12 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +193 -375
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +80 -59
- package/dist/copilot/turn-event-log-env.test.js +11 -15
- package/dist/daemon.js +19 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/eot.js +30 -8
- package/dist/memory/eot.test.js +220 -6
- 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 +108 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/paths.js +31 -11
- package/dist/store/db.js +187 -4
- package/dist/store/db.test.js +66 -2
- package/dist/test/helpers/reset-singletons.js +8 -0
- package/dist/test/helpers/reset-singletons.test.js +37 -0
- package/dist/test/setup-env.js +9 -1
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +143 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/fs.js +22 -13
- package/dist/wiki/index-manager.js +305 -330
- package/dist/wiki/index-manager.test.js +265 -144
- 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/log-manager.js +8 -5
- package/dist/wiki/log-manager.test.js +4 -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 +1 -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-DuKYxMIR.css → index-5kz9aRU9.css} +1 -1
- package/web/dist/assets/{index-DytB69KC.js → index-BbX9RKf3.js} +91 -89
- 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-DytB69KC.js.map +0 -1
|
@@ -555,4 +555,36 @@ test("memory_promote and memory_demote are orchestrator-only manual tier control
|
|
|
555
555
|
assert.equal(demoted.ok, true);
|
|
556
556
|
assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(observationId).tier, "cold");
|
|
557
557
|
});
|
|
558
|
+
test("memory_remember dual-writes decisions to the wiki timeline when an entity is provided", async () => {
|
|
559
|
+
const { toolsModule, agentsModule, dbModule } = await loadModules();
|
|
560
|
+
const tools = toolsModule.createTools({
|
|
561
|
+
client: { async listModels() { return []; } },
|
|
562
|
+
onAgentTaskComplete: () => { },
|
|
563
|
+
});
|
|
564
|
+
const wikiFs = await import(new URL("../wiki/fs.js", import.meta.url).href);
|
|
565
|
+
wikiFs.ensureWikiStructure();
|
|
566
|
+
wikiFs.writePage("pages/projects/project-x/index.md", "---\ntitle: Project X\nsummary: Project X summary\nupdated: 2026-05-14\ntags: []\n---\n\n# Project X\n\n## Summary\n\nCurrent state.\n");
|
|
567
|
+
const bindToolsToAgent = agentsModule.bindToolsToAgent;
|
|
568
|
+
const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
|
|
569
|
+
const memoryRemember = findTool(chapterhouseTools, "memory_remember");
|
|
570
|
+
const result = await memoryRemember.handler({
|
|
571
|
+
scope: "chapterhouse",
|
|
572
|
+
kind: "decision",
|
|
573
|
+
entity_name: "project-x",
|
|
574
|
+
entity_kind: "project",
|
|
575
|
+
title: "Ship append-only decisions",
|
|
576
|
+
content: "Store the decision in SQLite and mirror it into the project timeline.",
|
|
577
|
+
}, {});
|
|
578
|
+
assert.equal(result.ok, true);
|
|
579
|
+
const db = dbModule.getDb();
|
|
580
|
+
const decision = db.prepare(`SELECT title, rationale FROM mem_decisions WHERE title = ?`).get("Ship append-only decisions");
|
|
581
|
+
assert.deepEqual(decision, {
|
|
582
|
+
title: "Ship append-only decisions",
|
|
583
|
+
rationale: "Store the decision in SQLite and mirror it into the project timeline.",
|
|
584
|
+
});
|
|
585
|
+
const page = wikiFs.readPage("pages/projects/project-x/index.md") ?? "";
|
|
586
|
+
assert.match(page, /## Timeline/);
|
|
587
|
+
assert.match(page, /Ship append-only decisions/);
|
|
588
|
+
assert.match(page, /Store the decision in SQLite and mirror it into the project timeline\./);
|
|
589
|
+
});
|
|
558
590
|
//# sourceMappingURL=tools.memory.test.js.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
4
|
import test from "node:test";
|
|
6
5
|
async function loadToolsModule() {
|
|
7
6
|
return await import(new URL(`./tools.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
@@ -9,10 +8,14 @@ async function loadToolsModule() {
|
|
|
9
8
|
async function readWikiArtifacts() {
|
|
10
9
|
const nonce = `${Date.now()}-${Math.random()}`;
|
|
11
10
|
const wikiFs = await import(new URL(`../wiki/fs.js?case=${nonce}`, import.meta.url).href);
|
|
12
|
-
|
|
11
|
+
const indexManager = await import(new URL(`../wiki/index-manager.js?case=${nonce}`, import.meta.url).href);
|
|
12
|
+
return { wikiFs, indexManager };
|
|
13
13
|
}
|
|
14
|
+
test.before(() => {
|
|
15
|
+
mkdirSync(join(process.cwd(), ".test-work"), { recursive: true });
|
|
16
|
+
});
|
|
14
17
|
test.beforeEach(() => {
|
|
15
|
-
process.env.CHAPTERHOUSE_HOME = mkdtempSync(join(
|
|
18
|
+
process.env.CHAPTERHOUSE_HOME = mkdtempSync(join(process.cwd(), ".test-work", "chapterhouse-tools-wiki-"));
|
|
16
19
|
process.env.CHAPTERHOUSE_AGENT_NAME = "tools-test-agent";
|
|
17
20
|
});
|
|
18
21
|
test.afterEach(async () => {
|
|
@@ -38,9 +41,9 @@ test("wiki_update returns validation errors instead of throwing for invalid fron
|
|
|
38
41
|
content: "# Chapterhouse\n\nRuntime notes.\n",
|
|
39
42
|
});
|
|
40
43
|
assert.deepEqual(result, {
|
|
41
|
-
error: "Wiki page frontmatter violates the required shape: missing
|
|
44
|
+
error: "Wiki page frontmatter violates the required shape: missing 'summary'. Use:\n---\ntitle: <title>\nsummary: <plain-text one-line summary, max 200 chars>\nupdated: YYYY-MM-DD\ntags: []\nrelated: []\n---",
|
|
42
45
|
});
|
|
43
|
-
const wikiFs = await readWikiArtifacts();
|
|
46
|
+
const { wikiFs } = await readWikiArtifacts();
|
|
44
47
|
assert.equal(wikiFs.readPage("pages/shared/chapterhouse.md"), undefined);
|
|
45
48
|
});
|
|
46
49
|
test("wiki_update returns descriptive validation errors for malformed summaries and unknown tags", async () => {
|
|
@@ -99,22 +102,28 @@ Runtime notes.
|
|
|
99
102
|
`,
|
|
100
103
|
});
|
|
101
104
|
assert.equal(result, "Wiki page updated: Chapterhouse (pages/shared/chapterhouse.md)");
|
|
102
|
-
const wikiFs = await readWikiArtifacts();
|
|
105
|
+
const { wikiFs, indexManager } = await readWikiArtifacts();
|
|
106
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
103
107
|
assert.match(wikiFs.readPage("pages/shared/chapterhouse.md") ?? "", /summary: Runtime notes/);
|
|
104
|
-
assert.
|
|
108
|
+
assert.deepEqual(indexManager.parseIndex(), [
|
|
109
|
+
{
|
|
110
|
+
path: "pages/shared/chapterhouse.md",
|
|
111
|
+
title: "Chapterhouse",
|
|
112
|
+
summary: "Runtime notes",
|
|
113
|
+
section: "shared",
|
|
114
|
+
tags: ["engineering"],
|
|
115
|
+
updated: today,
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
105
118
|
});
|
|
106
|
-
test("wiki tools append audit entries to pages/_meta/log.md", async () => {
|
|
119
|
+
test("retained wiki tools append audit entries to pages/_meta/log.md", async () => {
|
|
107
120
|
const toolsModule = await loadToolsModule();
|
|
108
121
|
const tools = toolsModule.createTools({
|
|
109
122
|
client: { async listModels() { return []; } },
|
|
110
123
|
onAgentTaskComplete: () => { },
|
|
111
124
|
});
|
|
112
125
|
const wikiUpdate = tools.find((entry) => entry.name === "wiki_update");
|
|
113
|
-
|
|
114
|
-
const wikiLint = tools.find((entry) => entry.name === "wiki_lint");
|
|
115
|
-
const wikiRebuildIndex = tools.find((entry) => entry.name === "wiki_rebuild_index");
|
|
116
|
-
const forget = tools.find((entry) => entry.name === "forget");
|
|
117
|
-
assert.ok(wikiUpdate && wikiIngest && wikiLint && wikiRebuildIndex && forget);
|
|
126
|
+
assert.ok(wikiUpdate);
|
|
118
127
|
await wikiUpdate.handler({
|
|
119
128
|
path: "pages/shared/chapterhouse.md",
|
|
120
129
|
title: "Chapterhouse",
|
|
@@ -133,66 +142,78 @@ tags: [engineering]
|
|
|
133
142
|
Runtime notes with enough content to avoid incidental lint noise in the audit-log test.
|
|
134
143
|
`,
|
|
135
144
|
});
|
|
136
|
-
await
|
|
137
|
-
type: "text",
|
|
138
|
-
source: "Source content for the wiki ingest audit log test.",
|
|
139
|
-
name: "audit-log-source",
|
|
140
|
-
});
|
|
141
|
-
await wikiLint.handler({});
|
|
142
|
-
await wikiRebuildIndex.handler({});
|
|
143
|
-
await forget.handler({ page_path: "pages/shared/chapterhouse.md" });
|
|
144
|
-
const wikiFs = await readWikiArtifacts();
|
|
145
|
+
const { wikiFs } = await readWikiArtifacts();
|
|
145
146
|
const log = wikiFs.readLogFile();
|
|
146
147
|
assert.match(log, /update \| wiki_update: Chapterhouse \(pages\/shared\/chapterhouse\.md\) \| tools-test-agent/);
|
|
147
|
-
assert.match(log, /ingest \| Ingested text: audit-log-source \(\d+ chars\) \| tools-test-agent/);
|
|
148
|
-
assert.match(log, /lint \| .* \| tools-test-agent/);
|
|
149
|
-
assert.match(log, /rebuild-index \| wiki_rebuild_index: rebuilt \d+ entries from pages on disk \| tools-test-agent/);
|
|
150
|
-
assert.match(log, /delete \| forget: deleted page pages\/shared\/chapterhouse\.md \| tools-test-agent/);
|
|
151
148
|
});
|
|
152
|
-
test("
|
|
149
|
+
test("wiki_reindex rebuilds wiki_pages from disk on demand", async () => {
|
|
153
150
|
const toolsModule = await loadToolsModule();
|
|
154
151
|
const tools = toolsModule.createTools({
|
|
155
152
|
client: { async listModels() { return []; } },
|
|
156
153
|
onAgentTaskComplete: () => { },
|
|
157
154
|
});
|
|
158
|
-
const
|
|
159
|
-
assert.ok(
|
|
160
|
-
const wikiFs = await readWikiArtifacts();
|
|
161
|
-
wikiFs.
|
|
162
|
-
|
|
155
|
+
const wikiReindex = tools.find((entry) => entry.name === "wiki_reindex");
|
|
156
|
+
assert.ok(wikiReindex);
|
|
157
|
+
const { wikiFs, indexManager } = await readWikiArtifacts();
|
|
158
|
+
wikiFs.ensureWikiStructure();
|
|
159
|
+
wikiFs.writePage("pages/topics/rust/index.md", `---
|
|
160
|
+
title: Rust
|
|
161
|
+
summary: Systems programming
|
|
162
|
+
updated: 2026-05-12
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
# Rust
|
|
163
166
|
`);
|
|
164
|
-
|
|
167
|
+
for (const entry of indexManager.parseIndex()) {
|
|
168
|
+
indexManager.removeFromIndex(entry.path);
|
|
169
|
+
}
|
|
170
|
+
assert.equal(indexManager.parseIndex().length, 0, "Precondition: wiki_pages should start empty");
|
|
171
|
+
const result = await wikiReindex.handler({});
|
|
172
|
+
assert.match(result, /^Reindexed \d+ wiki page\(s\) from disk\.$/);
|
|
173
|
+
assert.ok(indexManager.parseIndex().some((entry) => entry.path === "pages/topics/rust/index.md"));
|
|
174
|
+
assert.match(wikiFs.readLogFile(), /update \| wiki_reindex: rebuilt \d+ page\(s\) \| tools-test-agent/);
|
|
175
|
+
});
|
|
176
|
+
test("removed legacy wiki tools return helpful stub messages without mutating wiki pages", async () => {
|
|
177
|
+
const toolsModule = await loadToolsModule();
|
|
178
|
+
const tools = toolsModule.createTools({
|
|
179
|
+
client: { async listModels() { return []; } },
|
|
180
|
+
onAgentTaskComplete: () => { },
|
|
181
|
+
});
|
|
182
|
+
const wikiUpdate = tools.find((entry) => entry.name === "wiki_update");
|
|
183
|
+
const remember = tools.find((entry) => entry.name === "remember");
|
|
184
|
+
const recall = tools.find((entry) => entry.name === "recall");
|
|
185
|
+
const forget = tools.find((entry) => entry.name === "forget");
|
|
186
|
+
const wikiIngest = tools.find((entry) => entry.name === "wiki_ingest");
|
|
187
|
+
const wikiLint = tools.find((entry) => entry.name === "wiki_lint");
|
|
188
|
+
const wikiRebuildIndex = tools.find((entry) => entry.name === "wiki_rebuild_index");
|
|
189
|
+
const wikiFix = tools.find((entry) => entry.name === "wiki_fix");
|
|
190
|
+
assert.ok(wikiUpdate && remember && recall && forget && wikiIngest && wikiLint && wikiRebuildIndex && wikiFix);
|
|
191
|
+
await wikiUpdate.handler({
|
|
192
|
+
path: "pages/shared/chapterhouse.md",
|
|
193
|
+
title: "Chapterhouse",
|
|
194
|
+
summary: "Runtime notes",
|
|
195
|
+
content: `---
|
|
165
196
|
title: Chapterhouse
|
|
166
197
|
summary: Runtime notes
|
|
167
198
|
updated: 2026-05-12
|
|
168
|
-
tags: [
|
|
199
|
+
tags: [engineering]
|
|
169
200
|
---
|
|
170
201
|
|
|
171
202
|
# Chapterhouse
|
|
172
203
|
|
|
173
|
-
Runtime notes
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const preview = await wikiFix.handler({});
|
|
188
|
-
assert.equal(typeof preview, "object");
|
|
189
|
-
assert.equal(preview.dryRun, true);
|
|
190
|
-
assert.equal(preview.changedFiles, 1);
|
|
191
|
-
assert.match(preview.diff, /--- a\/pages\/projects\/chapterhouse\/index\.md/);
|
|
192
|
-
assert.equal(wikiFs.readPage("pages/projects/chapterhouse/index.md"), before);
|
|
193
|
-
const applied = await wikiFix.handler({ dry_run: false });
|
|
194
|
-
assert.equal(applied.dryRun, false);
|
|
195
|
-
assert.match(wikiFs.readPage("pages/projects/chapterhouse/index.md") ?? "", /tags: \[runbook\]/);
|
|
196
|
-
assert.match(wikiFs.readLogFile(), /fix-tags \| pages\/projects\/chapterhouse\/index\.md \| tools-test-agent/);
|
|
204
|
+
Runtime notes.
|
|
205
|
+
`,
|
|
206
|
+
});
|
|
207
|
+
assert.equal(await remember.handler({ category: "fact", content: "The sky is blue" }), "This tool has been removed. Use wiki_update to write to wiki pages, or memory_remember for agent memory.");
|
|
208
|
+
assert.equal(await recall.handler({ keyword: "sky" }), "This tool has been removed. Use wiki_search to search wiki pages, or memory_recall for agent memory.");
|
|
209
|
+
assert.equal(await forget.handler({ page_path: "pages/shared/chapterhouse.md" }), "This tool has been removed. Use wiki_update to modify wiki pages.");
|
|
210
|
+
assert.equal(await wikiRebuildIndex.handler({}), "This tool has been removed. The wiki index is now maintained automatically via SQLite FTS5.");
|
|
211
|
+
assert.equal(await wikiLint.handler({}), "This tool has been removed. Wiki health checks are no longer needed with SQLite-backed storage.");
|
|
212
|
+
assert.equal(await wikiFix.handler({}), "This tool has been removed. Use wiki_update to correct wiki pages.");
|
|
213
|
+
assert.equal(await wikiIngest.handler({ source: "hello" }), "This tool has been removed. Use wiki_ingest_source instead.");
|
|
214
|
+
const { wikiFs } = await readWikiArtifacts();
|
|
215
|
+
assert.equal(wikiFs.readPage("pages/facts.md"), undefined);
|
|
216
|
+
assert.match(wikiFs.readPage("pages/shared/chapterhouse.md") ?? "", /Runtime notes\./);
|
|
217
|
+
assert.doesNotMatch(wikiFs.readLogFile(), /forget: deleted page|wiki_rebuild_index|wiki_lint|wiki_fix|wiki_ingest:/);
|
|
197
218
|
});
|
|
198
219
|
//# sourceMappingURL=tools.wiki.test.js.map
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
test("SESSION_BUFFER_CAPACITY respects
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
else {
|
|
15
|
-
process.env.CHAPTERHOUSE_SSE_BUFFER_CAPACITY = previous;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
3
|
+
test("SESSION_BUFFER_CAPACITY respects config.sseBufferCapacity", async (t) => {
|
|
4
|
+
t.mock.module("../config.js", {
|
|
5
|
+
namedExports: {
|
|
6
|
+
config: {
|
|
7
|
+
sseBufferCapacity: 3,
|
|
8
|
+
sseReplayLimit: 50,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
const module = await import(`./turn-event-log.js?capacity=${Date.now()}`);
|
|
13
|
+
assert.equal(module.SESSION_BUFFER_CAPACITY, 3);
|
|
18
14
|
});
|
|
19
15
|
//# sourceMappingURL=turn-event-log-env.test.js.map
|
package/dist/daemon.js
CHANGED
|
@@ -21,9 +21,13 @@ import { logger } from "./util/logger.js";
|
|
|
21
21
|
import { CHAPTERHOUSE_VERSION } from "./version.js";
|
|
22
22
|
import { isWorkiqAutoInstallEnabled, ensureWorkiqMcpEntry } from "./copilot/workiq-installer.js";
|
|
23
23
|
import { MemoryHousekeepingScheduler } from "./memory/housekeeping-scheduler.js";
|
|
24
|
+
import { runP6Migration } from "./memory/migration.js";
|
|
25
|
+
import { WikiConsolidationScheduler } from "./wiki/scheduler.js";
|
|
26
|
+
import { ensureWikiIndexPopulated } from "./wiki/index-manager.js";
|
|
24
27
|
const log = logger.child({ module: "daemon" });
|
|
25
28
|
const modeContext = new ModeContext(config);
|
|
26
29
|
let memoryHousekeepingScheduler;
|
|
30
|
+
let wikiConsolidationScheduler;
|
|
27
31
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
28
32
|
/**
|
|
29
33
|
* How long the daemon waits for in-flight work to finish before forcing an exit.
|
|
@@ -126,6 +130,17 @@ async function main() {
|
|
|
126
130
|
const moved = enforceTopicStructure();
|
|
127
131
|
log.info({ moved }, "Topic-structure migration complete");
|
|
128
132
|
}
|
|
133
|
+
const p6Migration = await runP6Migration(getDb());
|
|
134
|
+
log.info({ p6Migration }, "P6 wiki seed migration complete");
|
|
135
|
+
try {
|
|
136
|
+
const wikiReindex = ensureWikiIndexPopulated();
|
|
137
|
+
if (wikiReindex.reindexed) {
|
|
138
|
+
log.info(wikiReindex, "Rebuilt wiki index from disk on startup");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
log.error({ err: err instanceof Error ? err.message : err }, "Startup wiki reindex check failed");
|
|
143
|
+
}
|
|
129
144
|
// Prune orphaned session folders older than 7 days
|
|
130
145
|
pruneOldSessions();
|
|
131
146
|
// One-time deprecation note for legacy Telegram users (v1 → v2)
|
|
@@ -158,6 +173,8 @@ async function main() {
|
|
|
158
173
|
await startApiServer();
|
|
159
174
|
memoryHousekeepingScheduler = new MemoryHousekeepingScheduler();
|
|
160
175
|
memoryHousekeepingScheduler.start();
|
|
176
|
+
wikiConsolidationScheduler = new WikiConsolidationScheduler();
|
|
177
|
+
wikiConsolidationScheduler.start();
|
|
161
178
|
if (modeContext.canLogToAdo() && (config.adoPat || config.teamChapterhouseUrl)) {
|
|
162
179
|
new StandupScheduler().schedule();
|
|
163
180
|
}
|
|
@@ -212,6 +229,7 @@ async function shutdown() {
|
|
|
212
229
|
// Destroy all active agent sessions
|
|
213
230
|
await shutdownAgents();
|
|
214
231
|
await memoryHousekeepingScheduler?.stop();
|
|
232
|
+
await wikiConsolidationScheduler?.stop();
|
|
215
233
|
try {
|
|
216
234
|
stopEpisodeWriter();
|
|
217
235
|
}
|
|
@@ -234,6 +252,7 @@ export async function restartDaemon() {
|
|
|
234
252
|
// Destroy all active agent sessions
|
|
235
253
|
await shutdownAgents();
|
|
236
254
|
await memoryHousekeepingScheduler?.stop();
|
|
255
|
+
await wikiConsolidationScheduler?.stop();
|
|
237
256
|
try {
|
|
238
257
|
stopEpisodeWriter();
|
|
239
258
|
}
|
package/dist/memory/decisions.js
CHANGED
|
@@ -7,6 +7,7 @@ function toDecision(row) {
|
|
|
7
7
|
title: row.title,
|
|
8
8
|
rationale: row.rationale,
|
|
9
9
|
decidedAt: row.decided_at,
|
|
10
|
+
source: row.source ?? undefined,
|
|
10
11
|
tier: row.tier,
|
|
11
12
|
supersededBy: row.superseded_by ?? undefined,
|
|
12
13
|
archivedAt: row.archived_at ?? undefined,
|
|
@@ -15,14 +16,14 @@ function toDecision(row) {
|
|
|
15
16
|
}
|
|
16
17
|
export function recordDecision(input) {
|
|
17
18
|
const result = getDb().prepare(`
|
|
18
|
-
INSERT INTO mem_decisions (scope_id, entity_id, title, rationale, decided_at, tier, created_at)
|
|
19
|
-
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
20
|
-
`).run(input.scope_id, input.entity_id ?? null, input.title, input.rationale, input.decided_at ?? new Date().toISOString().slice(0, 10), input.tier ?? "warm");
|
|
19
|
+
INSERT INTO mem_decisions (scope_id, entity_id, title, rationale, decided_at, source, tier, created_at)
|
|
20
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
21
|
+
`).run(input.scope_id, input.entity_id ?? null, input.title, input.rationale, input.decided_at ?? new Date().toISOString().slice(0, 10), input.source ?? null, input.tier ?? "warm");
|
|
21
22
|
return getDecision(Number(result.lastInsertRowid));
|
|
22
23
|
}
|
|
23
24
|
export function getDecision(id) {
|
|
24
25
|
const row = getDb().prepare(`
|
|
25
|
-
SELECT id, scope_id, entity_id, title, rationale, decided_at, tier, superseded_by, archived_at, created_at
|
|
26
|
+
SELECT id, scope_id, entity_id, title, rationale, decided_at, source, tier, superseded_by, archived_at, created_at
|
|
26
27
|
FROM mem_decisions
|
|
27
28
|
WHERE id = ?
|
|
28
29
|
`).get(id);
|
|
@@ -30,7 +31,7 @@ export function getDecision(id) {
|
|
|
30
31
|
}
|
|
31
32
|
export function listDecisions(input = {}) {
|
|
32
33
|
const rows = getDb().prepare(`
|
|
33
|
-
SELECT id, scope_id, entity_id, title, rationale, decided_at, tier, superseded_by, archived_at, created_at
|
|
34
|
+
SELECT id, scope_id, entity_id, title, rationale, decided_at, source, tier, superseded_by, archived_at, created_at
|
|
34
35
|
FROM mem_decisions
|
|
35
36
|
WHERE (? IS NULL OR scope_id = ?)
|
|
36
37
|
AND (? IS NULL OR entity_id = ?)
|
package/dist/memory/entities.js
CHANGED
|
@@ -3,6 +3,7 @@ function toEntity(row) {
|
|
|
3
3
|
return {
|
|
4
4
|
id: row.id,
|
|
5
5
|
scopeId: row.scope_id,
|
|
6
|
+
slug: row.slug ?? undefined,
|
|
6
7
|
kind: row.kind,
|
|
7
8
|
name: row.name,
|
|
8
9
|
summary: row.summary ?? undefined,
|
|
@@ -14,7 +15,7 @@ function toEntity(row) {
|
|
|
14
15
|
}
|
|
15
16
|
export function getEntity(id) {
|
|
16
17
|
const row = getDb().prepare(`
|
|
17
|
-
SELECT id, scope_id, kind, name, summary, tier, confidence, created_at, updated_at
|
|
18
|
+
SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
|
|
18
19
|
FROM mem_entities
|
|
19
20
|
WHERE id = ?
|
|
20
21
|
`).get(id);
|
|
@@ -22,32 +23,42 @@ export function getEntity(id) {
|
|
|
22
23
|
}
|
|
23
24
|
export function findEntityByName(scopeId, kind, name) {
|
|
24
25
|
const row = getDb().prepare(`
|
|
25
|
-
SELECT id, scope_id, kind, name, summary, tier, confidence, created_at, updated_at
|
|
26
|
+
SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
|
|
26
27
|
FROM mem_entities
|
|
27
28
|
WHERE scope_id = ? AND kind = ? AND name = ?
|
|
28
29
|
`).get(scopeId, kind, name);
|
|
29
30
|
return row ? toEntity(row) : undefined;
|
|
30
31
|
}
|
|
32
|
+
export function findEntityBySlug(scopeId, slug) {
|
|
33
|
+
const row = getDb().prepare(`
|
|
34
|
+
SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
|
|
35
|
+
FROM mem_entities
|
|
36
|
+
WHERE scope_id = ? AND slug = ?
|
|
37
|
+
`).get(scopeId, slug);
|
|
38
|
+
return row ? toEntity(row) : undefined;
|
|
39
|
+
}
|
|
31
40
|
export function upsertEntity(input) {
|
|
32
41
|
const db = getDb();
|
|
33
|
-
const existing =
|
|
42
|
+
const existing = input.slug
|
|
43
|
+
? findEntityBySlug(input.scope_id, input.slug) ?? findEntityByName(input.scope_id, input.kind, input.name)
|
|
44
|
+
: findEntityByName(input.scope_id, input.kind, input.name);
|
|
34
45
|
if (existing) {
|
|
35
46
|
db.prepare(`
|
|
36
47
|
UPDATE mem_entities
|
|
37
|
-
SET summary = ?, tier = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP
|
|
48
|
+
SET slug = COALESCE(?, slug), summary = ?, tier = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP
|
|
38
49
|
WHERE id = ?
|
|
39
|
-
`).run(input.summary ?? existing.summary ?? null, input.tier ?? existing.tier, input.confidence ?? existing.confidence, existing.id);
|
|
50
|
+
`).run(input.slug ?? null, input.summary ?? existing.summary ?? null, input.tier ?? existing.tier, input.confidence ?? existing.confidence, existing.id);
|
|
40
51
|
return getEntity(existing.id);
|
|
41
52
|
}
|
|
42
53
|
const result = db.prepare(`
|
|
43
|
-
INSERT INTO mem_entities (scope_id, kind, name, summary, tier, confidence, created_at, updated_at)
|
|
44
|
-
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
45
|
-
`).run(input.scope_id, input.kind, input.name, input.summary ?? null, input.tier ?? "warm", input.confidence ?? 1.0);
|
|
54
|
+
INSERT INTO mem_entities (scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
56
|
+
`).run(input.scope_id, input.slug ?? null, input.kind, input.name, input.summary ?? null, input.tier ?? "warm", input.confidence ?? 1.0);
|
|
46
57
|
return getEntity(Number(result.lastInsertRowid));
|
|
47
58
|
}
|
|
48
59
|
export function listEntities(input = {}) {
|
|
49
60
|
const rows = getDb().prepare(`
|
|
50
|
-
SELECT id, scope_id, kind, name, summary, tier, confidence, created_at, updated_at
|
|
61
|
+
SELECT id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at
|
|
51
62
|
FROM mem_entities
|
|
52
63
|
WHERE (? IS NULL OR scope_id = ?)
|
|
53
64
|
AND (? IS NULL OR kind = ?)
|
package/dist/memory/eot.js
CHANGED
|
@@ -127,6 +127,16 @@ function isNonEmptyString(value) {
|
|
|
127
127
|
function isIsoTimestamp(value) {
|
|
128
128
|
return !Number.isNaN(Date.parse(value));
|
|
129
129
|
}
|
|
130
|
+
function validateObservationPayload(payload) {
|
|
131
|
+
const observation = payload;
|
|
132
|
+
if (!isNonEmptyString(observation.content)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
...observation,
|
|
137
|
+
content: observation.content.trim(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
130
140
|
function validateActionItemPayload(payload) {
|
|
131
141
|
const actionItem = payload;
|
|
132
142
|
if (!isNonEmptyString(actionItem.title)) {
|
|
@@ -195,15 +205,20 @@ function rememberAcceptedMemory(kind, scopeSlug, payload, source, confidence, so
|
|
|
195
205
|
throw new Error(`Unknown memory scope '${scopeSlug}'.`);
|
|
196
206
|
}
|
|
197
207
|
if (kind === "observation") {
|
|
198
|
-
const observation = payload;
|
|
208
|
+
const observation = validateObservationPayload(payload);
|
|
209
|
+
if (!observation) {
|
|
210
|
+
log.warn({ scopeSlug, source, sourceAgent }, "Skipping accepted observation proposal with empty content");
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const content = observation.content;
|
|
199
214
|
recordObservation({
|
|
200
215
|
scope_id: scope.id,
|
|
201
216
|
entity_id: observation.entity_id,
|
|
202
|
-
content
|
|
217
|
+
content,
|
|
203
218
|
source: observation.source ?? source,
|
|
204
219
|
confidence,
|
|
205
220
|
});
|
|
206
|
-
return;
|
|
221
|
+
return true;
|
|
207
222
|
}
|
|
208
223
|
if (kind === "decision") {
|
|
209
224
|
const decision = payload;
|
|
@@ -213,7 +228,7 @@ function rememberAcceptedMemory(kind, scopeSlug, payload, source, confidence, so
|
|
|
213
228
|
rationale: decision.rationale ?? decision.title,
|
|
214
229
|
decided_at: decision.decided_at,
|
|
215
230
|
});
|
|
216
|
-
return;
|
|
231
|
+
return true;
|
|
217
232
|
}
|
|
218
233
|
if (kind === "action_item") {
|
|
219
234
|
const actionItem = validateActionItemPayload(payload);
|
|
@@ -233,7 +248,7 @@ function rememberAcceptedMemory(kind, scopeSlug, payload, source, confidence, so
|
|
|
233
248
|
due_at: actionItem.due_at,
|
|
234
249
|
source: sourceAgent ? `subagent_proposal:${sourceAgent}` : actionItem.source ?? source,
|
|
235
250
|
});
|
|
236
|
-
return;
|
|
251
|
+
return true;
|
|
237
252
|
}
|
|
238
253
|
const entity = payload;
|
|
239
254
|
const entityKind = entity.entity_kind ?? entity.kind;
|
|
@@ -247,6 +262,7 @@ function rememberAcceptedMemory(kind, scopeSlug, payload, source, confidence, so
|
|
|
247
262
|
summary: entity.summary,
|
|
248
263
|
confidence,
|
|
249
264
|
});
|
|
265
|
+
return true;
|
|
250
266
|
}
|
|
251
267
|
export async function runEndOfTaskMemoryHook(input) {
|
|
252
268
|
const autoAcceptEnabled = isMemoryAutoAcceptEnabled();
|
|
@@ -293,7 +309,12 @@ export async function runEndOfTaskMemoryHook(input) {
|
|
|
293
309
|
else {
|
|
294
310
|
const envelope = parseEnvelope(proposal.payload);
|
|
295
311
|
try {
|
|
296
|
-
rememberAcceptedMemory(envelope.kind, resolveAcceptedProposalScopeSlug(envelope, proposal), envelope.payload, `agent:${proposal.sourceAgent}`, envelope.confidence, proposal.sourceAgent);
|
|
312
|
+
const accepted = rememberAcceptedMemory(envelope.kind, resolveAcceptedProposalScopeSlug(envelope, proposal), envelope.payload, `agent:${proposal.sourceAgent}`, envelope.confidence, proposal.sourceAgent);
|
|
313
|
+
if (!accepted) {
|
|
314
|
+
resolveInboxItem(proposal.id, "accepted", decision.reason);
|
|
315
|
+
summary.accepted++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
297
318
|
resolveInboxItem(proposal.id, "accepted", decision.reason);
|
|
298
319
|
summary.accepted++;
|
|
299
320
|
}
|
|
@@ -321,8 +342,9 @@ export async function runEndOfTaskMemoryHook(input) {
|
|
|
321
342
|
}
|
|
322
343
|
if (autoAcceptEnabled) {
|
|
323
344
|
for (const implicitMemory of review.implicit_memories) {
|
|
324
|
-
rememberAcceptedMemory(implicitMemory.kind, implicitMemory.scope_slug, implicitMemory.payload, "agent:eot", implicitMemory.confidence)
|
|
325
|
-
|
|
345
|
+
if (rememberAcceptedMemory(implicitMemory.kind, implicitMemory.scope_slug, implicitMemory.payload, "agent:eot", implicitMemory.confidence)) {
|
|
346
|
+
summary.implicit_extracted++;
|
|
347
|
+
}
|
|
326
348
|
}
|
|
327
349
|
}
|
|
328
350
|
log.info(summary, "memory.eot.processed");
|