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
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { logger } from "../util/logger.js";
|
|
2
|
+
import { applyMigrations } from "./migrations.js";
|
|
3
|
+
import { MEMORY_SCOPE_SEEDS, seedChapterhouseWikiIndexMemory } from "./repositories/memory.js";
|
|
4
|
+
import { seedWikiPagesFromDisk } from "./repositories/wiki.js";
|
|
5
|
+
function countRows(database, table) {
|
|
6
|
+
return database.prepare(`SELECT COUNT(*) as c FROM ${table}`).get().c;
|
|
7
|
+
}
|
|
8
|
+
export function initializeSchema(database, hooks) {
|
|
9
|
+
database.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS worker_sessions (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
name TEXT UNIQUE NOT NULL,
|
|
13
|
+
copilot_session_id TEXT,
|
|
14
|
+
working_dir TEXT NOT NULL,
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
16
|
+
last_output TEXT,
|
|
17
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
18
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
19
|
+
)
|
|
20
|
+
`);
|
|
21
|
+
database.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
slug TEXT UNIQUE NOT NULL,
|
|
25
|
+
name TEXT NOT NULL,
|
|
26
|
+
model TEXT NOT NULL,
|
|
27
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
28
|
+
current_task TEXT,
|
|
29
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
31
|
+
)
|
|
32
|
+
`);
|
|
33
|
+
database.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS agent_tasks (
|
|
35
|
+
task_id TEXT PRIMARY KEY,
|
|
36
|
+
agent_slug TEXT NOT NULL,
|
|
37
|
+
description TEXT NOT NULL,
|
|
38
|
+
prompt TEXT,
|
|
39
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
40
|
+
result TEXT,
|
|
41
|
+
origin_channel TEXT,
|
|
42
|
+
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
43
|
+
completed_at DATETIME
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
database.exec(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
48
|
+
slug TEXT PRIMARY KEY,
|
|
49
|
+
cwd TEXT NOT NULL,
|
|
50
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
51
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
database.exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS max_state (
|
|
56
|
+
key TEXT PRIMARY KEY,
|
|
57
|
+
value TEXT NOT NULL
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
database.exec(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS daemon_runs (
|
|
62
|
+
run_id TEXT PRIMARY KEY,
|
|
63
|
+
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
hooks.recordCurrentDaemonRun(database);
|
|
67
|
+
database.exec(`
|
|
68
|
+
CREATE TABLE IF NOT EXISTS conversation_log (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'agent_completion')),
|
|
71
|
+
content TEXT NOT NULL,
|
|
72
|
+
source TEXT NOT NULL DEFAULT 'unknown',
|
|
73
|
+
session_key TEXT NOT NULL DEFAULT 'default',
|
|
74
|
+
turn_id TEXT,
|
|
75
|
+
agent_slug TEXT,
|
|
76
|
+
agent_display_name TEXT,
|
|
77
|
+
run_id TEXT,
|
|
78
|
+
ts DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
database.exec(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
+
category TEXT NOT NULL CHECK(category IN ('preference', 'fact', 'project', 'person', 'routine')),
|
|
85
|
+
content TEXT NOT NULL,
|
|
86
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
87
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
88
|
+
last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
89
|
+
)
|
|
90
|
+
`);
|
|
91
|
+
applyMigrations(database, 1);
|
|
92
|
+
// New persistent session table — one row per chat session (default or project)
|
|
93
|
+
database.exec(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS copilot_sessions (
|
|
95
|
+
session_key TEXT PRIMARY KEY,
|
|
96
|
+
mode TEXT NOT NULL CHECK(mode IN ('default', 'project', 'agent')),
|
|
97
|
+
project_root TEXT,
|
|
98
|
+
copilot_session_id TEXT NOT NULL,
|
|
99
|
+
model TEXT,
|
|
100
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
101
|
+
)
|
|
102
|
+
`);
|
|
103
|
+
applyMigrations(database, 4);
|
|
104
|
+
// agent_task_events: append-only per-task activity log for /workers streaming
|
|
105
|
+
database.exec(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS agent_task_events (
|
|
107
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
108
|
+
task_id TEXT NOT NULL REFERENCES agent_tasks(task_id) ON DELETE CASCADE,
|
|
109
|
+
seq INTEGER NOT NULL,
|
|
110
|
+
ts INTEGER NOT NULL,
|
|
111
|
+
kind TEXT NOT NULL CHECK(kind IN ('tool_start', 'tool_complete', 'output_delta', 'task_status')),
|
|
112
|
+
tool_name TEXT,
|
|
113
|
+
summary TEXT,
|
|
114
|
+
text TEXT,
|
|
115
|
+
status TEXT
|
|
116
|
+
)
|
|
117
|
+
`);
|
|
118
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_agent_task_events_task_id ON agent_task_events(task_id, seq)`);
|
|
119
|
+
applyMigrations(database, 6);
|
|
120
|
+
// turn_events: append-only per-turn event log for the SSE chat channel (#130).
|
|
121
|
+
// Events are written eagerly; ring buffer serves live/recent hot replay.
|
|
122
|
+
database.exec(`
|
|
123
|
+
CREATE TABLE IF NOT EXISTS turn_events (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
turn_id TEXT NOT NULL,
|
|
126
|
+
session_key TEXT NOT NULL DEFAULT 'default',
|
|
127
|
+
run_id TEXT,
|
|
128
|
+
seq INTEGER NOT NULL,
|
|
129
|
+
ts INTEGER NOT NULL,
|
|
130
|
+
event_type TEXT NOT NULL,
|
|
131
|
+
payload TEXT NOT NULL
|
|
132
|
+
)
|
|
133
|
+
`);
|
|
134
|
+
const turnEventCols = database.prepare(`PRAGMA table_info(turn_events)`).all();
|
|
135
|
+
if (!turnEventCols.some((c) => c.name === "run_id")) {
|
|
136
|
+
database.exec(`ALTER TABLE turn_events ADD COLUMN run_id TEXT`);
|
|
137
|
+
}
|
|
138
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_turn_events_turn_id ON turn_events(turn_id, seq)`);
|
|
139
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_turn_events_session_key ON turn_events(session_key, seq)`);
|
|
140
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_turn_events_session_run ON turn_events(session_key, run_id, seq)`);
|
|
141
|
+
database.exec(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS mem_scopes (
|
|
143
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
144
|
+
slug TEXT NOT NULL,
|
|
145
|
+
title TEXT NOT NULL,
|
|
146
|
+
description TEXT NOT NULL,
|
|
147
|
+
keywords TEXT NOT NULL DEFAULT '[]',
|
|
148
|
+
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)),
|
|
149
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
150
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
151
|
+
)
|
|
152
|
+
`);
|
|
153
|
+
database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mem_scopes_slug_idx ON mem_scopes(slug)`);
|
|
154
|
+
database.exec(`
|
|
155
|
+
CREATE TABLE IF NOT EXISTS mem_patterns (
|
|
156
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
158
|
+
title TEXT NOT NULL,
|
|
159
|
+
summary TEXT NOT NULL,
|
|
160
|
+
source_observation_ids TEXT NOT NULL DEFAULT '[]',
|
|
161
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
162
|
+
tier TEXT NOT NULL DEFAULT 'warm',
|
|
163
|
+
created_at TEXT NOT NULL,
|
|
164
|
+
last_updated TEXT NOT NULL
|
|
165
|
+
)
|
|
166
|
+
`);
|
|
167
|
+
database.exec(`CREATE INDEX IF NOT EXISTS mem_patterns_scope_tier_idx ON mem_patterns(scope_id, tier)`);
|
|
168
|
+
database.exec(`
|
|
169
|
+
CREATE TABLE IF NOT EXISTS mem_entities (
|
|
170
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
171
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
172
|
+
slug TEXT,
|
|
173
|
+
kind TEXT NOT NULL,
|
|
174
|
+
name TEXT NOT NULL,
|
|
175
|
+
summary TEXT,
|
|
176
|
+
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
177
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
178
|
+
tier_pinned_at DATETIME,
|
|
179
|
+
tier_reason TEXT,
|
|
180
|
+
last_recalled_at DATETIME,
|
|
181
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
182
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
183
|
+
)
|
|
184
|
+
`);
|
|
185
|
+
applyMigrations(database, 7);
|
|
186
|
+
database.exec(`
|
|
187
|
+
CREATE TABLE IF NOT EXISTS mem_observations (
|
|
188
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
189
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
190
|
+
entity_id INTEGER REFERENCES mem_entities(id),
|
|
191
|
+
content TEXT NOT NULL,
|
|
192
|
+
source TEXT NOT NULL,
|
|
193
|
+
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
194
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
195
|
+
embedding BLOB,
|
|
196
|
+
superseded_by INTEGER REFERENCES mem_observations(id) ON DELETE SET NULL,
|
|
197
|
+
archived_at DATETIME,
|
|
198
|
+
tier_pinned_at DATETIME,
|
|
199
|
+
tier_reason TEXT,
|
|
200
|
+
last_recalled_at DATETIME,
|
|
201
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
202
|
+
)
|
|
203
|
+
`);
|
|
204
|
+
const observationCols = database.prepare(`PRAGMA table_info(mem_observations)`).all();
|
|
205
|
+
if (!observationCols.some((column) => column.name === "superseded_by")) {
|
|
206
|
+
database.exec(`ALTER TABLE mem_observations ADD COLUMN superseded_by INTEGER REFERENCES mem_observations(id) ON DELETE SET NULL`);
|
|
207
|
+
}
|
|
208
|
+
if (!observationCols.some((column) => column.name === "archived_at")) {
|
|
209
|
+
database.exec(`ALTER TABLE mem_observations ADD COLUMN archived_at DATETIME`);
|
|
210
|
+
}
|
|
211
|
+
database.exec(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS mem_decisions (
|
|
213
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
214
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
215
|
+
entity_id INTEGER REFERENCES mem_entities(id),
|
|
216
|
+
title TEXT NOT NULL,
|
|
217
|
+
rationale TEXT NOT NULL,
|
|
218
|
+
decided_at TEXT NOT NULL,
|
|
219
|
+
source TEXT,
|
|
220
|
+
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
221
|
+
superseded_by INTEGER REFERENCES mem_decisions(id) ON DELETE SET NULL,
|
|
222
|
+
archived_at DATETIME,
|
|
223
|
+
tier_pinned_at DATETIME,
|
|
224
|
+
tier_reason TEXT,
|
|
225
|
+
last_recalled_at DATETIME,
|
|
226
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
227
|
+
)
|
|
228
|
+
`);
|
|
229
|
+
database.exec(`
|
|
230
|
+
CREATE TABLE IF NOT EXISTS mem_action_items (
|
|
231
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
232
|
+
scope_id INTEGER NOT NULL REFERENCES mem_scopes(id),
|
|
233
|
+
entity_id INTEGER REFERENCES mem_entities(id),
|
|
234
|
+
title TEXT NOT NULL,
|
|
235
|
+
detail TEXT,
|
|
236
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'done', 'dropped', 'snoozed')),
|
|
237
|
+
due_at TEXT,
|
|
238
|
+
snooze_until TEXT,
|
|
239
|
+
source TEXT,
|
|
240
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
241
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
242
|
+
resolved_at TEXT,
|
|
243
|
+
resolution_reason TEXT,
|
|
244
|
+
tier TEXT NOT NULL DEFAULT 'warm' CHECK(tier IN ('hot', 'warm', 'cold')),
|
|
245
|
+
tier_pinned_at TEXT,
|
|
246
|
+
tier_reason TEXT,
|
|
247
|
+
last_recalled_at TEXT
|
|
248
|
+
)
|
|
249
|
+
`);
|
|
250
|
+
database.exec(`
|
|
251
|
+
CREATE TABLE IF NOT EXISTS wiki_pages (
|
|
252
|
+
path TEXT PRIMARY KEY,
|
|
253
|
+
title TEXT NOT NULL,
|
|
254
|
+
entity_type TEXT,
|
|
255
|
+
tags TEXT DEFAULT '[]',
|
|
256
|
+
summary TEXT,
|
|
257
|
+
last_updated TEXT,
|
|
258
|
+
visibility TEXT DEFAULT 'private',
|
|
259
|
+
version INTEGER DEFAULT 1,
|
|
260
|
+
compiled_truth_hash TEXT,
|
|
261
|
+
pinned INTEGER DEFAULT 0
|
|
262
|
+
)
|
|
263
|
+
`);
|
|
264
|
+
database.exec(`
|
|
265
|
+
CREATE TABLE IF NOT EXISTS wiki_sources (
|
|
266
|
+
id TEXT PRIMARY KEY,
|
|
267
|
+
source_type TEXT NOT NULL,
|
|
268
|
+
origin TEXT NOT NULL,
|
|
269
|
+
title TEXT,
|
|
270
|
+
ingested_at TEXT NOT NULL,
|
|
271
|
+
raw_path TEXT,
|
|
272
|
+
parsed_content TEXT,
|
|
273
|
+
pages_updated TEXT DEFAULT '[]',
|
|
274
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
275
|
+
session_id TEXT,
|
|
276
|
+
session_name TEXT
|
|
277
|
+
)
|
|
278
|
+
`);
|
|
279
|
+
database.exec(`
|
|
280
|
+
CREATE TABLE IF NOT EXISTS wiki_links (
|
|
281
|
+
from_page TEXT NOT NULL,
|
|
282
|
+
to_page TEXT NOT NULL,
|
|
283
|
+
link_type TEXT NOT NULL,
|
|
284
|
+
extracted_at TEXT NOT NULL,
|
|
285
|
+
PRIMARY KEY (from_page, to_page, link_type)
|
|
286
|
+
)
|
|
287
|
+
`);
|
|
288
|
+
database.exec(`CREATE INDEX IF NOT EXISTS wiki_links_to ON wiki_links(to_page)`);
|
|
289
|
+
applyMigrations(database, 10);
|
|
290
|
+
database.exec(`
|
|
291
|
+
CREATE TABLE IF NOT EXISTS mem_inbox (
|
|
292
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
293
|
+
scope_id INTEGER REFERENCES mem_scopes(id),
|
|
294
|
+
kind TEXT NOT NULL,
|
|
295
|
+
payload TEXT NOT NULL,
|
|
296
|
+
source_agent TEXT NOT NULL,
|
|
297
|
+
source_task_id TEXT,
|
|
298
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'accepted', 'rejected', 'edited')),
|
|
299
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
300
|
+
resolved_at TEXT,
|
|
301
|
+
resolution_reason TEXT
|
|
302
|
+
)
|
|
303
|
+
`);
|
|
304
|
+
database.exec(`CREATE INDEX IF NOT EXISTS mem_inbox_status_idx ON mem_inbox(status)`);
|
|
305
|
+
database.exec(`CREATE INDEX IF NOT EXISTS mem_inbox_task_status_idx ON mem_inbox(source_task_id, status, kind)`);
|
|
306
|
+
const inboxCols = database.prepare(`PRAGMA table_info(mem_inbox)`).all();
|
|
307
|
+
if (!inboxCols.some((column) => column.name === "resolution_reason")) {
|
|
308
|
+
database.exec(`ALTER TABLE mem_inbox ADD COLUMN resolution_reason TEXT`);
|
|
309
|
+
}
|
|
310
|
+
database.exec(`
|
|
311
|
+
CREATE TABLE IF NOT EXISTS mem_settings (
|
|
312
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
313
|
+
key TEXT NOT NULL UNIQUE,
|
|
314
|
+
value TEXT NOT NULL
|
|
315
|
+
)
|
|
316
|
+
`);
|
|
317
|
+
const seedMemoryScopes = database.transaction(() => {
|
|
318
|
+
const insert = database.prepare(`
|
|
319
|
+
INSERT OR IGNORE INTO mem_scopes (slug, title, description, keywords, active, created_at, updated_at)
|
|
320
|
+
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
321
|
+
`);
|
|
322
|
+
for (const scope of MEMORY_SCOPE_SEEDS) {
|
|
323
|
+
insert.run(scope.slug, scope.title, scope.description, JSON.stringify(scope.keywords));
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
seedMemoryScopes();
|
|
327
|
+
seedChapterhouseWikiIndexMemory(database);
|
|
328
|
+
// Prune conversation log at startup — keep more history for better recovery
|
|
329
|
+
database.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 1000)`).run();
|
|
330
|
+
// Set up FTS5 for memory search (graceful fallback if not available)
|
|
331
|
+
try {
|
|
332
|
+
database.exec(`
|
|
333
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
334
|
+
content,
|
|
335
|
+
content_rowid='id'
|
|
336
|
+
)
|
|
337
|
+
`);
|
|
338
|
+
database.exec(`
|
|
339
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS mem_observations_fts USING fts5(
|
|
340
|
+
content,
|
|
341
|
+
content_rowid='id'
|
|
342
|
+
)
|
|
343
|
+
`);
|
|
344
|
+
database.exec(`
|
|
345
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS mem_decisions_fts USING fts5(
|
|
346
|
+
title,
|
|
347
|
+
rationale,
|
|
348
|
+
content_rowid='id'
|
|
349
|
+
)
|
|
350
|
+
`);
|
|
351
|
+
database.exec(`
|
|
352
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS mem_action_items_fts USING fts5(
|
|
353
|
+
title,
|
|
354
|
+
detail,
|
|
355
|
+
content_rowid='id'
|
|
356
|
+
)
|
|
357
|
+
`);
|
|
358
|
+
// Sync triggers
|
|
359
|
+
database.exec(`DROP TRIGGER IF EXISTS memories_ai`);
|
|
360
|
+
database.exec(`DROP TRIGGER IF EXISTS memories_ad`);
|
|
361
|
+
database.exec(`DROP TRIGGER IF EXISTS memories_au`);
|
|
362
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_observations_ai`);
|
|
363
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_observations_ad`);
|
|
364
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_observations_au`);
|
|
365
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_decisions_ai`);
|
|
366
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_decisions_ad`);
|
|
367
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_decisions_au`);
|
|
368
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_action_items_ai`);
|
|
369
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_action_items_ad`);
|
|
370
|
+
database.exec(`DROP TRIGGER IF EXISTS mem_action_items_au`);
|
|
371
|
+
database.exec(`
|
|
372
|
+
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
|
|
373
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
374
|
+
END
|
|
375
|
+
`);
|
|
376
|
+
database.exec(`
|
|
377
|
+
CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
|
|
378
|
+
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
379
|
+
END
|
|
380
|
+
`);
|
|
381
|
+
database.exec(`
|
|
382
|
+
CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
|
|
383
|
+
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
384
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
385
|
+
END
|
|
386
|
+
`);
|
|
387
|
+
database.exec(`
|
|
388
|
+
CREATE TRIGGER mem_observations_ai AFTER INSERT ON mem_observations BEGIN
|
|
389
|
+
INSERT INTO mem_observations_fts(rowid, content) VALUES (new.id, new.content);
|
|
390
|
+
END
|
|
391
|
+
`);
|
|
392
|
+
database.exec(`
|
|
393
|
+
CREATE TRIGGER mem_observations_ad AFTER DELETE ON mem_observations BEGIN
|
|
394
|
+
DELETE FROM mem_observations_fts WHERE rowid = old.id;
|
|
395
|
+
END
|
|
396
|
+
`);
|
|
397
|
+
database.exec(`
|
|
398
|
+
CREATE TRIGGER mem_observations_au AFTER UPDATE ON mem_observations BEGIN
|
|
399
|
+
DELETE FROM mem_observations_fts WHERE rowid = old.id;
|
|
400
|
+
INSERT INTO mem_observations_fts(rowid, content) VALUES (new.id, new.content);
|
|
401
|
+
END
|
|
402
|
+
`);
|
|
403
|
+
database.exec(`
|
|
404
|
+
CREATE TRIGGER mem_decisions_ai AFTER INSERT ON mem_decisions BEGIN
|
|
405
|
+
INSERT INTO mem_decisions_fts(rowid, title, rationale)
|
|
406
|
+
VALUES (new.id, new.title, new.rationale);
|
|
407
|
+
END
|
|
408
|
+
`);
|
|
409
|
+
database.exec(`
|
|
410
|
+
CREATE TRIGGER mem_decisions_ad AFTER DELETE ON mem_decisions BEGIN
|
|
411
|
+
DELETE FROM mem_decisions_fts WHERE rowid = old.id;
|
|
412
|
+
END
|
|
413
|
+
`);
|
|
414
|
+
database.exec(`
|
|
415
|
+
CREATE TRIGGER mem_decisions_au AFTER UPDATE ON mem_decisions BEGIN
|
|
416
|
+
DELETE FROM mem_decisions_fts WHERE rowid = old.id;
|
|
417
|
+
INSERT INTO mem_decisions_fts(rowid, title, rationale)
|
|
418
|
+
VALUES (new.id, new.title, new.rationale);
|
|
419
|
+
END
|
|
420
|
+
`);
|
|
421
|
+
database.exec(`
|
|
422
|
+
CREATE TRIGGER mem_action_items_ai AFTER INSERT ON mem_action_items BEGIN
|
|
423
|
+
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
424
|
+
VALUES (new.id, new.title, new.detail);
|
|
425
|
+
END
|
|
426
|
+
`);
|
|
427
|
+
database.exec(`
|
|
428
|
+
CREATE TRIGGER mem_action_items_ad AFTER DELETE ON mem_action_items BEGIN
|
|
429
|
+
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
430
|
+
END
|
|
431
|
+
`);
|
|
432
|
+
database.exec(`
|
|
433
|
+
CREATE TRIGGER mem_action_items_au AFTER UPDATE ON mem_action_items BEGIN
|
|
434
|
+
DELETE FROM mem_action_items_fts WHERE rowid = old.id;
|
|
435
|
+
INSERT INTO mem_action_items_fts(rowid, title, detail)
|
|
436
|
+
VALUES (new.id, new.title, new.detail);
|
|
437
|
+
END
|
|
438
|
+
`);
|
|
439
|
+
database.exec(`
|
|
440
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS wiki_pages_fts USING fts5(
|
|
441
|
+
path UNINDEXED,
|
|
442
|
+
title,
|
|
443
|
+
entity_type,
|
|
444
|
+
tags,
|
|
445
|
+
summary,
|
|
446
|
+
content='wiki_pages',
|
|
447
|
+
content_rowid='rowid'
|
|
448
|
+
)
|
|
449
|
+
`);
|
|
450
|
+
database.exec(`DROP TRIGGER IF EXISTS wiki_pages_ai`);
|
|
451
|
+
database.exec(`DROP TRIGGER IF EXISTS wiki_pages_ad`);
|
|
452
|
+
database.exec(`DROP TRIGGER IF EXISTS wiki_pages_au`);
|
|
453
|
+
database.exec(`
|
|
454
|
+
CREATE TRIGGER wiki_pages_ai AFTER INSERT ON wiki_pages BEGIN
|
|
455
|
+
INSERT INTO wiki_pages_fts(rowid, path, title, entity_type, tags, summary)
|
|
456
|
+
VALUES (new.rowid, new.path, new.title, new.entity_type, new.tags, new.summary);
|
|
457
|
+
END
|
|
458
|
+
`);
|
|
459
|
+
database.exec(`
|
|
460
|
+
CREATE TRIGGER wiki_pages_ad AFTER DELETE ON wiki_pages BEGIN
|
|
461
|
+
INSERT INTO wiki_pages_fts(wiki_pages_fts, rowid, path, title, entity_type, tags, summary)
|
|
462
|
+
VALUES('delete', old.rowid, old.path, old.title, old.entity_type, old.tags, old.summary);
|
|
463
|
+
END
|
|
464
|
+
`);
|
|
465
|
+
database.exec(`
|
|
466
|
+
CREATE TRIGGER wiki_pages_au AFTER UPDATE ON wiki_pages BEGIN
|
|
467
|
+
INSERT INTO wiki_pages_fts(wiki_pages_fts, rowid, path, title, entity_type, tags, summary)
|
|
468
|
+
VALUES('delete', old.rowid, old.path, old.title, old.entity_type, old.tags, old.summary);
|
|
469
|
+
INSERT INTO wiki_pages_fts(rowid, path, title, entity_type, tags, summary)
|
|
470
|
+
VALUES(new.rowid, new.path, new.title, new.entity_type, new.tags, new.summary);
|
|
471
|
+
END
|
|
472
|
+
`);
|
|
473
|
+
seedWikiPagesFromDisk(database);
|
|
474
|
+
// Backfill: check if FTS is in sync by comparing row counts
|
|
475
|
+
const rebuildTables = [];
|
|
476
|
+
if (countRows(database, "memories") > 0 && countRows(database, "memories_fts") < countRows(database, "memories")) {
|
|
477
|
+
rebuildTables.push("memories_fts");
|
|
478
|
+
}
|
|
479
|
+
if (countRows(database, "mem_observations") > 0 && countRows(database, "mem_observations_fts") < countRows(database, "mem_observations")) {
|
|
480
|
+
rebuildTables.push("mem_observations_fts");
|
|
481
|
+
}
|
|
482
|
+
if (countRows(database, "mem_decisions") > 0 && countRows(database, "mem_decisions_fts") < countRows(database, "mem_decisions")) {
|
|
483
|
+
rebuildTables.push("mem_decisions_fts");
|
|
484
|
+
}
|
|
485
|
+
if (countRows(database, "mem_action_items") > 0 && countRows(database, "mem_action_items_fts") < countRows(database, "mem_action_items")) {
|
|
486
|
+
rebuildTables.push("mem_action_items_fts");
|
|
487
|
+
}
|
|
488
|
+
if (countRows(database, "wiki_pages") > 0 && countRows(database, "wiki_pages_fts") < countRows(database, "wiki_pages")) {
|
|
489
|
+
rebuildTables.push("wiki_pages_fts");
|
|
490
|
+
}
|
|
491
|
+
hooks.setFts5Available(true);
|
|
492
|
+
hooks.queueFts5Rebuild(database, rebuildTables);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// FTS5 not available in this SQLite build — fall back to LIKE queries
|
|
496
|
+
hooks.setFts5Available(false);
|
|
497
|
+
hooks.setFts5Ready(true);
|
|
498
|
+
logger.warn("FTS5 is not available in this SQLite build — memory recall will use LIKE queries (degraded quality)");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
//# sourceMappingURL=schema.js.map
|
package/dist/util/logger.js
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
// System events (startup, sessions, agent dispatch, DB) go at info.
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
import pino from "pino";
|
|
18
|
+
import { config } from "../config.js";
|
|
18
19
|
const VALID_LEVELS = new Set(["trace", "debug", "info", "warn", "error", "fatal", "silent"]);
|
|
19
20
|
export function resolveLevel(envOverride) {
|
|
20
|
-
const raw = (envOverride ??
|
|
21
|
+
const raw = (envOverride ?? config.logLevel ?? "info").toLowerCase();
|
|
21
22
|
return VALID_LEVELS.has(raw) ? raw : "info";
|
|
22
23
|
}
|
|
23
24
|
/** Create a fresh pino logger for the given level. Useful in tests. */
|
|
@@ -26,7 +27,7 @@ export function createLogger(level) {
|
|
|
26
27
|
}
|
|
27
28
|
// In test environments (CHAPTERHOUSE_DISABLE_DOTENV=1), default to silent so
|
|
28
29
|
// test output stays clean. An explicit LOG_LEVEL env var overrides this.
|
|
29
|
-
const defaultLevel =
|
|
30
|
+
const defaultLevel = config.disableDotenv && !config.logLevel
|
|
30
31
|
? "silent"
|
|
31
32
|
: resolveLevel();
|
|
32
33
|
export const logger = pino({
|
|
@@ -9,10 +9,9 @@ import { getChapterhouseHome, resolveWikiRelativePath } from "../paths.js";
|
|
|
9
9
|
import { childLogger } from "../util/logger.js";
|
|
10
10
|
import { deletePage, listPages, readPage, writePage } from "./fs.js";
|
|
11
11
|
import { parseWikiFrontmatter, validateAndBackfillFrontmatter } from "./frontmatter.js";
|
|
12
|
-
import { rebuildWikiIndex, upsertWikiPage, removeWikiPage } from "./index-manager.js";
|
|
12
|
+
import { rebuildWikiIndex, refreshWikiPages, upsertWikiPage, removeWikiPage } from "./index-manager.js";
|
|
13
13
|
import { normalizeWikiPath } from "./path-utils.js";
|
|
14
14
|
const log = childLogger("wiki.consolidation");
|
|
15
|
-
const TRUTH_REWRITE_BUDGET = 18;
|
|
16
15
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
16
|
const STALE_SESSION_MS = 7 * ONE_DAY_MS;
|
|
18
17
|
const DEFAULT_MODEL = "claude-haiku-4.5";
|
|
@@ -33,9 +32,11 @@ export async function runConsolidationWithDeps(db, partialDeps = {}) {
|
|
|
33
32
|
staleSessionsNotified: 0,
|
|
34
33
|
llmCallsUsed: 0,
|
|
35
34
|
};
|
|
36
|
-
|
|
35
|
+
// Cursor resumes within collectRewriteCandidates' deterministic ordering.
|
|
36
|
+
const rewriteCandidates = orderCandidatesFromCursor(collectRewriteCandidates(db), getRewriteCursor(db));
|
|
37
|
+
let lastProcessedRewritePath = null;
|
|
37
38
|
for (const candidate of rewriteCandidates) {
|
|
38
|
-
if (result.llmCallsUsed >=
|
|
39
|
+
if (result.llmCallsUsed >= deps.truthRewriteBudget) {
|
|
39
40
|
break;
|
|
40
41
|
}
|
|
41
42
|
const row = db.prepare(`SELECT title FROM wiki_pages WHERE path = ?`).get(candidate.path);
|
|
@@ -51,6 +52,7 @@ export async function runConsolidationWithDeps(db, partialDeps = {}) {
|
|
|
51
52
|
result.llmCallsUsed += 1;
|
|
52
53
|
applyTruthRewrite(db, candidate.path, candidate.content, synthesized, runAt.toISOString());
|
|
53
54
|
modifiedPaths.add(candidate.path);
|
|
55
|
+
lastProcessedRewritePath = candidate.path;
|
|
54
56
|
result.truthRewrites += 1;
|
|
55
57
|
}
|
|
56
58
|
result.fragmentsMerged += mergeFragments(db, runAt.toISOString(), modifiedPaths);
|
|
@@ -58,7 +60,12 @@ export async function runConsolidationWithDeps(db, partialDeps = {}) {
|
|
|
58
60
|
result.sourcesArchived += archiveOrphanSources(db);
|
|
59
61
|
result.staleSessionsNotified += notifyStaleSessions(db, deps, runAt);
|
|
60
62
|
ensureConsolidationStateTable(db);
|
|
61
|
-
|
|
63
|
+
if (modifiedPaths.size > 0) {
|
|
64
|
+
refreshWikiPages([...modifiedPaths]);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
deps.rebuildIndex();
|
|
68
|
+
}
|
|
62
69
|
result.linksRepaired += repairOrphanLinks(db);
|
|
63
70
|
result.pagesReindexed = modifiedPaths.size;
|
|
64
71
|
db.prepare(`
|
|
@@ -66,6 +73,7 @@ export async function runConsolidationWithDeps(db, partialDeps = {}) {
|
|
|
66
73
|
VALUES (1, ?, ?)
|
|
67
74
|
ON CONFLICT(id) DO UPDATE SET last_run = excluded.last_run, pages_processed = excluded.pages_processed
|
|
68
75
|
`).run(runAt.toISOString(), modifiedPaths.size);
|
|
76
|
+
updateRewriteCursor(db, rewriteCandidates, lastProcessedRewritePath);
|
|
69
77
|
await deps.commitWikiChanges(runAt);
|
|
70
78
|
return result;
|
|
71
79
|
}
|
|
@@ -76,6 +84,7 @@ function createDefaultDeps(db) {
|
|
|
76
84
|
rebuildIndex: rebuildWikiIndex,
|
|
77
85
|
commitWikiChanges: async (runAt) => commitWikiChanges(runAt),
|
|
78
86
|
createActionItem: ({ title, detail, source }) => createStaleSessionActionItem(db, { title, detail, source }),
|
|
87
|
+
truthRewriteBudget: config.pkbTruthRewriteBudget,
|
|
79
88
|
};
|
|
80
89
|
}
|
|
81
90
|
function ensureConsolidationSchema(db) {
|
|
@@ -96,9 +105,36 @@ function ensureConsolidationStateTable(db) {
|
|
|
96
105
|
CREATE TABLE IF NOT EXISTS wiki_consolidation_state (
|
|
97
106
|
id INTEGER PRIMARY KEY,
|
|
98
107
|
last_run TEXT,
|
|
99
|
-
pages_processed INTEGER NOT NULL DEFAULT 0
|
|
108
|
+
pages_processed INTEGER NOT NULL DEFAULT 0,
|
|
109
|
+
rewrite_cursor_path TEXT
|
|
100
110
|
)
|
|
101
111
|
`);
|
|
112
|
+
const columns = db.prepare(`PRAGMA table_info(wiki_consolidation_state)`).all();
|
|
113
|
+
if (!columns.some((column) => column.name === "rewrite_cursor_path")) {
|
|
114
|
+
db.exec(`ALTER TABLE wiki_consolidation_state ADD COLUMN rewrite_cursor_path TEXT`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function getRewriteCursor(db) {
|
|
118
|
+
const row = db.prepare(`SELECT rewrite_cursor_path FROM wiki_consolidation_state WHERE id = 1`).get();
|
|
119
|
+
return row?.rewrite_cursor_path ?? null;
|
|
120
|
+
}
|
|
121
|
+
function orderCandidatesFromCursor(candidates, cursor) {
|
|
122
|
+
if (!cursor) {
|
|
123
|
+
return candidates;
|
|
124
|
+
}
|
|
125
|
+
const index = candidates.findIndex((candidate) => candidate.path === cursor);
|
|
126
|
+
if (index < 0 || index === candidates.length - 1) {
|
|
127
|
+
return candidates;
|
|
128
|
+
}
|
|
129
|
+
return [...candidates.slice(index + 1), ...candidates.slice(0, index + 1)];
|
|
130
|
+
}
|
|
131
|
+
function updateRewriteCursor(db, candidates, lastProcessedPath) {
|
|
132
|
+
if (!lastProcessedPath) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const processedIndex = candidates.findIndex((candidate) => candidate.path === lastProcessedPath);
|
|
136
|
+
const nextCursor = processedIndex >= 0 && processedIndex < candidates.length - 1 ? lastProcessedPath : null;
|
|
137
|
+
db.prepare(`UPDATE wiki_consolidation_state SET rewrite_cursor_path = ? WHERE id = 1`).run(nextCursor);
|
|
102
138
|
}
|
|
103
139
|
function collectRewriteCandidates(db) {
|
|
104
140
|
const rows = db.prepare(`
|
|
@@ -276,9 +312,9 @@ function rewritePageReferences(db, fragmentPath, canonicalPath, updatedAt, modif
|
|
|
276
312
|
if (!content) {
|
|
277
313
|
continue;
|
|
278
314
|
}
|
|
279
|
-
let nextContent = content
|
|
315
|
+
let nextContent = replaceWikiLink(content, fragmentTitle, canonicalTitle);
|
|
280
316
|
if (fragmentSlug && canonicalSlug) {
|
|
281
|
-
nextContent = nextContent
|
|
317
|
+
nextContent = replaceWikiLink(nextContent, fragmentSlug, canonicalSlug);
|
|
282
318
|
}
|
|
283
319
|
if (nextContent === content) {
|
|
284
320
|
continue;
|
|
@@ -295,6 +331,11 @@ function rewritePageReferences(db, fragmentPath, canonicalPath, updatedAt, modif
|
|
|
295
331
|
modifiedPaths.add(pagePath);
|
|
296
332
|
}
|
|
297
333
|
}
|
|
334
|
+
function replaceWikiLink(content, from, to) {
|
|
335
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
336
|
+
const pattern = new RegExp(`\\[\\[${escaped}(\\|[^\\]]+)?\\]\\]`, "g");
|
|
337
|
+
return content.replace(pattern, (_match, alias) => alias ? `[[${to}${alias}]]` : `[[${to}]]`);
|
|
338
|
+
}
|
|
298
339
|
function repointLinks(db, canonicalPath, fragmentPath) {
|
|
299
340
|
const rows = db.prepare(`
|
|
300
341
|
SELECT from_page, to_page, link_type, extracted_at
|
|
@@ -453,7 +494,7 @@ function recordDuplicateActionItemCheck(db, scopeId, title) {
|
|
|
453
494
|
return Boolean(row);
|
|
454
495
|
}
|
|
455
496
|
async function synthesizeTruthWithCopilot(input) {
|
|
456
|
-
const token = config.copilotAuthToken
|
|
497
|
+
const token = config.copilotAuthToken;
|
|
457
498
|
if (!token) {
|
|
458
499
|
throw new Error(`Cannot consolidate '${input.pagePath}' without a Copilot auth token.`);
|
|
459
500
|
}
|