chapterhouse 0.9.2 → 0.11.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/README.md +1 -1
- package/dist/api/auth.js +11 -1
- package/dist/api/auth.test.js +29 -0
- package/dist/api/errors.js +23 -0
- package/dist/api/route-coverage.test.js +61 -21
- package/dist/api/routes/agents.js +472 -0
- package/dist/api/routes/memory.js +299 -0
- package/dist/api/routes/projects.js +170 -0
- package/dist/api/routes/sessions.js +347 -0
- package/dist/api/routes/system.js +82 -0
- package/dist/api/routes/wiki.js +455 -0
- package/dist/api/routes/wiki.test.js +49 -0
- package/dist/api/send-json.js +16 -0
- package/dist/api/send-json.test.js +18 -0
- package/dist/api/server-runtime.js +45 -3
- package/dist/api/server.js +34 -1764
- package/dist/api/server.test.js +239 -8
- package/dist/api/sse-hub.js +37 -0
- package/dist/cli.js +1 -1
- package/dist/config.js +151 -58
- package/dist/config.test.js +29 -0
- package/dist/copilot/okr-mapper.js +2 -11
- package/dist/copilot/orchestrator.js +358 -352
- package/dist/copilot/orchestrator.test.js +139 -4
- package/dist/copilot/prompt-date.js +2 -1
- package/dist/copilot/session-manager.js +25 -23
- package/dist/copilot/session-manager.test.js +35 -1
- package/dist/copilot/standup.js +2 -2
- package/dist/copilot/task-event-log.js +7 -1
- package/dist/copilot/task-event-log.test.js +13 -0
- package/dist/copilot/tools/agent.js +608 -0
- package/dist/copilot/tools/index.js +19 -0
- package/dist/copilot/tools/memory.js +678 -0
- package/dist/copilot/tools/models.js +2 -0
- package/dist/copilot/tools/okr.js +171 -0
- package/dist/copilot/tools/wiki.js +333 -0
- package/dist/copilot/tools-deps.js +4 -0
- package/dist/copilot/tools.agent.test.js +10 -8
- package/dist/copilot/tools.inventory.test.js +76 -0
- package/dist/copilot/tools.js +1 -1780
- package/dist/copilot/tools.okr.test.js +31 -0
- package/dist/copilot/tools.wiki.test.js +6 -3
- package/dist/copilot/turn-event-log.js +31 -4
- package/dist/copilot/turn-event-log.test.js +24 -2
- package/dist/copilot/workiq-installer.test.js +2 -2
- package/dist/daemon-install.js +3 -2
- package/dist/daemon.js +9 -17
- package/dist/integrations/ado-client.js +90 -9
- package/dist/integrations/ado-client.test.js +56 -0
- package/dist/integrations/team-push.js +1 -0
- package/dist/integrations/team-push.test.js +6 -0
- package/dist/integrations/teams-notify.js +1 -0
- package/dist/integrations/teams-notify.test.js +5 -0
- package/dist/memory/active-scope.test.js +0 -1
- package/dist/memory/checkpoint.js +89 -72
- package/dist/memory/checkpoint.test.js +23 -3
- package/dist/memory/eot.js +87 -85
- package/dist/memory/eot.test.js +71 -3
- package/dist/memory/hooks.js +2 -4
- package/dist/memory/housekeeping-scheduler.js +1 -1
- package/dist/memory/housekeeping-scheduler.test.js +1 -2
- package/dist/memory/housekeeping.js +100 -3
- package/dist/memory/housekeeping.test.js +33 -2
- package/dist/memory/reflect.test.js +2 -0
- package/dist/memory/scope-lock.js +26 -0
- package/dist/memory/scope-lock.test.js +118 -0
- package/dist/memory/scopes.test.js +0 -1
- package/dist/mode-context.js +58 -5
- package/dist/mode-context.test.js +68 -0
- package/dist/paths.js +1 -0
- package/dist/setup.js +3 -2
- package/dist/shared/api-schemas.js +48 -5
- package/dist/store/connection.js +96 -0
- package/dist/store/db.js +5 -1498
- package/dist/store/db.test.js +182 -1
- package/dist/store/migrations.js +460 -0
- package/dist/store/repositories/memory.js +281 -0
- package/dist/store/repositories/okr.js +3 -0
- package/dist/store/repositories/projects.js +5 -0
- package/dist/store/repositories/sessions.js +284 -0
- package/dist/store/repositories/wiki.js +60 -0
- package/dist/store/schema.js +501 -0
- package/dist/util/logger.js +3 -2
- package/dist/wiki/consolidation.js +50 -9
- package/dist/wiki/consolidation.test.js +45 -0
- package/dist/wiki/frontmatter.js +43 -13
- package/dist/wiki/frontmatter.test.js +24 -0
- package/dist/wiki/fs.js +16 -4
- package/dist/wiki/fs.test.js +84 -0
- package/dist/wiki/index-manager.js +30 -2
- package/dist/wiki/index-manager.test.js +43 -12
- package/dist/wiki/ingest.js +1 -1
- package/dist/wiki/lock.js +11 -1
- package/dist/wiki/log-manager.js +2 -7
- package/dist/wiki/migrate.js +44 -17
- package/dist/wiki/project-registry.js +10 -5
- package/dist/wiki/project-registry.test.js +14 -0
- package/dist/wiki/scheduler.js +1 -1
- package/dist/wiki/seed-team-wiki.js +2 -1
- package/dist/wiki/team-sync.js +31 -6
- package/dist/wiki/team-sync.test.js +81 -0
- package/package.json +1 -1
- package/web/dist/assets/WikiEdit-EBVoY1Pk.js +30 -0
- package/web/dist/assets/WikiEdit-EBVoY1Pk.js.map +1 -0
- package/web/dist/assets/WikiGraph-BUbbABq-.js +2 -0
- package/web/dist/assets/WikiGraph-BUbbABq-.js.map +1 -0
- package/web/dist/assets/icon-acolyte-cream.svg +10 -0
- package/web/dist/assets/icon-acolyte-dark.svg +10 -0
- package/web/dist/assets/icon-acolyte-gold.svg +10 -0
- package/web/dist/assets/icon-acolyte-ibad.svg +10 -0
- package/web/dist/assets/icon-acolyte-lit.svg +10 -0
- package/web/dist/assets/icon-acolyte-mono.svg +10 -0
- package/web/dist/assets/icon-acolyte.png +0 -0
- package/web/dist/assets/icon-acolyte.svg +10 -0
- package/web/dist/assets/index-BGLL9pgM.css +10 -0
- package/web/dist/assets/index-KFX8UmOb.js +250 -0
- package/web/dist/assets/index-KFX8UmOb.js.map +1 -0
- package/web/dist/index.html +6 -4
- package/web/dist/assets/index-5kz9aRU9.css +0 -10
- package/web/dist/assets/index-iQrv3lQN.js +0 -286
- package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
package/dist/store/db.js
CHANGED
|
@@ -1,1499 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// Reset in tests via src/test/helpers/reset-singletons.ts
|
|
7
|
-
let db;
|
|
8
|
-
let logInsertCount = 0;
|
|
9
|
-
let fts5Available = false;
|
|
10
|
-
let currentDaemonRunId;
|
|
11
|
-
let daemonRunRecorded = false;
|
|
12
|
-
function hasColumn(database, table, column) {
|
|
13
|
-
return database.prepare(`PRAGMA table_info(${table})`).all().some((entry) => entry.name === column);
|
|
14
|
-
}
|
|
15
|
-
export function getCurrentRunId() {
|
|
16
|
-
currentDaemonRunId ??= randomUUID();
|
|
17
|
-
return currentDaemonRunId;
|
|
18
|
-
}
|
|
19
|
-
function recordCurrentDaemonRun(database) {
|
|
20
|
-
if (daemonRunRecorded) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
database.prepare(`INSERT OR IGNORE INTO daemon_runs (run_id) VALUES (?)`).run(getCurrentRunId());
|
|
24
|
-
daemonRunRecorded = true;
|
|
25
|
-
}
|
|
26
|
-
function memoryTierCase(tableAlias = "") {
|
|
27
|
-
const prefix = tableAlias ? `${tableAlias}.` : "";
|
|
28
|
-
return `
|
|
29
|
-
CASE
|
|
30
|
-
WHEN ${prefix}archived_at IS NOT NULL OR ${prefix}superseded_by IS NOT NULL THEN 'cold'
|
|
31
|
-
WHEN ${prefix}tier = 'glacier' THEN 'cold'
|
|
32
|
-
WHEN ${prefix}tier IN ('hot', 'warm', 'cold') THEN ${prefix}tier
|
|
33
|
-
ELSE 'warm'
|
|
34
|
-
END
|
|
35
|
-
`;
|
|
36
|
-
}
|
|
37
|
-
function entityTierCase(tableAlias = "") {
|
|
38
|
-
const prefix = tableAlias ? `${tableAlias}.` : "";
|
|
39
|
-
return `
|
|
40
|
-
CASE
|
|
41
|
-
WHEN ${prefix}tier = 'glacier' THEN 'cold'
|
|
42
|
-
WHEN ${prefix}tier IN ('hot', 'warm', 'cold') THEN ${prefix}tier
|
|
43
|
-
ELSE 'warm'
|
|
44
|
-
END
|
|
45
|
-
`;
|
|
46
|
-
}
|
|
47
|
-
function tableCreateSql(database, table) {
|
|
48
|
-
const row = database.prepare(`
|
|
49
|
-
SELECT sql
|
|
50
|
-
FROM sqlite_master
|
|
51
|
-
WHERE type = 'table' AND name = ?
|
|
52
|
-
`).get(table);
|
|
53
|
-
return row?.sql ?? "";
|
|
54
|
-
}
|
|
55
|
-
const ACTION_LOG_PAGE_RE = /^pages\/_meta\/log(?:-\d{4})?\.md$/;
|
|
56
|
-
const LEGACY_INDEX_PAGE = "pages/index.md";
|
|
57
|
-
function isIgnoredWikiIndexPage(path) {
|
|
58
|
-
return path === LEGACY_INDEX_PAGE || ACTION_LOG_PAGE_RE.test(path);
|
|
59
|
-
}
|
|
60
|
-
function wikiBasenameTitle(path) {
|
|
61
|
-
const segs = path.split("/").filter(Boolean);
|
|
62
|
-
const file = segs[segs.length - 1] || path;
|
|
63
|
-
const base = file.replace(/\.md$/, "");
|
|
64
|
-
const titleBase = base === "index" && segs.length >= 2 ? segs[segs.length - 2] : base;
|
|
65
|
-
return titleBase.split(/[-_]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
66
|
-
}
|
|
67
|
-
function summarizeWikiBody(body) {
|
|
68
|
-
for (const raw of body.split("\n")) {
|
|
69
|
-
const line = raw.trim();
|
|
70
|
-
if (!line || line.startsWith("#") || line.startsWith("<!--")) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const summary = line.replace(/^[-*]\s+/, "").replace(/_\(\d{4}-\d{2}-\d{2}\)_$/, "").trim();
|
|
74
|
-
if (summary) {
|
|
75
|
-
return summary.length > 160 ? `${summary.slice(0, 157)}…` : summary;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return "";
|
|
79
|
-
}
|
|
80
|
-
function seedWikiPagesFromDisk(database) {
|
|
81
|
-
ensureWikiStructure();
|
|
82
|
-
const pages = listPages().filter((page) => !isIgnoredWikiIndexPage(page));
|
|
83
|
-
if (pages.length === 0) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const wikiPageCount = database.prepare(`SELECT COUNT(*) AS count FROM wiki_pages`).get().count;
|
|
87
|
-
if (wikiPageCount > 0) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const upsert = database.prepare(`
|
|
91
|
-
INSERT INTO wiki_pages (path, title, entity_type, tags, summary, last_updated)
|
|
92
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
93
|
-
ON CONFLICT(path) DO UPDATE SET
|
|
94
|
-
title = excluded.title,
|
|
95
|
-
entity_type = excluded.entity_type,
|
|
96
|
-
tags = excluded.tags,
|
|
97
|
-
summary = excluded.summary,
|
|
98
|
-
last_updated = excluded.last_updated,
|
|
99
|
-
version = wiki_pages.version + 1
|
|
100
|
-
`);
|
|
101
|
-
for (const page of pages) {
|
|
102
|
-
const content = readPage(page);
|
|
103
|
-
if (!content) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const { parsed: fm, body } = parseWikiFrontmatter(content);
|
|
107
|
-
const summary = fm.summary?.trim() || summarizeWikiBody(body) || fm.title || wikiBasenameTitle(page);
|
|
108
|
-
const entityType = fm.metadata?.["entity_type"] ?? null;
|
|
109
|
-
upsert.run(page, fm.title ?? wikiBasenameTitle(page), entityType, JSON.stringify(fm.tags ?? []), summary, fm.updated ?? new Date().toISOString());
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function rebuildMemoryTierTables(database) {
|
|
113
|
-
const needsRebuild = ["mem_entities", "mem_observations", "mem_decisions"]
|
|
114
|
-
.some((table) => tableCreateSql(database, table).includes("'glacier'"));
|
|
115
|
-
if (!needsRebuild) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
database.pragma("foreign_keys = OFF");
|
|
119
|
-
try {
|
|
120
|
-
database.transaction(() => {
|
|
121
|
-
database.exec(`ALTER TABLE mem_entities RENAME TO mem_entities_legacy_tier`);
|
|
122
|
-
database.exec(`
|
|
123
|
-
CREATE TABLE mem_entities (
|
|
124
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
126
|
-
slug TEXT,
|
|
127
|
-
kind TEXT NOT NULL,
|
|
128
|
-
name TEXT NOT NULL,
|
|
129
|
-
summary TEXT,
|
|
130
|
-
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
131
|
-
confidence REAL NOT NULL DEFAULT 1.0,
|
|
132
|
-
tier_pinned_at DATETIME,
|
|
133
|
-
tier_reason TEXT,
|
|
134
|
-
last_recalled_at DATETIME,
|
|
135
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
136
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
137
|
-
)
|
|
138
|
-
`);
|
|
139
|
-
database.exec(`
|
|
140
|
-
INSERT INTO mem_entities (id, scope_id, slug, kind, name, summary, tier, confidence, created_at, updated_at)
|
|
141
|
-
SELECT id, scope_id, NULL, kind, name, summary, ${entityTierCase()}, confidence, created_at, updated_at
|
|
142
|
-
FROM mem_entities_legacy_tier
|
|
143
|
-
`);
|
|
144
|
-
database.exec(`DROP TABLE mem_entities_legacy_tier`);
|
|
145
|
-
database.exec(`ALTER TABLE mem_observations RENAME TO mem_observations_legacy_tier`);
|
|
146
|
-
database.exec(`
|
|
147
|
-
CREATE TABLE mem_observations (
|
|
148
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
149
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
150
|
-
entity_id INTEGER REFERENCES mem_entities(id),
|
|
151
|
-
content TEXT NOT NULL,
|
|
152
|
-
source TEXT NOT NULL,
|
|
153
|
-
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
154
|
-
confidence REAL NOT NULL DEFAULT 1.0,
|
|
155
|
-
embedding BLOB,
|
|
156
|
-
superseded_by INTEGER REFERENCES mem_observations(id) ON DELETE SET NULL,
|
|
157
|
-
archived_at DATETIME,
|
|
158
|
-
tier_pinned_at DATETIME,
|
|
159
|
-
tier_reason TEXT,
|
|
160
|
-
last_recalled_at DATETIME,
|
|
161
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
162
|
-
)
|
|
163
|
-
`);
|
|
164
|
-
database.exec(`
|
|
165
|
-
INSERT INTO mem_observations (
|
|
166
|
-
id, scope_id, entity_id, content, source, tier, confidence, embedding, superseded_by, archived_at, created_at
|
|
167
|
-
)
|
|
168
|
-
SELECT id, scope_id, entity_id, content, source, ${memoryTierCase()}, confidence, embedding, superseded_by, archived_at, created_at
|
|
169
|
-
FROM mem_observations_legacy_tier
|
|
170
|
-
`);
|
|
171
|
-
database.exec(`DROP TABLE mem_observations_legacy_tier`);
|
|
172
|
-
database.exec(`ALTER TABLE mem_decisions RENAME TO mem_decisions_legacy_tier`);
|
|
173
|
-
database.exec(`
|
|
174
|
-
CREATE TABLE mem_decisions (
|
|
175
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
176
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
177
|
-
entity_id INTEGER REFERENCES mem_entities(id),
|
|
178
|
-
title TEXT NOT NULL,
|
|
179
|
-
rationale TEXT NOT NULL,
|
|
180
|
-
decided_at TEXT NOT NULL,
|
|
181
|
-
source TEXT,
|
|
182
|
-
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
183
|
-
superseded_by INTEGER REFERENCES mem_decisions(id) ON DELETE SET NULL,
|
|
184
|
-
archived_at DATETIME,
|
|
185
|
-
tier_pinned_at DATETIME,
|
|
186
|
-
tier_reason TEXT,
|
|
187
|
-
last_recalled_at DATETIME,
|
|
188
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
189
|
-
)
|
|
190
|
-
`);
|
|
191
|
-
database.exec(`
|
|
192
|
-
INSERT INTO mem_decisions (
|
|
193
|
-
id, scope_id, entity_id, title, rationale, decided_at, source, tier, superseded_by, archived_at, created_at
|
|
194
|
-
)
|
|
195
|
-
SELECT id, scope_id, entity_id, title, rationale, decided_at, NULL, ${memoryTierCase()}, superseded_by, archived_at, created_at
|
|
196
|
-
FROM mem_decisions_legacy_tier
|
|
197
|
-
`);
|
|
198
|
-
database.exec(`DROP TABLE mem_decisions_legacy_tier`);
|
|
199
|
-
})();
|
|
200
|
-
}
|
|
201
|
-
finally {
|
|
202
|
-
database.pragma("foreign_keys = ON");
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function ensureMemoryTierColumns(database) {
|
|
206
|
-
for (const table of ["mem_entities", "mem_observations", "mem_decisions", "mem_action_items"]) {
|
|
207
|
-
if (!hasColumn(database, table, "tier")) {
|
|
208
|
-
database.exec(`ALTER TABLE ${table} ADD COLUMN tier TEXT DEFAULT 'warm'`);
|
|
209
|
-
}
|
|
210
|
-
if (!hasColumn(database, table, "tier_pinned_at")) {
|
|
211
|
-
database.exec(`ALTER TABLE ${table} ADD COLUMN tier_pinned_at DATETIME`);
|
|
212
|
-
}
|
|
213
|
-
if (!hasColumn(database, table, "tier_reason")) {
|
|
214
|
-
database.exec(`ALTER TABLE ${table} ADD COLUMN tier_reason TEXT`);
|
|
215
|
-
}
|
|
216
|
-
if (!hasColumn(database, table, "last_recalled_at")) {
|
|
217
|
-
database.exec(`ALTER TABLE ${table} ADD COLUMN last_recalled_at DATETIME`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
database.exec(`
|
|
221
|
-
UPDATE mem_entities
|
|
222
|
-
SET tier = CASE
|
|
223
|
-
WHEN tier = 'glacier' THEN 'cold'
|
|
224
|
-
WHEN tier IN ('hot', 'warm', 'cold') THEN tier
|
|
225
|
-
WHEN confidence > 0.7 AND datetime(updated_at) >= datetime('now', '-30 days') THEN 'hot'
|
|
226
|
-
ELSE 'warm'
|
|
227
|
-
END
|
|
228
|
-
`);
|
|
229
|
-
database.exec(`
|
|
230
|
-
UPDATE mem_observations
|
|
231
|
-
SET tier = CASE
|
|
232
|
-
WHEN archived_at IS NOT NULL OR superseded_by IS NOT NULL THEN 'cold'
|
|
233
|
-
WHEN tier = 'glacier' THEN 'cold'
|
|
234
|
-
WHEN tier IN ('hot', 'warm', 'cold') THEN tier
|
|
235
|
-
WHEN confidence > 0.7 AND datetime(created_at) >= datetime('now', '-30 days') THEN 'hot'
|
|
236
|
-
ELSE 'warm'
|
|
237
|
-
END
|
|
238
|
-
`);
|
|
239
|
-
database.exec(`
|
|
240
|
-
UPDATE mem_decisions
|
|
241
|
-
SET tier = CASE
|
|
242
|
-
WHEN archived_at IS NOT NULL OR superseded_by IS NOT NULL THEN 'cold'
|
|
243
|
-
WHEN tier = 'glacier' THEN 'cold'
|
|
244
|
-
WHEN tier IN ('hot', 'warm', 'cold') THEN tier
|
|
245
|
-
WHEN datetime(COALESCE(decided_at, created_at)) >= datetime('now', '-30 days') THEN 'hot'
|
|
246
|
-
ELSE 'warm'
|
|
247
|
-
END
|
|
248
|
-
`);
|
|
249
|
-
database.exec(`
|
|
250
|
-
UPDATE mem_action_items
|
|
251
|
-
SET tier = CASE
|
|
252
|
-
WHEN status IN ('done', 'dropped') THEN 'cold'
|
|
253
|
-
WHEN tier = 'glacier' THEN 'cold'
|
|
254
|
-
WHEN tier IN ('hot', 'warm', 'cold') THEN tier
|
|
255
|
-
WHEN status = 'open' AND due_at IS NOT NULL AND datetime(due_at) <= datetime('now', '+7 days') THEN 'hot'
|
|
256
|
-
ELSE 'warm'
|
|
257
|
-
END
|
|
258
|
-
`);
|
|
259
|
-
}
|
|
260
|
-
function ensureMemoryIndexes(database) {
|
|
261
|
-
database.exec(`CREATE INDEX IF NOT EXISTS mem_entities_scope_kind_idx ON mem_entities(scope_id, kind)`);
|
|
262
|
-
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_kind_name_idx ON mem_entities(scope_id, kind, name)`);
|
|
263
|
-
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_slug_idx ON mem_entities(scope_id, slug) WHERE slug IS NOT NULL`);
|
|
264
|
-
database.exec(`CREATE INDEX IF NOT EXISTS mem_observations_scope_idx ON mem_observations(scope_id)`);
|
|
265
|
-
database.exec(`CREATE INDEX IF NOT EXISTS mem_decisions_scope_idx ON mem_decisions(scope_id)`);
|
|
266
|
-
database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_scope_status ON mem_action_items(scope_id, status)`);
|
|
267
|
-
database.exec(`CREATE INDEX IF NOT EXISTS idx_mem_action_items_due ON mem_action_items(status, due_at)`);
|
|
268
|
-
}
|
|
269
|
-
const MEMORY_SCOPE_SEEDS = [
|
|
270
|
-
{
|
|
271
|
-
slug: "global",
|
|
272
|
-
title: "Global",
|
|
273
|
-
description: "Cross-cutting facts that apply everywhere",
|
|
274
|
-
keywords: ["everywhere", "general"],
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
slug: "chapterhouse",
|
|
278
|
-
title: "Chapterhouse",
|
|
279
|
-
description: "Chapterhouse codebase, conventions, decisions, gotchas",
|
|
280
|
-
keywords: ["chapterhouse", "this repo", "this project", "the daemon"],
|
|
281
|
-
},
|
|
282
|
-
];
|
|
283
|
-
const CHAPTERHOUSE_WIKI_INDEX_SOURCE = "wiki:pages/projects/chapterhouse/index.md";
|
|
284
|
-
const CHAPTERHOUSE_WIKI_HOT_TIER_REASON = "P6 PR1 wiki migration hot-tier candidate";
|
|
285
|
-
const CHAPTERHOUSE_WIKI_DECISION_HOT_TIER_REASON = "P6 PR2 wiki migration hot-tier candidate";
|
|
286
|
-
const CHAPTERHOUSE_PROJECT_ENTITY_SEED = {
|
|
287
|
-
name: "Chapterhouse",
|
|
288
|
-
kind: "project",
|
|
289
|
-
summary: "Always-on team-level AI assistant daemon that orchestrates specialist subagents and maintains a persistent knowledge wiki.",
|
|
290
|
-
};
|
|
291
|
-
const CHAPTERHOUSE_WIKI_OBSERVATION_SEEDS = [
|
|
292
|
-
{
|
|
293
|
-
content: "Chapterhouse architecture: Node.js daemon, web UI, per-conversation sessions, specialist agents, modular skills, MCP servers, local markdown wiki, project-rules prompt injection, and background workers with SSE event streaming.",
|
|
294
|
-
tier: "warm",
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
content: "Chapterhouse source lives at bketelsen/chapterhouse with local checkout ~/projects/chapterhouse; engineering plans are in docs/plans/ and product specs are in docs/prd/.",
|
|
298
|
-
tier: "warm",
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
content: "Background agent completions arrive as automatic system notifications; after spawning a background agent, end the turn and wait instead of polling read_agent, sleeping, or re-checking list_agents.",
|
|
302
|
-
tier: "hot",
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
content: "Use the default Chapterhouse chat for research and general questions; move code changes against a specific project into that project's chat session.",
|
|
306
|
-
tier: "warm",
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
content: "Squad manages Chapterhouse's own development workflow.",
|
|
310
|
-
tier: "warm",
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
content: "Chapterhouse uses frequent semver patch releases published to npm via the Publish to npm GitHub Actions workflow on tag push; never publish manually.",
|
|
314
|
-
tier: "hot",
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
content: "Agent-memory P1 shipped on 2026-05-13 as Chapterhouse v0.4.0 across 7 PRs: #215 through #225.",
|
|
318
|
-
tier: "warm",
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
content: "The v0.3.2 per-session orchestrator was confirmed under real concurrent load on 2026-05-08, enabling concurrent sessions without cross-blocking.",
|
|
322
|
-
tier: "hot",
|
|
323
|
-
},
|
|
324
|
-
];
|
|
325
|
-
const CHAPTERHOUSE_WIKI_DECISION_SEEDS = [
|
|
326
|
-
{
|
|
327
|
-
title: "Standardize API validation and JSON errors around `src/api/errors.ts`",
|
|
328
|
-
rationale: "Keep one 400/403/404/500 contract across routes instead of ad hoc parsing and responses; `src/api/errors.ts`, `src/api/server.ts`.",
|
|
329
|
-
decidedAt: "2026-05-06",
|
|
330
|
-
tier: "warm",
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
title: "Run in standalone mode when neither Entra nor an API token is configured",
|
|
334
|
-
rationale: "Disable auth checks and team sync together so local single-user startup still works; `src/config.ts`, `src/api/auth.ts`.",
|
|
335
|
-
decidedAt: "2026-05-06",
|
|
336
|
-
tier: "warm",
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
title: "Use bearer auth for SSE and serialize wiki writes",
|
|
340
|
-
rationale: "`/stream` no longer accepts query tokens, and wiki mutations go through `withWikiWrite()`; `web/src/stream.ts`, `src/wiki/lock.ts`.",
|
|
341
|
-
decidedAt: "2026-05-06",
|
|
342
|
-
tier: "warm",
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
title: "Default production to least privilege",
|
|
346
|
-
rationale: "Run as a non-root container, validate env early, and keep CORS and security headers tight in production; `Dockerfile`, `deploy/deploy.sh`, `src/config.ts`.",
|
|
347
|
-
decidedAt: "2026-05-06",
|
|
348
|
-
tier: "warm",
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
title: "Keep chat state session-scoped by `sessionKey`",
|
|
352
|
-
rationale: "Conversation history, SSE routing, and frontend buffers stay isolated per session instead of bleeding across chats; `src/store/db.ts`, `web/src/store.ts`.",
|
|
353
|
-
decidedAt: "2026-05-07",
|
|
354
|
-
tier: "warm",
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
title: "Preserve the 3-layer daemon timing contract",
|
|
358
|
-
rationale: "Orchestrator timeout must stay below daemon shutdown grace, which must stay below systemd `TimeoutStopSec`; `README.md`, `src/daemon.ts`, `src/daemon-install.ts`.",
|
|
359
|
-
decidedAt: "2026-05-08",
|
|
360
|
-
tier: "hot",
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
title: "Start WorkIQ MCP auto-install at daemon startup",
|
|
364
|
-
rationale: "Write the MCP entry before the SDK client starts so new sessions see it immediately; issue #33, PR #78.",
|
|
365
|
-
decidedAt: "2026-05-08",
|
|
366
|
-
tier: "warm",
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
title: "Use per-session orchestrators instead of a global queue",
|
|
370
|
-
rationale: "`SessionManager` and `SessionRegistry` keep independent queues so one chat cannot block another; issue #74, `src/copilot/session-manager.ts`.",
|
|
371
|
-
decidedAt: "2026-05-08",
|
|
372
|
-
tier: "hot",
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
title: "Make wiki path hierarchy the primary navigation model",
|
|
376
|
-
rationale: "`path` drives the tree and breadcrumbs, while `section` remains secondary metadata; `web/src/wiki/index.ts`, `web/src/routes/Wiki.tsx`.",
|
|
377
|
-
decidedAt: "2026-05-06",
|
|
378
|
-
tier: "warm",
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
title: "Route wiki breadcrumbs through `selected` state",
|
|
382
|
-
rationale: "Breadcrumb clicks use the same route-state flow as the sidebar so navigation stays predictable; `web/src/components/wiki/WikiBreadcrumbs.tsx`.",
|
|
383
|
-
decidedAt: "2026-05-06",
|
|
384
|
-
tier: "warm",
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
title: "Derive wiki page scope from configured team paths",
|
|
388
|
-
rationale: "The API returns `scope` and the UI shows personal/team indicators without adding new persisted metadata; `src/api/server.ts`, `web/src/components/wiki/WikiScopeIndicator.tsx`.",
|
|
389
|
-
decidedAt: "2026-05-06",
|
|
390
|
-
tier: "warm",
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
title: "Sort sidebar recents by actual project activity",
|
|
394
|
-
rationale: "`last_used_at` replaces registration time so recent projects reflect chat usage rather than signup order; issue #26, PR #30.",
|
|
395
|
-
decidedAt: "2026-05-08",
|
|
396
|
-
tier: "warm",
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
title: "Preload `CHAPTERHOUSE_DISABLE_DOTENV=1` for Node tests",
|
|
400
|
-
rationale: "Tests must not inherit developer-local env files before config singletons initialize; `src/test/setup-env.ts`, `src/config.ts`.",
|
|
401
|
-
decidedAt: "2026-05-06",
|
|
402
|
-
tier: "warm",
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
title: "Test API routes through a spawned server process",
|
|
406
|
-
rationale: "Real HTTP coverage catches auth, config preload, error middleware, and wiki I/O behavior that an unexported in-process app would miss; `src/api/server.test.ts`.",
|
|
407
|
-
decidedAt: "2026-05-06",
|
|
408
|
-
tier: "warm",
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
title: "Centralize frontend API Zod schemas in `web/src/api-schemas.ts`",
|
|
412
|
-
rationale: "Keep one audit surface for the browser-to-daemon contract and require validated JSON reads; issue #40, PR #63.",
|
|
413
|
-
decidedAt: "2026-05-08",
|
|
414
|
-
tier: "warm",
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
title: "Make frontend test isolation an infrastructure concern",
|
|
418
|
-
rationale: "Vitest globals, `test-setup.ts`, and MSW own cleanup so tests do not hand-roll it; issue #43, PR #62.",
|
|
419
|
-
decidedAt: "2026-05-08",
|
|
420
|
-
tier: "warm",
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
title: "Clean `dist/` before `npm test`",
|
|
424
|
-
rationale: "Remove stale compiled artifacts so deleted tests cannot linger in the Node test run; `package.json`.",
|
|
425
|
-
decidedAt: "2026-05-08",
|
|
426
|
-
tier: "warm",
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
title: "Adopt `pino` as the backend logger",
|
|
430
|
-
rationale: "Use `childLogger()` everywhere and keep chat content at `debug`, not `info`; issue #13, PR #28.",
|
|
431
|
-
decidedAt: "2026-05-08",
|
|
432
|
-
tier: "warm",
|
|
433
|
-
},
|
|
434
|
-
{
|
|
435
|
-
title: "Keep daemon control as a thin CLI over `src/daemon-install.ts`",
|
|
436
|
-
rationale: "Generate service artifacts in one module and support launchd and systemd user services without a separate control layer; issue #14, PR #24.",
|
|
437
|
-
decidedAt: "2026-05-08",
|
|
438
|
-
tier: "warm",
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
title: "Default publish workflows to Node 24+",
|
|
442
|
-
rationale: "npm Trusted Publishing depends on npm 11.5.1+, which Node 22 does not provide; `.github/workflows/npm-publish.yml`.",
|
|
443
|
-
decidedAt: "2026-05-08",
|
|
444
|
-
tier: "warm",
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
title: "Make `chapterhouse update` registry-aware",
|
|
448
|
-
rationale: "Registry installs update via npm, while legacy git installs keep the older pull-and-rebuild path; issue #31, PR #32.",
|
|
449
|
-
decidedAt: "2026-05-08",
|
|
450
|
-
tier: "warm",
|
|
451
|
-
},
|
|
452
|
-
{
|
|
453
|
-
title: "Keep markdownlint pragmatic rather than maximalist",
|
|
454
|
-
rationale: "Disable MD060, ignore `.github/agents/**`, and suppress MD041 only where template UX requires it; `.markdownlint-cli2.jsonc`.",
|
|
455
|
-
decidedAt: "2026-05-08",
|
|
456
|
-
tier: "warm",
|
|
457
|
-
},
|
|
458
|
-
{
|
|
459
|
-
title: "Enforce issue-closing references in feature and fix PRs",
|
|
460
|
-
rationale: "PR bodies must include `Closes #N`, `Fixes #N`, or `Resolves #N`, with a documented `no-issue` bypass; PR #60, `.github/workflows/lint-pr-closes.yml`.",
|
|
461
|
-
decidedAt: "2026-05-08",
|
|
462
|
-
tier: "warm",
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
title: "Use Conventional Commits for commits and PR titles",
|
|
466
|
-
rationale: "Keep repo history machine-readable and enforce it with commitlint, husky, and PR-title linting; `commitlint.config.js`, `.github/workflows/lint-pr-title.yml`.",
|
|
467
|
-
decidedAt: "2026-05-08",
|
|
468
|
-
tier: "warm",
|
|
469
|
-
},
|
|
470
|
-
];
|
|
471
|
-
function seedChapterhouseWikiIndexMemory(database) {
|
|
472
|
-
database.transaction(() => {
|
|
473
|
-
const chapterhouseScope = database.prepare(`
|
|
474
|
-
SELECT id
|
|
475
|
-
FROM mem_scopes
|
|
476
|
-
WHERE slug = 'chapterhouse'
|
|
477
|
-
`).get();
|
|
478
|
-
if (!chapterhouseScope) {
|
|
479
|
-
throw new Error("Cannot seed Chapterhouse wiki memory because scope 'chapterhouse' does not exist.");
|
|
480
|
-
}
|
|
481
|
-
database.prepare(`
|
|
482
|
-
INSERT INTO mem_entities (scope_id, kind, name, summary, tier, confidence, created_at, updated_at)
|
|
483
|
-
VALUES (?, ?, ?, ?, 'warm', 1.0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
484
|
-
ON CONFLICT(scope_id, kind, name) DO UPDATE SET
|
|
485
|
-
summary = excluded.summary,
|
|
486
|
-
updated_at = CURRENT_TIMESTAMP
|
|
487
|
-
`).run(chapterhouseScope.id, CHAPTERHOUSE_PROJECT_ENTITY_SEED.kind, CHAPTERHOUSE_PROJECT_ENTITY_SEED.name, CHAPTERHOUSE_PROJECT_ENTITY_SEED.summary);
|
|
488
|
-
const chapterhouseEntity = database.prepare(`
|
|
489
|
-
SELECT id
|
|
490
|
-
FROM mem_entities
|
|
491
|
-
WHERE scope_id = ? AND kind = ? AND name = ?
|
|
492
|
-
`).get(chapterhouseScope.id, CHAPTERHOUSE_PROJECT_ENTITY_SEED.kind, CHAPTERHOUSE_PROJECT_ENTITY_SEED.name);
|
|
493
|
-
if (!chapterhouseEntity) {
|
|
494
|
-
throw new Error("Cannot seed Chapterhouse wiki observations because the Chapterhouse entity was not created.");
|
|
495
|
-
}
|
|
496
|
-
const insertObservation = database.prepare(`
|
|
497
|
-
INSERT INTO mem_observations (
|
|
498
|
-
scope_id, entity_id, content, source, tier, confidence, tier_pinned_at, tier_reason, created_at
|
|
499
|
-
)
|
|
500
|
-
SELECT ?, ?, ?, ?, ?, 1.0,
|
|
501
|
-
CASE WHEN ? = 'hot' THEN CURRENT_TIMESTAMP ELSE NULL END,
|
|
502
|
-
CASE WHEN ? = 'hot' THEN ? ELSE NULL END,
|
|
503
|
-
CURRENT_TIMESTAMP
|
|
504
|
-
WHERE NOT EXISTS (
|
|
505
|
-
SELECT 1 FROM mem_observations WHERE scope_id = ? AND content = ?
|
|
506
|
-
)
|
|
507
|
-
`);
|
|
508
|
-
const updateObservation = database.prepare(`
|
|
509
|
-
UPDATE mem_observations
|
|
510
|
-
SET entity_id = ?,
|
|
511
|
-
source = ?,
|
|
512
|
-
tier = CASE WHEN ? = 'hot' THEN 'hot' ELSE tier END,
|
|
513
|
-
tier_pinned_at = CASE WHEN ? = 'hot' THEN COALESCE(tier_pinned_at, CURRENT_TIMESTAMP) ELSE tier_pinned_at END,
|
|
514
|
-
tier_reason = CASE WHEN ? = 'hot' THEN ? ELSE tier_reason END
|
|
515
|
-
WHERE scope_id = ? AND content = ?
|
|
516
|
-
`);
|
|
517
|
-
for (const observation of CHAPTERHOUSE_WIKI_OBSERVATION_SEEDS) {
|
|
518
|
-
insertObservation.run(chapterhouseScope.id, chapterhouseEntity.id, observation.content, CHAPTERHOUSE_WIKI_INDEX_SOURCE, observation.tier, observation.tier, observation.tier, CHAPTERHOUSE_WIKI_HOT_TIER_REASON, chapterhouseScope.id, observation.content);
|
|
519
|
-
updateObservation.run(chapterhouseEntity.id, CHAPTERHOUSE_WIKI_INDEX_SOURCE, observation.tier, observation.tier, observation.tier, CHAPTERHOUSE_WIKI_HOT_TIER_REASON, chapterhouseScope.id, observation.content);
|
|
520
|
-
}
|
|
521
|
-
const insertDecision = database.prepare(`
|
|
522
|
-
INSERT INTO mem_decisions (
|
|
523
|
-
scope_id, entity_id, title, rationale, decided_at, tier, tier_pinned_at, tier_reason, created_at
|
|
524
|
-
)
|
|
525
|
-
SELECT ?, ?, ?, ?, ?, ?,
|
|
526
|
-
CASE WHEN ? = 'hot' THEN CURRENT_TIMESTAMP ELSE NULL END,
|
|
527
|
-
CASE WHEN ? = 'hot' THEN ? ELSE NULL END,
|
|
528
|
-
CURRENT_TIMESTAMP
|
|
529
|
-
WHERE NOT EXISTS (
|
|
530
|
-
SELECT 1 FROM mem_decisions WHERE scope_id = ? AND title = ?
|
|
531
|
-
)
|
|
532
|
-
`);
|
|
533
|
-
const updateDecision = database.prepare(`
|
|
534
|
-
UPDATE mem_decisions
|
|
535
|
-
SET entity_id = ?,
|
|
536
|
-
rationale = ?,
|
|
537
|
-
decided_at = ?,
|
|
538
|
-
tier = ?,
|
|
539
|
-
tier_pinned_at = CASE WHEN ? = 'hot' THEN COALESCE(tier_pinned_at, CURRENT_TIMESTAMP) ELSE NULL END,
|
|
540
|
-
tier_reason = CASE WHEN ? = 'hot' THEN ? ELSE NULL END
|
|
541
|
-
WHERE scope_id = ? AND title = ?
|
|
542
|
-
`);
|
|
543
|
-
for (const decision of CHAPTERHOUSE_WIKI_DECISION_SEEDS) {
|
|
544
|
-
insertDecision.run(chapterhouseScope.id, chapterhouseEntity.id, decision.title, decision.rationale, decision.decidedAt, decision.tier, decision.tier, decision.tier, CHAPTERHOUSE_WIKI_DECISION_HOT_TIER_REASON, chapterhouseScope.id, decision.title);
|
|
545
|
-
updateDecision.run(chapterhouseEntity.id, decision.rationale, decision.decidedAt, decision.tier, decision.tier, decision.tier, CHAPTERHOUSE_WIKI_DECISION_HOT_TIER_REASON, chapterhouseScope.id, decision.title);
|
|
546
|
-
}
|
|
547
|
-
})();
|
|
548
|
-
}
|
|
549
|
-
export function getDb() {
|
|
550
|
-
if (!db) {
|
|
551
|
-
ensureChapterhouseHome();
|
|
552
|
-
db = new Database(getDbPath());
|
|
553
|
-
db.pragma("journal_mode = WAL");
|
|
554
|
-
db.pragma("busy_timeout = 5000");
|
|
555
|
-
db.pragma("foreign_keys = ON");
|
|
556
|
-
db.exec(`
|
|
557
|
-
CREATE TABLE IF NOT EXISTS worker_sessions (
|
|
558
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
559
|
-
name TEXT UNIQUE NOT NULL,
|
|
560
|
-
copilot_session_id TEXT,
|
|
561
|
-
working_dir TEXT NOT NULL,
|
|
562
|
-
status TEXT NOT NULL DEFAULT 'idle',
|
|
563
|
-
last_output TEXT,
|
|
564
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
565
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
566
|
-
)
|
|
567
|
-
`);
|
|
568
|
-
db.exec(`
|
|
569
|
-
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
570
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
571
|
-
slug TEXT UNIQUE NOT NULL,
|
|
572
|
-
name TEXT NOT NULL,
|
|
573
|
-
model TEXT NOT NULL,
|
|
574
|
-
status TEXT NOT NULL DEFAULT 'idle',
|
|
575
|
-
current_task TEXT,
|
|
576
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
577
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
578
|
-
)
|
|
579
|
-
`);
|
|
580
|
-
db.exec(`
|
|
581
|
-
CREATE TABLE IF NOT EXISTS agent_tasks (
|
|
582
|
-
task_id TEXT PRIMARY KEY,
|
|
583
|
-
agent_slug TEXT NOT NULL,
|
|
584
|
-
description TEXT NOT NULL,
|
|
585
|
-
prompt TEXT,
|
|
586
|
-
status TEXT NOT NULL DEFAULT 'running',
|
|
587
|
-
result TEXT,
|
|
588
|
-
origin_channel TEXT,
|
|
589
|
-
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
590
|
-
completed_at DATETIME
|
|
591
|
-
)
|
|
592
|
-
`);
|
|
593
|
-
db.exec(`
|
|
594
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
595
|
-
slug TEXT PRIMARY KEY,
|
|
596
|
-
cwd TEXT NOT NULL,
|
|
597
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
598
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
599
|
-
)
|
|
600
|
-
`);
|
|
601
|
-
db.exec(`
|
|
602
|
-
CREATE TABLE IF NOT EXISTS max_state (
|
|
603
|
-
key TEXT PRIMARY KEY,
|
|
604
|
-
value TEXT NOT NULL
|
|
605
|
-
)
|
|
606
|
-
`);
|
|
607
|
-
db.exec(`
|
|
608
|
-
CREATE TABLE IF NOT EXISTS daemon_runs (
|
|
609
|
-
run_id TEXT PRIMARY KEY,
|
|
610
|
-
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
611
|
-
)
|
|
612
|
-
`);
|
|
613
|
-
recordCurrentDaemonRun(db);
|
|
614
|
-
db.exec(`
|
|
615
|
-
CREATE TABLE IF NOT EXISTS conversation_log (
|
|
616
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
617
|
-
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'agent_completion')),
|
|
618
|
-
content TEXT NOT NULL,
|
|
619
|
-
source TEXT NOT NULL DEFAULT 'unknown',
|
|
620
|
-
session_key TEXT NOT NULL DEFAULT 'default',
|
|
621
|
-
turn_id TEXT,
|
|
622
|
-
agent_slug TEXT,
|
|
623
|
-
agent_display_name TEXT,
|
|
624
|
-
run_id TEXT,
|
|
625
|
-
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
626
|
-
)
|
|
627
|
-
`);
|
|
628
|
-
db.exec(`
|
|
629
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
630
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
631
|
-
category TEXT NOT NULL CHECK(category IN ('preference', 'fact', 'project', 'person', 'routine')),
|
|
632
|
-
content TEXT NOT NULL,
|
|
633
|
-
source TEXT NOT NULL DEFAULT 'user',
|
|
634
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
635
|
-
last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
636
|
-
)
|
|
637
|
-
`);
|
|
638
|
-
// Migrate: if the table already existed with a stricter CHECK, recreate it
|
|
639
|
-
try {
|
|
640
|
-
db.prepare(`INSERT INTO conversation_log (role, content, source) VALUES ('agent_completion', '__migration_test__', 'test')`).run();
|
|
641
|
-
db.prepare(`DELETE FROM conversation_log WHERE content = '__migration_test__'`).run();
|
|
642
|
-
}
|
|
643
|
-
catch {
|
|
644
|
-
// CHECK constraint doesn't allow current synthetic roles — recreate table preserving data
|
|
645
|
-
db.exec(`ALTER TABLE conversation_log RENAME TO conversation_log_old`);
|
|
646
|
-
const oldConvCols = db.prepare(`PRAGMA table_info(conversation_log_old)`).all();
|
|
647
|
-
const oldConvColNames = new Set(oldConvCols.map((column) => column.name));
|
|
648
|
-
const sessionKeySelect = oldConvColNames.has("session_key") ? "session_key" : "'default'";
|
|
649
|
-
const turnIdSelect = oldConvColNames.has("turn_id") ? "turn_id" : "NULL";
|
|
650
|
-
const agentSlugSelect = oldConvColNames.has("agent_slug") ? "agent_slug" : "NULL";
|
|
651
|
-
const agentDisplayNameSelect = oldConvColNames.has("agent_display_name") ? "agent_display_name" : "NULL";
|
|
652
|
-
const runIdSelect = oldConvColNames.has("run_id") ? "run_id" : "NULL";
|
|
653
|
-
db.exec(`
|
|
654
|
-
CREATE TABLE conversation_log (
|
|
655
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
656
|
-
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'agent_completion')),
|
|
657
|
-
content TEXT NOT NULL,
|
|
658
|
-
source TEXT NOT NULL DEFAULT 'unknown',
|
|
659
|
-
session_key TEXT NOT NULL DEFAULT 'default',
|
|
660
|
-
turn_id TEXT,
|
|
661
|
-
agent_slug TEXT,
|
|
662
|
-
agent_display_name TEXT,
|
|
663
|
-
run_id TEXT,
|
|
664
|
-
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
665
|
-
)
|
|
666
|
-
`);
|
|
667
|
-
db.exec(`
|
|
668
|
-
INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id, ts)
|
|
669
|
-
SELECT role, content, source, ${sessionKeySelect}, ${turnIdSelect}, ${agentSlugSelect}, ${agentDisplayNameSelect}, ${runIdSelect}, ts FROM conversation_log_old
|
|
670
|
-
`);
|
|
671
|
-
db.exec(`DROP TABLE conversation_log_old`);
|
|
672
|
-
}
|
|
673
|
-
// New persistent session table — one row per chat session (default or project)
|
|
674
|
-
db.exec(`
|
|
675
|
-
CREATE TABLE IF NOT EXISTS copilot_sessions (
|
|
676
|
-
session_key TEXT PRIMARY KEY,
|
|
677
|
-
mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
|
|
678
|
-
project_root TEXT,
|
|
679
|
-
copilot_session_id TEXT NOT NULL,
|
|
680
|
-
model TEXT,
|
|
681
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
682
|
-
)
|
|
683
|
-
`);
|
|
684
|
-
try {
|
|
685
|
-
db.prepare(`
|
|
686
|
-
INSERT INTO copilot_sessions (session_key, mode, copilot_session_id)
|
|
687
|
-
VALUES ('__mode_probe__', 'agent', '__probe__')
|
|
688
|
-
`).run();
|
|
689
|
-
db.prepare(`DELETE FROM copilot_sessions WHERE session_key = '__mode_probe__'`).run();
|
|
690
|
-
}
|
|
691
|
-
catch {
|
|
692
|
-
db.exec(`ALTER TABLE copilot_sessions RENAME TO copilot_sessions_old`);
|
|
693
|
-
db.exec(`
|
|
694
|
-
CREATE TABLE copilot_sessions (
|
|
695
|
-
session_key TEXT PRIMARY KEY,
|
|
696
|
-
mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
|
|
697
|
-
project_root TEXT,
|
|
698
|
-
copilot_session_id TEXT NOT NULL,
|
|
699
|
-
model TEXT,
|
|
700
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
701
|
-
)
|
|
702
|
-
`);
|
|
703
|
-
db.exec(`
|
|
704
|
-
INSERT INTO copilot_sessions (session_key, mode, project_root, copilot_session_id, model, updated_at)
|
|
705
|
-
SELECT session_key, mode, project_root, copilot_session_id, model, updated_at
|
|
706
|
-
FROM copilot_sessions_old
|
|
707
|
-
`);
|
|
708
|
-
db.exec(`DROP TABLE copilot_sessions_old`);
|
|
709
|
-
}
|
|
710
|
-
// Migrate: add metadata columns to conversation_log if not present
|
|
711
|
-
const convCols = db.prepare(`PRAGMA table_info(conversation_log)`).all();
|
|
712
|
-
if (!convCols.some((c) => c.name === 'session_key')) {
|
|
713
|
-
db.exec(`ALTER TABLE conversation_log ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
|
|
714
|
-
}
|
|
715
|
-
if (!convCols.some((c) => c.name === 'turn_id')) {
|
|
716
|
-
db.exec(`ALTER TABLE conversation_log ADD COLUMN turn_id TEXT`);
|
|
717
|
-
}
|
|
718
|
-
if (!convCols.some((c) => c.name === 'agent_slug')) {
|
|
719
|
-
db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_slug TEXT`);
|
|
720
|
-
}
|
|
721
|
-
if (!convCols.some((c) => c.name === 'agent_display_name')) {
|
|
722
|
-
db.exec(`ALTER TABLE conversation_log ADD COLUMN agent_display_name TEXT`);
|
|
723
|
-
}
|
|
724
|
-
if (!convCols.some((c) => c.name === "run_id")) {
|
|
725
|
-
db.exec(`ALTER TABLE conversation_log ADD COLUMN run_id TEXT`);
|
|
726
|
-
}
|
|
727
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_log_session_run ON conversation_log(session_key, run_id, id)`);
|
|
728
|
-
// Migrate: add session_key column to agent_tasks if not present
|
|
729
|
-
const taskCols = db.prepare(`PRAGMA table_info(agent_tasks)`).all();
|
|
730
|
-
if (!taskCols.some((c) => c.name === 'session_key')) {
|
|
731
|
-
db.exec(`ALTER TABLE agent_tasks ADD COLUMN session_key TEXT NOT NULL DEFAULT 'default'`);
|
|
732
|
-
}
|
|
733
|
-
// Migrate: add source column to agent_tasks ('adhoc') if not present
|
|
734
|
-
if (!taskCols.some((c) => c.name === 'source')) {
|
|
735
|
-
db.exec(`ALTER TABLE agent_tasks ADD COLUMN source TEXT NOT NULL DEFAULT 'adhoc'`);
|
|
736
|
-
}
|
|
737
|
-
if (!taskCols.some((c) => c.name === "prompt")) {
|
|
738
|
-
db.exec(`ALTER TABLE agent_tasks ADD COLUMN prompt TEXT`);
|
|
739
|
-
}
|
|
740
|
-
// agent_task_events: append-only per-task activity log for /workers streaming
|
|
741
|
-
db.exec(`
|
|
742
|
-
CREATE TABLE IF NOT EXISTS agent_task_events (
|
|
743
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
744
|
-
task_id TEXT NOT NULL REFERENCES agent_tasks(task_id) ON DELETE CASCADE,
|
|
745
|
-
seq INTEGER NOT NULL,
|
|
746
|
-
ts INTEGER NOT NULL,
|
|
747
|
-
kind TEXT NOT NULL CHECK(kind IN ('tool_start', 'tool_complete', 'output_delta', 'task_status')),
|
|
748
|
-
tool_name TEXT,
|
|
749
|
-
summary TEXT,
|
|
750
|
-
text TEXT,
|
|
751
|
-
status TEXT
|
|
752
|
-
)
|
|
753
|
-
`);
|
|
754
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_task_events_task_id ON agent_task_events(task_id, seq)`);
|
|
755
|
-
// Migrate existing agent_task_events tables that lack text/status columns
|
|
756
|
-
const taskEventCols = db.prepare(`PRAGMA table_info(agent_task_events)`).all();
|
|
757
|
-
if (!taskEventCols.some((c) => c.name === "text") || !taskEventCols.some((c) => c.name === "status")) {
|
|
758
|
-
db.exec(`ALTER TABLE agent_task_events RENAME TO agent_task_events_old`);
|
|
759
|
-
db.exec(`
|
|
760
|
-
CREATE TABLE agent_task_events (
|
|
761
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
762
|
-
task_id TEXT NOT NULL REFERENCES agent_tasks(task_id) ON DELETE CASCADE,
|
|
763
|
-
seq INTEGER NOT NULL,
|
|
764
|
-
ts INTEGER NOT NULL,
|
|
765
|
-
kind TEXT NOT NULL CHECK(kind IN ('tool_start', 'tool_complete', 'output_delta', 'task_status')),
|
|
766
|
-
tool_name TEXT,
|
|
767
|
-
summary TEXT,
|
|
768
|
-
text TEXT,
|
|
769
|
-
status TEXT
|
|
770
|
-
)
|
|
771
|
-
`);
|
|
772
|
-
db.exec(`
|
|
773
|
-
INSERT INTO agent_task_events (id, task_id, seq, ts, kind, tool_name, summary)
|
|
774
|
-
SELECT id, task_id, seq, ts, kind, tool_name, summary FROM agent_task_events_old
|
|
775
|
-
`);
|
|
776
|
-
db.exec(`DROP TABLE agent_task_events_old`);
|
|
777
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_task_events_task_id ON agent_task_events(task_id, seq)`);
|
|
778
|
-
}
|
|
779
|
-
// Migrate: add event_seq column to agent_tasks for monotonic event numbering
|
|
780
|
-
const taskColsNow = db.prepare(`PRAGMA table_info(agent_tasks)`).all();
|
|
781
|
-
if (!taskColsNow.some((c) => c.name === 'event_seq')) {
|
|
782
|
-
db.exec(`ALTER TABLE agent_tasks ADD COLUMN event_seq INTEGER NOT NULL DEFAULT 0`);
|
|
783
|
-
}
|
|
784
|
-
// turn_events: append-only per-turn event log for the SSE chat channel (#130).
|
|
785
|
-
// Events are written eagerly; ring buffer serves live/recent hot replay.
|
|
786
|
-
db.exec(`
|
|
787
|
-
CREATE TABLE IF NOT EXISTS turn_events (
|
|
788
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
789
|
-
turn_id TEXT NOT NULL,
|
|
790
|
-
session_key TEXT NOT NULL DEFAULT 'default',
|
|
791
|
-
run_id TEXT,
|
|
792
|
-
seq INTEGER NOT NULL,
|
|
793
|
-
ts INTEGER NOT NULL,
|
|
794
|
-
event_type TEXT NOT NULL,
|
|
795
|
-
payload TEXT NOT NULL
|
|
796
|
-
)
|
|
797
|
-
`);
|
|
798
|
-
const turnEventCols = db.prepare(`PRAGMA table_info(turn_events)`).all();
|
|
799
|
-
if (!turnEventCols.some((c) => c.name === "run_id")) {
|
|
800
|
-
db.exec(`ALTER TABLE turn_events ADD COLUMN run_id TEXT`);
|
|
801
|
-
}
|
|
802
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_turn_events_turn_id ON turn_events(turn_id, seq)`);
|
|
803
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_turn_events_session_key ON turn_events(session_key, seq)`);
|
|
804
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_turn_events_session_run ON turn_events(session_key, run_id, seq)`);
|
|
805
|
-
db.exec(`
|
|
806
|
-
CREATE TABLE IF NOT EXISTS mem_scopes (
|
|
807
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
808
|
-
slug TEXT NOT NULL,
|
|
809
|
-
title TEXT NOT NULL,
|
|
810
|
-
description TEXT NOT NULL,
|
|
811
|
-
keywords TEXT NOT NULL DEFAULT '[]',
|
|
812
|
-
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)),
|
|
813
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
814
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
815
|
-
)
|
|
816
|
-
`);
|
|
817
|
-
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_scopes_slug_idx ON mem_scopes(slug)`);
|
|
818
|
-
db.exec(`
|
|
819
|
-
CREATE TABLE IF NOT EXISTS mem_patterns (
|
|
820
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
821
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
822
|
-
title TEXT NOT NULL,
|
|
823
|
-
summary TEXT NOT NULL,
|
|
824
|
-
source_observation_ids TEXT NOT NULL DEFAULT '[]',
|
|
825
|
-
confidence REAL NOT NULL DEFAULT 0.5,
|
|
826
|
-
tier TEXT NOT NULL DEFAULT 'warm',
|
|
827
|
-
created_at TEXT NOT NULL,
|
|
828
|
-
last_updated TEXT NOT NULL
|
|
829
|
-
)
|
|
830
|
-
`);
|
|
831
|
-
db.exec(`CREATE INDEX IF NOT EXISTS mem_patterns_scope_tier_idx ON mem_patterns(scope_id, tier)`);
|
|
832
|
-
db.exec(`
|
|
833
|
-
CREATE TABLE IF NOT EXISTS mem_entities (
|
|
834
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
835
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
836
|
-
slug TEXT,
|
|
837
|
-
kind TEXT NOT NULL,
|
|
838
|
-
name TEXT NOT NULL,
|
|
839
|
-
summary TEXT,
|
|
840
|
-
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
841
|
-
confidence REAL NOT NULL DEFAULT 1.0,
|
|
842
|
-
tier_pinned_at DATETIME,
|
|
843
|
-
tier_reason TEXT,
|
|
844
|
-
last_recalled_at DATETIME,
|
|
845
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
846
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
847
|
-
)
|
|
848
|
-
`);
|
|
849
|
-
const entityCols = db.prepare(`PRAGMA table_info(mem_entities)`).all();
|
|
850
|
-
if (!entityCols.some((column) => column.name === "slug")) {
|
|
851
|
-
db.exec(`ALTER TABLE mem_entities ADD COLUMN slug TEXT`);
|
|
852
|
-
}
|
|
853
|
-
db.exec(`
|
|
854
|
-
DELETE FROM mem_entities
|
|
855
|
-
WHERE id NOT IN (
|
|
856
|
-
SELECT MIN(id)
|
|
857
|
-
FROM mem_entities
|
|
858
|
-
GROUP BY scope_id, kind, name
|
|
859
|
-
)
|
|
860
|
-
`);
|
|
861
|
-
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_kind_name_idx ON mem_entities(scope_id, kind, name)`);
|
|
862
|
-
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_entities_scope_slug_idx ON mem_entities(scope_id, slug) WHERE slug IS NOT NULL`);
|
|
863
|
-
db.exec(`
|
|
864
|
-
CREATE TABLE IF NOT EXISTS mem_observations (
|
|
865
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
866
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
867
|
-
entity_id INTEGER REFERENCES mem_entities(id),
|
|
868
|
-
content TEXT NOT NULL,
|
|
869
|
-
source TEXT NOT NULL,
|
|
870
|
-
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
871
|
-
confidence REAL NOT NULL DEFAULT 1.0,
|
|
872
|
-
embedding BLOB,
|
|
873
|
-
superseded_by INTEGER REFERENCES mem_observations(id) ON DELETE SET NULL,
|
|
874
|
-
archived_at DATETIME,
|
|
875
|
-
tier_pinned_at DATETIME,
|
|
876
|
-
tier_reason TEXT,
|
|
877
|
-
last_recalled_at DATETIME,
|
|
878
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
879
|
-
)
|
|
880
|
-
`);
|
|
881
|
-
const observationCols = db.prepare(`PRAGMA table_info(mem_observations)`).all();
|
|
882
|
-
if (!observationCols.some((column) => column.name === "superseded_by")) {
|
|
883
|
-
db.exec(`ALTER TABLE mem_observations ADD COLUMN superseded_by INTEGER REFERENCES mem_observations(id) ON DELETE SET NULL`);
|
|
884
|
-
}
|
|
885
|
-
if (!observationCols.some((column) => column.name === "archived_at")) {
|
|
886
|
-
db.exec(`ALTER TABLE mem_observations ADD COLUMN archived_at DATETIME`);
|
|
887
|
-
}
|
|
888
|
-
db.exec(`
|
|
889
|
-
CREATE TABLE IF NOT EXISTS mem_decisions (
|
|
890
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
891
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
892
|
-
entity_id INTEGER REFERENCES mem_entities(id),
|
|
893
|
-
title TEXT NOT NULL,
|
|
894
|
-
rationale TEXT NOT NULL,
|
|
895
|
-
decided_at TEXT NOT NULL,
|
|
896
|
-
source TEXT,
|
|
897
|
-
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
898
|
-
superseded_by INTEGER REFERENCES mem_decisions(id) ON DELETE SET NULL,
|
|
899
|
-
archived_at DATETIME,
|
|
900
|
-
tier_pinned_at DATETIME,
|
|
901
|
-
tier_reason TEXT,
|
|
902
|
-
last_recalled_at DATETIME,
|
|
903
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
904
|
-
)
|
|
905
|
-
`);
|
|
906
|
-
db.exec(`
|
|
907
|
-
CREATE TABLE IF NOT EXISTS mem_action_items (
|
|
908
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
909
|
-
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
910
|
-
entity_id INTEGER REFERENCES mem_entities(id),
|
|
911
|
-
title TEXT NOT NULL,
|
|
912
|
-
detail TEXT,
|
|
913
|
-
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'done', 'dropped', 'snoozed')),
|
|
914
|
-
due_at TEXT,
|
|
915
|
-
snooze_until TEXT,
|
|
916
|
-
source TEXT,
|
|
917
|
-
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
918
|
-
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
919
|
-
resolved_at TEXT,
|
|
920
|
-
resolution_reason TEXT,
|
|
921
|
-
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
922
|
-
tier_pinned_at TEXT,
|
|
923
|
-
tier_reason TEXT,
|
|
924
|
-
last_recalled_at TEXT
|
|
925
|
-
)
|
|
926
|
-
`);
|
|
927
|
-
db.exec(`
|
|
928
|
-
CREATE TABLE IF NOT EXISTS wiki_pages (
|
|
929
|
-
path TEXT PRIMARY KEY,
|
|
930
|
-
title TEXT NOT NULL,
|
|
931
|
-
entity_type TEXT,
|
|
932
|
-
tags TEXT DEFAULT '[]',
|
|
933
|
-
summary TEXT,
|
|
934
|
-
last_updated TEXT,
|
|
935
|
-
visibility TEXT DEFAULT 'private',
|
|
936
|
-
version INTEGER DEFAULT 1,
|
|
937
|
-
compiled_truth_hash TEXT,
|
|
938
|
-
pinned INTEGER DEFAULT 0
|
|
939
|
-
)
|
|
940
|
-
`);
|
|
941
|
-
db.exec(`
|
|
942
|
-
CREATE TABLE IF NOT EXISTS wiki_sources (
|
|
943
|
-
id TEXT PRIMARY KEY,
|
|
944
|
-
source_type TEXT NOT NULL,
|
|
945
|
-
origin TEXT NOT NULL,
|
|
946
|
-
title TEXT,
|
|
947
|
-
ingested_at TEXT NOT NULL,
|
|
948
|
-
raw_path TEXT,
|
|
949
|
-
parsed_content TEXT,
|
|
950
|
-
pages_updated TEXT DEFAULT '[]',
|
|
951
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
952
|
-
session_id TEXT,
|
|
953
|
-
session_name TEXT
|
|
954
|
-
)
|
|
955
|
-
`);
|
|
956
|
-
db.exec(`
|
|
957
|
-
CREATE TABLE IF NOT EXISTS wiki_links (
|
|
958
|
-
from_page TEXT NOT NULL,
|
|
959
|
-
to_page TEXT NOT NULL,
|
|
960
|
-
link_type TEXT NOT NULL,
|
|
961
|
-
extracted_at TEXT NOT NULL,
|
|
962
|
-
PRIMARY KEY (from_page, to_page, link_type)
|
|
963
|
-
)
|
|
964
|
-
`);
|
|
965
|
-
db.exec(`CREATE INDEX IF NOT EXISTS wiki_links_to ON wiki_links(to_page)`);
|
|
966
|
-
const wikiSourceCols = db.prepare(`PRAGMA table_info(wiki_sources)`).all();
|
|
967
|
-
if (!wikiSourceCols.some((column) => column.name === "status")) {
|
|
968
|
-
db.exec(`ALTER TABLE wiki_sources ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`);
|
|
969
|
-
}
|
|
970
|
-
if (!wikiSourceCols.some((column) => column.name === "session_id")) {
|
|
971
|
-
db.exec(`ALTER TABLE wiki_sources ADD COLUMN session_id TEXT`);
|
|
972
|
-
}
|
|
973
|
-
if (!wikiSourceCols.some((column) => column.name === "session_name")) {
|
|
974
|
-
db.exec(`ALTER TABLE wiki_sources ADD COLUMN session_name TEXT`);
|
|
975
|
-
}
|
|
976
|
-
const decisionCols = db.prepare(`PRAGMA table_info(mem_decisions)`).all();
|
|
977
|
-
if (!decisionCols.some((column) => column.name === "superseded_by")) {
|
|
978
|
-
db.exec(`ALTER TABLE mem_decisions ADD COLUMN superseded_by INTEGER REFERENCES mem_decisions(id) ON DELETE SET NULL`);
|
|
979
|
-
}
|
|
980
|
-
if (!decisionCols.some((column) => column.name === "archived_at")) {
|
|
981
|
-
db.exec(`ALTER TABLE mem_decisions ADD COLUMN archived_at DATETIME`);
|
|
982
|
-
}
|
|
983
|
-
if (!decisionCols.some((column) => column.name === "source")) {
|
|
984
|
-
db.exec(`ALTER TABLE mem_decisions ADD COLUMN source TEXT`);
|
|
985
|
-
}
|
|
986
|
-
rebuildMemoryTierTables(db);
|
|
987
|
-
ensureMemoryTierColumns(db);
|
|
988
|
-
ensureMemoryIndexes(db);
|
|
989
|
-
db.exec(`
|
|
990
|
-
CREATE TABLE IF NOT EXISTS mem_inbox (
|
|
991
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
992
|
-
scope_id INTEGER REFERENCES mem_scopes(id),
|
|
993
|
-
kind TEXT NOT NULL,
|
|
994
|
-
payload TEXT NOT NULL,
|
|
995
|
-
source_agent TEXT NOT NULL,
|
|
996
|
-
source_task_id TEXT,
|
|
997
|
-
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'accepted', 'rejected', 'edited')),
|
|
998
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
999
|
-
resolved_at TEXT,
|
|
1000
|
-
resolution_reason TEXT
|
|
1001
|
-
)
|
|
1002
|
-
`);
|
|
1003
|
-
db.exec(`CREATE INDEX IF NOT EXISTS mem_inbox_status_idx ON mem_inbox(status)`);
|
|
1004
|
-
db.exec(`CREATE INDEX IF NOT EXISTS mem_inbox_task_status_idx ON mem_inbox(source_task_id, status, kind)`);
|
|
1005
|
-
const inboxCols = db.prepare(`PRAGMA table_info(mem_inbox)`).all();
|
|
1006
|
-
if (!inboxCols.some((column) => column.name === "resolution_reason")) {
|
|
1007
|
-
db.exec(`ALTER TABLE mem_inbox ADD COLUMN resolution_reason TEXT`);
|
|
1008
|
-
}
|
|
1009
|
-
db.exec(`
|
|
1010
|
-
CREATE TABLE IF NOT EXISTS mem_settings (
|
|
1011
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1012
|
-
key TEXT NOT NULL UNIQUE,
|
|
1013
|
-
value TEXT NOT NULL
|
|
1014
|
-
)
|
|
1015
|
-
`);
|
|
1016
|
-
const seedMemoryScopes = db.transaction(() => {
|
|
1017
|
-
const insert = db.prepare(`
|
|
1018
|
-
INSERT OR IGNORE INTO mem_scopes (slug, title, description, keywords, active, created_at, updated_at)
|
|
1019
|
-
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
1020
|
-
`);
|
|
1021
|
-
for (const scope of MEMORY_SCOPE_SEEDS) {
|
|
1022
|
-
insert.run(scope.slug, scope.title, scope.description, JSON.stringify(scope.keywords));
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
1025
|
-
seedMemoryScopes();
|
|
1026
|
-
seedChapterhouseWikiIndexMemory(db);
|
|
1027
|
-
// Prune conversation log at startup — keep more history for better recovery
|
|
1028
|
-
db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 1000)`).run();
|
|
1029
|
-
// Set up FTS5 for memory search (graceful fallback if not available)
|
|
1030
|
-
try {
|
|
1031
|
-
db.exec(`
|
|
1032
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
1033
|
-
content,
|
|
1034
|
-
content_rowid='id'
|
|
1035
|
-
)
|
|
1036
|
-
`);
|
|
1037
|
-
db.exec(`
|
|
1038
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS mem_observations_fts USING fts5(
|
|
1039
|
-
content,
|
|
1040
|
-
content_rowid='id'
|
|
1041
|
-
)
|
|
1042
|
-
`);
|
|
1043
|
-
db.exec(`
|
|
1044
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS mem_decisions_fts USING fts5(
|
|
1045
|
-
title,
|
|
1046
|
-
rationale,
|
|
1047
|
-
content_rowid='id'
|
|
1048
|
-
)
|
|
1049
|
-
`);
|
|
1050
|
-
db.exec(`
|
|
1051
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS mem_action_items_fts USING fts5(
|
|
1052
|
-
title,
|
|
1053
|
-
detail,
|
|
1054
|
-
content_rowid='id'
|
|
1055
|
-
)
|
|
1056
|
-
`);
|
|
1057
|
-
// Sync triggers
|
|
1058
|
-
db.exec(`DROP TRIGGER IF EXISTS memories_ai`);
|
|
1059
|
-
db.exec(`DROP TRIGGER IF EXISTS memories_ad`);
|
|
1060
|
-
db.exec(`DROP TRIGGER IF EXISTS memories_au`);
|
|
1061
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_observations_ai`);
|
|
1062
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_observations_ad`);
|
|
1063
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_observations_au`);
|
|
1064
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ai`);
|
|
1065
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_ad`);
|
|
1066
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_decisions_au`);
|
|
1067
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ai`);
|
|
1068
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_ad`);
|
|
1069
|
-
db.exec(`DROP TRIGGER IF EXISTS mem_action_items_au`);
|
|
1070
|
-
db.exec(`
|
|
1071
|
-
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
|
|
1072
|
-
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
1073
|
-
END
|
|
1074
|
-
`);
|
|
1075
|
-
db.exec(`
|
|
1076
|
-
CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
|
|
1077
|
-
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
1078
|
-
END
|
|
1079
|
-
`);
|
|
1080
|
-
db.exec(`
|
|
1081
|
-
CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
|
|
1082
|
-
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
1083
|
-
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
1084
|
-
END
|
|
1085
|
-
`);
|
|
1086
|
-
db.exec(`
|
|
1087
|
-
CREATE TRIGGER mem_observations_ai AFTER INSERT ON mem_observations BEGIN
|
|
1088
|
-
INSERT INTO mem_observations_fts(rowid, content) VALUES (new.id, new.content);
|
|
1089
|
-
END
|
|
1090
|
-
`);
|
|
1091
|
-
db.exec(`
|
|
1092
|
-
CREATE TRIGGER mem_observations_ad AFTER DELETE ON mem_observations BEGIN
|
|
1093
|
-
DELETE FROM mem_observations_fts WHERE rowid = old.id;
|
|
1094
|
-
END
|
|
1095
|
-
`);
|
|
1096
|
-
db.exec(`
|
|
1097
|
-
CREATE TRIGGER mem_observations_au AFTER UPDATE ON mem_observations BEGIN
|
|
1098
|
-
DELETE FROM mem_observations_fts WHERE rowid = old.id;
|
|
1099
|
-
INSERT INTO mem_observations_fts(rowid, content) VALUES (new.id, new.content);
|
|
1100
|
-
END
|
|
1101
|
-
`);
|
|
1102
|
-
db.exec(`
|
|
1103
|
-
CREATE TRIGGER mem_decisions_ai AFTER INSERT ON mem_decisions BEGIN
|
|
1104
|
-
INSERT INTO mem_decisions_fts(rowid, title, rationale)
|
|
1105
|
-
VALUES (new.id, new.title, new.rationale);
|
|
1106
|
-
END
|
|
1107
|
-
`);
|
|
1108
|
-
db.exec(`
|
|
1109
|
-
CREATE TRIGGER mem_decisions_ad AFTER DELETE ON mem_decisions BEGIN
|
|
1110
|
-
DELETE FROM mem_decisions_fts WHERE rowid = old.id;
|
|
1111
|
-
END
|
|
1112
|
-
`);
|
|
1113
|
-
db.exec(`
|
|
1114
|
-
CREATE TRIGGER mem_decisions_au AFTER UPDATE ON mem_decisions BEGIN
|
|
1115
|
-
DELETE FROM mem_decisions_fts WHERE rowid = old.id;
|
|
1116
|
-
INSERT INTO mem_decisions_fts(rowid, title, rationale)
|
|
1117
|
-
VALUES (new.id, new.title, new.rationale);
|
|
1118
|
-
END
|
|
1119
|
-
`);
|
|
1120
|
-
db.exec(`
|
|
1121
|
-
CREATE TRIGGER mem_action_items_ai AFTER INSERT ON mem_action_items BEGIN
|
|
1122
|
-
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
1123
|
-
VALUES (new.id, new.title, new.detail);
|
|
1124
|
-
END
|
|
1125
|
-
`);
|
|
1126
|
-
db.exec(`
|
|
1127
|
-
CREATE TRIGGER mem_action_items_ad AFTER DELETE ON mem_action_items BEGIN
|
|
1128
|
-
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
1129
|
-
END
|
|
1130
|
-
`);
|
|
1131
|
-
db.exec(`
|
|
1132
|
-
CREATE TRIGGER mem_action_items_au AFTER UPDATE ON mem_action_items BEGIN
|
|
1133
|
-
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
1134
|
-
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
1135
|
-
VALUES (new.id, new.title, new.detail);
|
|
1136
|
-
END
|
|
1137
|
-
`);
|
|
1138
|
-
db.exec(`
|
|
1139
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS wiki_pages_fts USING fts5(
|
|
1140
|
-
path UNINDEXED,
|
|
1141
|
-
title,
|
|
1142
|
-
entity_type,
|
|
1143
|
-
tags,
|
|
1144
|
-
summary,
|
|
1145
|
-
content='wiki_pages',
|
|
1146
|
-
content_rowid='rowid'
|
|
1147
|
-
)
|
|
1148
|
-
`);
|
|
1149
|
-
db.exec(`DROP TRIGGER IF EXISTS wiki_pages_ai`);
|
|
1150
|
-
db.exec(`DROP TRIGGER IF EXISTS wiki_pages_ad`);
|
|
1151
|
-
db.exec(`DROP TRIGGER IF EXISTS wiki_pages_au`);
|
|
1152
|
-
db.exec(`
|
|
1153
|
-
CREATE TRIGGER wiki_pages_ai AFTER INSERT ON wiki_pages BEGIN
|
|
1154
|
-
INSERT INTO wiki_pages_fts(rowid, path, title, entity_type, tags, summary)
|
|
1155
|
-
VALUES (new.rowid, new.path, new.title, new.entity_type, new.tags, new.summary);
|
|
1156
|
-
END
|
|
1157
|
-
`);
|
|
1158
|
-
db.exec(`
|
|
1159
|
-
CREATE TRIGGER wiki_pages_ad AFTER DELETE ON wiki_pages BEGIN
|
|
1160
|
-
INSERT INTO wiki_pages_fts(wiki_pages_fts, rowid, path, title, entity_type, tags, summary)
|
|
1161
|
-
VALUES('delete', old.rowid, old.path, old.title, old.entity_type, old.tags, old.summary);
|
|
1162
|
-
END
|
|
1163
|
-
`);
|
|
1164
|
-
db.exec(`
|
|
1165
|
-
CREATE TRIGGER wiki_pages_au AFTER UPDATE ON wiki_pages BEGIN
|
|
1166
|
-
INSERT INTO wiki_pages_fts(wiki_pages_fts, rowid, path, title, entity_type, tags, summary)
|
|
1167
|
-
VALUES('delete', old.rowid, old.path, old.title, old.entity_type, old.tags, old.summary);
|
|
1168
|
-
INSERT INTO wiki_pages_fts(rowid, path, title, entity_type, tags, summary)
|
|
1169
|
-
VALUES(new.rowid, new.path, new.title, new.entity_type, new.tags, new.summary);
|
|
1170
|
-
END
|
|
1171
|
-
`);
|
|
1172
|
-
seedWikiPagesFromDisk(db);
|
|
1173
|
-
// Backfill: check if FTS is in sync by comparing row counts
|
|
1174
|
-
const memCount = db.prepare(`SELECT COUNT(*) as c FROM memories`).get().c;
|
|
1175
|
-
const ftsCount = db.prepare(`SELECT COUNT(*) as c FROM memories_fts`).get().c;
|
|
1176
|
-
if (memCount > 0 && ftsCount < memCount) {
|
|
1177
|
-
db.exec(`INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')`);
|
|
1178
|
-
}
|
|
1179
|
-
const observationCount = db.prepare(`SELECT COUNT(*) as c FROM mem_observations`).get().c;
|
|
1180
|
-
const observationFtsCount = db.prepare(`SELECT COUNT(*) as c FROM mem_observations_fts`).get().c;
|
|
1181
|
-
if (observationCount > 0 && observationFtsCount < observationCount) {
|
|
1182
|
-
db.exec(`INSERT INTO mem_observations_fts(mem_observations_fts) VALUES ('rebuild')`);
|
|
1183
|
-
}
|
|
1184
|
-
const decisionCount = db.prepare(`SELECT COUNT(*) as c FROM mem_decisions`).get().c;
|
|
1185
|
-
const decisionFtsCount = db.prepare(`SELECT COUNT(*) as c FROM mem_decisions_fts`).get().c;
|
|
1186
|
-
if (decisionCount > 0 && decisionFtsCount < decisionCount) {
|
|
1187
|
-
db.exec(`INSERT INTO mem_decisions_fts(mem_decisions_fts) VALUES ('rebuild')`);
|
|
1188
|
-
}
|
|
1189
|
-
const actionItemCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items`).get().c;
|
|
1190
|
-
const actionItemFtsCount = db.prepare(`SELECT COUNT(*) as c FROM mem_action_items_fts`).get().c;
|
|
1191
|
-
if (actionItemCount > 0 && actionItemFtsCount < actionItemCount) {
|
|
1192
|
-
db.exec(`INSERT INTO mem_action_items_fts(mem_action_items_fts) VALUES ('rebuild')`);
|
|
1193
|
-
}
|
|
1194
|
-
const wikiPageCount = db.prepare(`SELECT COUNT(*) as c FROM wiki_pages`).get().c;
|
|
1195
|
-
const wikiPageFtsCount = db.prepare(`SELECT COUNT(*) as c FROM wiki_pages_fts`).get().c;
|
|
1196
|
-
if (wikiPageCount > 0 && wikiPageFtsCount < wikiPageCount) {
|
|
1197
|
-
db.exec(`INSERT INTO wiki_pages_fts(wiki_pages_fts) VALUES ('rebuild')`);
|
|
1198
|
-
}
|
|
1199
|
-
fts5Available = true;
|
|
1200
|
-
}
|
|
1201
|
-
catch {
|
|
1202
|
-
// FTS5 not available in this SQLite build — fall back to LIKE queries
|
|
1203
|
-
fts5Available = false;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return db;
|
|
1207
|
-
}
|
|
1208
|
-
export function isFts5Available() {
|
|
1209
|
-
getDb();
|
|
1210
|
-
return fts5Available;
|
|
1211
|
-
}
|
|
1212
|
-
export function getState(key) {
|
|
1213
|
-
const db = getDb();
|
|
1214
|
-
const row = db.prepare(`SELECT value FROM max_state WHERE key = ?`).get(key);
|
|
1215
|
-
return row?.value;
|
|
1216
|
-
}
|
|
1217
|
-
export function setState(key, value) {
|
|
1218
|
-
const db = getDb();
|
|
1219
|
-
db.prepare(`INSERT OR REPLACE INTO max_state (key, value) VALUES (?, ?)`).run(key, value);
|
|
1220
|
-
}
|
|
1221
|
-
/** Remove a key from persistent state. */
|
|
1222
|
-
export function deleteState(key) {
|
|
1223
|
-
const db = getDb();
|
|
1224
|
-
db.prepare(`DELETE FROM max_state WHERE key = ?`).run(key);
|
|
1225
|
-
}
|
|
1226
|
-
/** Log a conversation turn (user, assistant, or system). */
|
|
1227
|
-
export function logConversation(role, content, source, sessionKey = "default", metadata) {
|
|
1228
|
-
const db = getDb();
|
|
1229
|
-
db.prepare(`INSERT INTO conversation_log (role, content, source, session_key, turn_id, agent_slug, agent_display_name, run_id)
|
|
1230
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(role, content, source, sessionKey, metadata?.turnId ?? null, metadata?.agentSlug ?? null, metadata?.agentDisplayName ?? null, getCurrentRunId());
|
|
1231
|
-
// Keep last 1000 entries to support context recovery after session loss
|
|
1232
|
-
logInsertCount++;
|
|
1233
|
-
if (logInsertCount % 50 === 0) {
|
|
1234
|
-
db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 1000)`).run();
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
/** Retrieve a stored Copilot session by key. */
|
|
1238
|
-
export function getCopilotSession(sessionKey) {
|
|
1239
|
-
const db = getDb();
|
|
1240
|
-
const row = db
|
|
1241
|
-
.prepare(`SELECT copilot_session_id, model FROM copilot_sessions WHERE session_key = ?`)
|
|
1242
|
-
.get(sessionKey);
|
|
1243
|
-
if (!row)
|
|
1244
|
-
return undefined;
|
|
1245
|
-
return { copilotSessionId: row.copilot_session_id, model: row.model ?? undefined };
|
|
1246
|
-
}
|
|
1247
|
-
/** Insert or update a Copilot session record. */
|
|
1248
|
-
export function upsertCopilotSession(sessionKey, mode, copilotSessionId, projectRoot, model) {
|
|
1249
|
-
const db = getDb();
|
|
1250
|
-
db.prepare(`INSERT INTO copilot_sessions (session_key, mode, project_root, copilot_session_id, model, updated_at)
|
|
1251
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
1252
|
-
ON CONFLICT(session_key) DO UPDATE SET
|
|
1253
|
-
copilot_session_id = excluded.copilot_session_id,
|
|
1254
|
-
model = excluded.model,
|
|
1255
|
-
project_root = excluded.project_root,
|
|
1256
|
-
updated_at = CURRENT_TIMESTAMP`).run(sessionKey, mode, projectRoot ?? null, copilotSessionId, model ?? null);
|
|
1257
|
-
}
|
|
1258
|
-
/**
|
|
1259
|
-
* Return the session_key stored against a task, or 'default' if the column
|
|
1260
|
-
* doesn't exist yet (Kaylee will add it in a follow-on phase).
|
|
1261
|
-
*/
|
|
1262
|
-
export function getTaskSessionKey(taskId) {
|
|
1263
|
-
const db = getDb();
|
|
1264
|
-
try {
|
|
1265
|
-
const row = db
|
|
1266
|
-
.prepare(`SELECT session_key FROM agent_tasks WHERE task_id = ?`)
|
|
1267
|
-
.get(taskId);
|
|
1268
|
-
return row?.session_key ?? "default";
|
|
1269
|
-
}
|
|
1270
|
-
catch {
|
|
1271
|
-
return "default";
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Return whether a batch of agent tasks has fully reached terminal completion.
|
|
1276
|
-
*
|
|
1277
|
-
* `completed` and `error` are terminal. Missing rows are treated as not-ready
|
|
1278
|
-
* so callers do not open downstream barriers (for example, a Scribe merge pass)
|
|
1279
|
-
* until every expected task has recorded a terminal state.
|
|
1280
|
-
*/
|
|
1281
|
-
export function getTaskBarrierStatus(taskIds) {
|
|
1282
|
-
const orderedTaskIds = Array.from(new Set(taskIds.filter((taskId) => taskId.trim().length > 0)));
|
|
1283
|
-
if (orderedTaskIds.length === 0) {
|
|
1284
|
-
return {
|
|
1285
|
-
ready: true,
|
|
1286
|
-
pendingTaskIds: [],
|
|
1287
|
-
missingTaskIds: [],
|
|
1288
|
-
statuses: [],
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
const db = getDb();
|
|
1292
|
-
const placeholders = orderedTaskIds.map(() => "?").join(", ");
|
|
1293
|
-
const rows = db.prepare(`SELECT task_id, status FROM agent_tasks WHERE task_id IN (${placeholders})`).all(...orderedTaskIds);
|
|
1294
|
-
const statusByTaskId = new Map(rows.map((row) => [row.task_id, row.status ?? "missing"]));
|
|
1295
|
-
const statuses = orderedTaskIds.map((taskId) => ({
|
|
1296
|
-
taskId,
|
|
1297
|
-
status: statusByTaskId.get(taskId) ?? "missing",
|
|
1298
|
-
}));
|
|
1299
|
-
const missingTaskIds = statuses.filter((entry) => entry.status === "missing").map((entry) => entry.taskId);
|
|
1300
|
-
const pendingTaskIds = statuses
|
|
1301
|
-
.filter((entry) => entry.status !== "completed" && entry.status !== "error")
|
|
1302
|
-
.map((entry) => entry.taskId);
|
|
1303
|
-
return {
|
|
1304
|
-
ready: pendingTaskIds.length === 0,
|
|
1305
|
-
pendingTaskIds,
|
|
1306
|
-
missingTaskIds,
|
|
1307
|
-
statuses,
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Poll `getTaskBarrierStatus` until all tasks are terminal or the timeout
|
|
1312
|
-
* is reached.
|
|
1313
|
-
*
|
|
1314
|
-
* On VS Code, all subagents in a single coordinator turn run concurrently,
|
|
1315
|
-
* so Scribe cannot rely on siblings being complete at the instant it starts.
|
|
1316
|
-
* This function lets Scribe (or any downstream pass) wait until every
|
|
1317
|
-
* sibling has written its `.squad/decisions/inbox/` file and reached a
|
|
1318
|
-
* terminal state before proceeding with the merge.
|
|
1319
|
-
*
|
|
1320
|
-
* Returns the last observed `TaskBarrierStatus`. Callers must check
|
|
1321
|
-
* `.ready` — a `false` result means the timeout was reached with at least
|
|
1322
|
-
* one task still pending.
|
|
1323
|
-
*/
|
|
1324
|
-
export async function pollTaskBarrierStatus(taskIds, options) {
|
|
1325
|
-
const intervalMs = options?.intervalMs ?? 500;
|
|
1326
|
-
const timeoutMs = options?.timeoutMs ?? 30_000;
|
|
1327
|
-
const deadline = Date.now() + timeoutMs;
|
|
1328
|
-
while (true) {
|
|
1329
|
-
const status = getTaskBarrierStatus(taskIds);
|
|
1330
|
-
if (status.ready)
|
|
1331
|
-
return status;
|
|
1332
|
-
if (Date.now() >= deadline)
|
|
1333
|
-
return status;
|
|
1334
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
/**
|
|
1338
|
-
* Get recent conversation history formatted for injection into system message.
|
|
1339
|
-
*
|
|
1340
|
-
* When `sessionKey` is provided, only rows belonging to that session are
|
|
1341
|
-
* returned — useful for per-session isolation that must not bleed context
|
|
1342
|
-
* from other sessions. When omitted, rows from all sessions are returned
|
|
1343
|
-
* (legacy behavior for callers that don't care about session isolation).
|
|
1344
|
-
*/
|
|
1345
|
-
export function getRecentConversation(limit, sessionKey) {
|
|
1346
|
-
const db = getDb();
|
|
1347
|
-
const effectiveLimit = limit ?? 20;
|
|
1348
|
-
const rows = sessionKey
|
|
1349
|
-
? db.prepare(`SELECT role, content, source, ts FROM conversation_log WHERE session_key = ? ORDER BY id DESC LIMIT ?`).all(sessionKey, effectiveLimit)
|
|
1350
|
-
: db.prepare(`SELECT role, content, source, ts FROM conversation_log ORDER BY id DESC LIMIT ?`).all(effectiveLimit);
|
|
1351
|
-
if (rows.length === 0)
|
|
1352
|
-
return "";
|
|
1353
|
-
// Reverse so oldest is first (chronological order)
|
|
1354
|
-
rows.reverse();
|
|
1355
|
-
return rows.map((r) => {
|
|
1356
|
-
const tag = r.role === "user" ? `[${r.source}] User`
|
|
1357
|
-
: r.role === "system" ? `[${r.source}] System`
|
|
1358
|
-
: r.role === "agent_completion" ? `[${r.source}] Agent completion`
|
|
1359
|
-
: "Chapterhouse";
|
|
1360
|
-
// Truncate long messages to keep context manageable
|
|
1361
|
-
const content = r.content.length > 1500 ? r.content.slice(0, 1500) + "…" : r.content;
|
|
1362
|
-
return `${tag}: ${content}`;
|
|
1363
|
-
}).join("\n\n");
|
|
1364
|
-
}
|
|
1365
|
-
const MAX_SESSION_MESSAGES_LIMIT = 500;
|
|
1366
|
-
const DEFAULT_SESSION_MESSAGES_LIMIT = 100;
|
|
1367
|
-
/**
|
|
1368
|
-
* Normalize a SQLite CURRENT_TIMESTAMP string to a proper ISO-8601 UTC string.
|
|
1369
|
-
*
|
|
1370
|
-
* SQLite stores CURRENT_TIMESTAMP as "YYYY-MM-DD HH:MM:SS" — no timezone
|
|
1371
|
-
* marker, space separator. Browsers that receive this bare string may parse it
|
|
1372
|
-
* as *local* time instead of UTC, shifting every displayed timestamp by the
|
|
1373
|
-
* user's UTC offset. This helper appends the `Z` suffix (and replaces the space
|
|
1374
|
-
* separator with `T`) so that `new Date(ts)` always parses as UTC.
|
|
1375
|
-
*
|
|
1376
|
-
* - Already-ISO strings (containing `T`) are returned unchanged (idempotent).
|
|
1377
|
-
* - Strings with a `T` but no `Z` are left as-is; they are already ISO format
|
|
1378
|
-
* and the caller is responsible for any timezone semantics (we do not blindly
|
|
1379
|
-
* append `Z` and risk double-shifting a value that might already be local).
|
|
1380
|
-
* - Falsy / empty input is returned as an empty string rather than throwing.
|
|
1381
|
-
*/
|
|
1382
|
-
export function normalizeSqliteTsToIso(ts) {
|
|
1383
|
-
if (!ts)
|
|
1384
|
-
return "";
|
|
1385
|
-
if (ts.includes("T"))
|
|
1386
|
-
return ts;
|
|
1387
|
-
return ts.replace(" ", "T") + "Z";
|
|
1388
|
-
}
|
|
1389
|
-
/**
|
|
1390
|
-
* Return conversation_log rows for a specific session as structured JSON,
|
|
1391
|
-
* suitable for seeding the frontend Zustand store on mount.
|
|
1392
|
-
*
|
|
1393
|
-
* Unlike `getRecentConversation()`, this returns structured objects (not a
|
|
1394
|
-
* formatted string) and omits internal system messages. Synthetic background
|
|
1395
|
-
* completion notices are included and mapped to assistant-style turns so reload
|
|
1396
|
-
* matches the live chat rendering path.
|
|
1397
|
-
*/
|
|
1398
|
-
export function getSessionMessages(sessionKey, limit, options = {}) {
|
|
1399
|
-
const db = getDb();
|
|
1400
|
-
const effectiveLimit = Math.min(limit ?? DEFAULT_SESSION_MESSAGES_LIMIT, MAX_SESSION_MESSAGES_LIMIT);
|
|
1401
|
-
const includeHistorical = options.includeHistorical ?? false;
|
|
1402
|
-
const runId = options.runId ?? getCurrentRunId();
|
|
1403
|
-
const rows = includeHistorical
|
|
1404
|
-
? db
|
|
1405
|
-
.prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
|
|
1406
|
-
WHERE session_key = ? AND role IN ('user', 'assistant', 'agent_completion')
|
|
1407
|
-
ORDER BY id DESC LIMIT ?`)
|
|
1408
|
-
.all(sessionKey, effectiveLimit)
|
|
1409
|
-
: db
|
|
1410
|
-
.prepare(`SELECT id, role, content, ts, turn_id, agent_slug, agent_display_name FROM conversation_log
|
|
1411
|
-
WHERE session_key = ? AND run_id = ? AND role IN ('user', 'assistant', 'agent_completion')
|
|
1412
|
-
ORDER BY id DESC LIMIT ?`)
|
|
1413
|
-
.all(sessionKey, runId, effectiveLimit);
|
|
1414
|
-
// Reverse so oldest is first (chronological order for the UI)
|
|
1415
|
-
rows.reverse();
|
|
1416
|
-
return rows.map((r) => {
|
|
1417
|
-
const message = {
|
|
1418
|
-
id: r.id,
|
|
1419
|
-
role: r.role === "agent_completion" ? "assistant" : r.role,
|
|
1420
|
-
content: r.content,
|
|
1421
|
-
ts: normalizeSqliteTsToIso(r.ts),
|
|
1422
|
-
turn_id: r.turn_id,
|
|
1423
|
-
};
|
|
1424
|
-
if (r.turn_id)
|
|
1425
|
-
message.turnId = r.turn_id;
|
|
1426
|
-
if (r.agent_slug)
|
|
1427
|
-
message.agentSlug = r.agent_slug;
|
|
1428
|
-
if (r.agent_display_name)
|
|
1429
|
-
message.agentDisplayName = r.agent_display_name;
|
|
1430
|
-
return message;
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
/**
|
|
1434
|
-
* Append one event to agent_task_events and return the new event.
|
|
1435
|
-
* Uses a transaction so seq is monotonically incremented.
|
|
1436
|
-
* Non-fatal: silently ignores DB errors (task may not exist yet due to race).
|
|
1437
|
-
*/
|
|
1438
|
-
export function appendTaskEvent(taskId, kind, toolName, summary, text = null, status = null) {
|
|
1439
|
-
const db = getDb();
|
|
1440
|
-
try {
|
|
1441
|
-
return db.transaction(() => {
|
|
1442
|
-
db.prepare(`UPDATE agent_tasks SET event_seq = event_seq + 1 WHERE task_id = ?`).run(taskId);
|
|
1443
|
-
const row = db.prepare(`SELECT event_seq FROM agent_tasks WHERE task_id = ?`).get(taskId);
|
|
1444
|
-
if (!row)
|
|
1445
|
-
return undefined;
|
|
1446
|
-
const seq = row.event_seq;
|
|
1447
|
-
const ts = Date.now();
|
|
1448
|
-
const info = db.prepare(`INSERT INTO agent_task_events (task_id, seq, ts, kind, tool_name, summary, text, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(taskId, seq, ts, kind, toolName, summary, text, status);
|
|
1449
|
-
return { id: Number(info.lastInsertRowid), taskId, seq, ts, kind, toolName, summary, text, status };
|
|
1450
|
-
})();
|
|
1451
|
-
}
|
|
1452
|
-
catch {
|
|
1453
|
-
return undefined;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
export function appendTaskOutputDeltaEvent(taskId, text) {
|
|
1457
|
-
return appendTaskEvent(taskId, "output_delta", null, null, text, null);
|
|
1458
|
-
}
|
|
1459
|
-
export function appendTaskStatusEvent(taskId, status, summary = null) {
|
|
1460
|
-
return appendTaskEvent(taskId, "task_status", null, summary, null, status);
|
|
1461
|
-
}
|
|
1462
|
-
export function updateTaskResult(taskId, status, result) {
|
|
1463
|
-
const db = getDb();
|
|
1464
|
-
db.prepare(`UPDATE agent_tasks SET status = ?, result = ?, completed_at = CURRENT_TIMESTAMP WHERE task_id = ?`).run(status, result ? result.slice(0, 10000) : null, taskId);
|
|
1465
|
-
}
|
|
1466
|
-
/**
|
|
1467
|
-
* Return all events for a task ordered by seq ascending.
|
|
1468
|
-
*/
|
|
1469
|
-
export function getTaskEvents(taskId, afterSeq = 0) {
|
|
1470
|
-
const db = getDb();
|
|
1471
|
-
const rows = db.prepare(`SELECT id, task_id, seq, ts, kind, tool_name, summary, text, status
|
|
1472
|
-
FROM agent_task_events WHERE task_id = ? AND seq > ? ORDER BY seq ASC`).all(taskId, afterSeq);
|
|
1473
|
-
return rows.map((r) => ({
|
|
1474
|
-
id: r.id,
|
|
1475
|
-
taskId: r.task_id,
|
|
1476
|
-
seq: r.seq,
|
|
1477
|
-
ts: r.ts,
|
|
1478
|
-
kind: r.kind,
|
|
1479
|
-
toolName: r.tool_name,
|
|
1480
|
-
summary: r.summary,
|
|
1481
|
-
text: r.text,
|
|
1482
|
-
status: r.status,
|
|
1483
|
-
}));
|
|
1484
|
-
}
|
|
1485
|
-
export function closeDb() {
|
|
1486
|
-
if (db) {
|
|
1487
|
-
db.close();
|
|
1488
|
-
db = undefined;
|
|
1489
|
-
daemonRunRecorded = false;
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
export function resetDbForTests() {
|
|
1493
|
-
closeDb();
|
|
1494
|
-
logInsertCount = 0;
|
|
1495
|
-
fts5Available = false;
|
|
1496
|
-
currentDaemonRunId = undefined;
|
|
1497
|
-
daemonRunRecorded = false;
|
|
1498
|
-
}
|
|
1
|
+
export { closeDb, getCurrentRunId, getDb, isFts5Available, isFts5Ready, resetDbForTests } from "./connection.js";
|
|
2
|
+
export { applyMigrations, DB_MIGRATIONS } from "./migrations.js";
|
|
3
|
+
export { seedWikiPagesFromDisk } from "./repositories/wiki.js";
|
|
4
|
+
export { seedChapterhouseWikiIndexMemory } from "./repositories/memory.js";
|
|
5
|
+
export { appendTaskEvent, appendTaskOutputDeltaEvent, appendTaskStatusEvent, deleteCopilotSession, deleteState, getCopilotSession, getRecentConversation, getSessionMessages, getState, getTaskBarrierStatus, getTaskEvents, getTaskSessionKey, logConversation, normalizeSqliteTsToIso, pollTaskBarrierStatus, setState, updateTaskResult, upsertCopilotSession, } from "./repositories/sessions.js";
|
|
1499
6
|
//# sourceMappingURL=db.js.map
|