pi-hermes-memory 0.7.23 → 0.8.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.
@@ -2,29 +2,73 @@
2
2
  * Session flush — gives the agent one turn to save memories before context is lost.
3
3
  * Ported from hermes-agent/run_agent.py (flush_memories).
4
4
  * See PLAN.md → "Hermes Source File Reference Map" for source lines.
5
+ *
6
+ * Default transport: in-process direct completion (same mechanism as
7
+ * background review — see review-memory-ops.ts). Falls back to a `pi -p`
8
+ * subprocess only if direct mode fails or reviewTransport forces subprocess.
5
9
  */
6
10
 
7
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
12
  import { MemoryStore } from "../store/memory-store.js";
9
- import { FLUSH_PROMPT } from "../constants.js";
13
+ import { DatabaseManager } from "../store/db.js";
14
+ import { DIRECT_FLUSH_SYSTEM_PROMPT, ENTRY_DELIMITER, FLUSH_PROMPT } from "../constants.js";
10
15
  import type { MemoryConfig } from "../types.js";
11
16
  import { collectMessageParts } from "./message-parts.js";
12
17
  import { execChildPrompt } from "./pi-child-process.js";
18
+ import { runDirectMemoryCompletion, usesDirectTransport } from "./review-memory-ops.js";
19
+
20
+ function buildDirectFlushUserPrompt(
21
+ store: MemoryStore,
22
+ projectStore: MemoryStore | null,
23
+ parts: string[],
24
+ ): string {
25
+ const sections = [
26
+ "--- Current Memory ---",
27
+ store.getMemoryEntries().join(ENTRY_DELIMITER) || "(empty)",
28
+ "",
29
+ "--- Current User Profile ---",
30
+ store.getUserEntries().join(ENTRY_DELIMITER) || "(empty)",
31
+ ];
32
+
33
+ if (projectStore) {
34
+ sections.push(
35
+ "",
36
+ "--- Current Project Memory ---",
37
+ projectStore.getMemoryEntries().join(ENTRY_DELIMITER) || "(empty)",
38
+ );
39
+ }
40
+
41
+ sections.push(
42
+ "",
43
+ "--- Conversation ---",
44
+ parts.join("\n\n"),
45
+ );
46
+
47
+ return sections.join("\n");
48
+ }
13
49
 
14
50
  export function setupSessionFlush(
15
51
  pi: ExtensionAPI,
16
52
  store: MemoryStore,
17
53
  projectStore: MemoryStore | null,
18
54
  config: MemoryConfig,
55
+ dbManager: DatabaseManager | null = null,
56
+ projectName?: string | null,
57
+ deps: { runDirectMemoryCompletion?: typeof runDirectMemoryCompletion } = {},
19
58
  ): void {
20
59
  let userTurnCount = 0;
60
+ const runDirect = deps.runDirectMemoryCompletion ?? runDirectMemoryCompletion;
21
61
 
22
62
  pi.on("message_end", async (event, _ctx) => {
23
63
  if (event.message.role === "user") userTurnCount++;
24
64
  });
25
65
 
26
- /** Shared flush logic — builds conversation snapshot and spawns pi -p */
27
- async function flush(ctx: any, signal?: AbortSignal, timeoutMs = 30000): Promise<void> {
66
+ /** Shared flush logic — builds conversation snapshot and saves memories */
67
+ async function flush(
68
+ ctx: Pick<ExtensionContext, "sessionManager" | "model" | "modelRegistry">,
69
+ signal?: AbortSignal,
70
+ timeoutMs = 30000,
71
+ ): Promise<void> {
28
72
  if (userTurnCount < config.flushMinTurns) return;
29
73
 
30
74
  let entries;
@@ -35,6 +79,29 @@ export function setupSessionFlush(
35
79
  }
36
80
 
37
81
  const parts = collectMessageParts(entries, config.flushRecentMessages);
82
+
83
+ if (usesDirectTransport(config)) {
84
+ try {
85
+ const directResult = await runDirect(
86
+ ctx,
87
+ store,
88
+ projectStore,
89
+ {
90
+ systemPrompt: DIRECT_FLUSH_SYSTEM_PROMPT,
91
+ userPrompt: buildDirectFlushUserPrompt(store, projectStore, parts),
92
+ config,
93
+ timeoutMs,
94
+ signal,
95
+ },
96
+ dbManager,
97
+ projectName,
98
+ );
99
+ if (directResult.ok) return;
100
+ } catch {
101
+ // Fall through to subprocess below.
102
+ }
103
+ }
104
+
38
105
  const flushMessage = [
39
106
  FLUSH_PROMPT,
40
107
  "",
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Markdown memory sync command — /memory-sync-markdown imports existing
3
- * Markdown-backed memories into the SQLite search store.
2
+ * Markdown memory sync command — /memory-sync-markdown reconciles the SQLite
3
+ * search mirror with authoritative Markdown memory files.
4
4
  */
5
5
 
6
6
  import fs from 'node:fs';
@@ -8,20 +8,30 @@ import path from 'node:path';
8
8
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
9
9
  import { DatabaseManager } from '../store/db.js';
10
10
  import {
11
- parseMarkdownMemoryEntry,
12
- syncMemoryEntry,
11
+ reconcileMarkdownFailureScopes,
12
+ reconcileMarkdownMemoryScope,
13
13
  } from '../store/sqlite-memory-store.js';
14
14
  import { ENTRY_DELIMITER, MEMORY_FILE, USER_FILE } from '../constants.js';
15
15
  import { AGENT_ROOT } from '../paths.js';
16
+ import {
17
+ migrateExtensionRoot,
18
+ type ExtensionRootMigrationOptions,
19
+ } from '../extension-root-migration.js';
20
+ import { withMarkdownMutationLock } from '../store/markdown-mutation-lock.js';
16
21
 
17
22
  export interface BackfillCounters {
18
23
  filesScanned: number;
19
24
  entriesScanned: number;
20
25
  imported: number;
21
26
  skipped: number;
27
+ removed: number;
22
28
  warnings: string[];
23
29
  }
24
30
 
31
+ export interface MigrationSyncOptions extends ExtensionRootMigrationOptions {
32
+ onMigrationSucceeded?: () => void;
33
+ }
34
+
25
35
  function readEntries(filePath: string): string[] {
26
36
  if (!fs.existsSync(filePath)) return [];
27
37
  const raw = fs.readFileSync(filePath, 'utf-8').trim();
@@ -29,37 +39,15 @@ function readEntries(filePath: string): string[] {
29
39
  return raw.split(ENTRY_DELIMITER).map((entry) => entry.trim()).filter(Boolean);
30
40
  }
31
41
 
32
- function importEntries(
33
- dbManager: DatabaseManager,
34
- counters: BackfillCounters,
35
- entries: string[],
36
- target: 'memory' | 'user' | 'failure',
37
- project: string | null = null,
38
- ): void {
39
- for (const rawEntry of entries) {
40
- counters.entriesScanned++;
41
- try {
42
- const parsed = parseMarkdownMemoryEntry(rawEntry, target, project);
43
- const result = syncMemoryEntry(dbManager, parsed);
44
- if (result.action === 'inserted') counters.imported++;
45
- else counters.skipped++;
46
- } catch (err) {
47
- counters.warnings.push(
48
- `${path.basename(project ?? 'global')}/${target}: ${err instanceof Error ? err.message : String(err)}`,
49
- );
50
- }
51
- }
52
- }
53
-
54
42
  function scanProjectDirs(agentRoot: string, globalDir: string, projectsMemoryDir = "projects-memory"): Array<{ name: string; memoryFile: string }> {
55
- const projectsRoot = path.join(agentRoot, projectsMemoryDir);
43
+ const projectsRoot = path.resolve(agentRoot, projectsMemoryDir);
56
44
  const projects = new Map<string, string>();
57
45
 
58
46
  if (fs.existsSync(projectsRoot)) {
59
47
  for (const name of fs.readdirSync(projectsRoot)) {
60
- const dir = path.join(projectsRoot, name);
61
- const memoryFile = path.join(dir, MEMORY_FILE);
62
- if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(memoryFile)) {
48
+ if (!isSafeProjectName(name, projectsRoot)) continue;
49
+ const memoryFile = resolveAuthoritativeMemoryFile(projectsRoot, name);
50
+ if (memoryFile) {
63
51
  projects.set(name, memoryFile);
64
52
  }
65
53
  }
@@ -74,9 +62,9 @@ function scanProjectDirs(agentRoot: string, globalDir: string, projectsMemoryDir
74
62
  for (const name of fs.readdirSync(agentRoot)) {
75
63
  if ((globalDirName && name === globalDirName) || name === projectsMemoryDir || name === 'skills' || name.startsWith('.')) continue;
76
64
  if (projects.has(name)) continue;
77
- const dir = path.join(agentRoot, name);
78
- const memoryFile = path.join(dir, MEMORY_FILE);
79
- if (fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.existsSync(memoryFile)) {
65
+ if (!isSafeProjectName(name, resolvedAgentRoot)) continue;
66
+ const memoryFile = resolveAuthoritativeMemoryFile(resolvedAgentRoot, name);
67
+ if (memoryFile) {
80
68
  projects.set(name, memoryFile);
81
69
  }
82
70
  }
@@ -87,17 +75,73 @@ function scanProjectDirs(agentRoot: string, globalDir: string, projectsMemoryDir
87
75
  .filter(({ memoryFile }) => fs.existsSync(memoryFile));
88
76
  }
89
77
 
90
- export function syncMarkdownMemoriesToSqlite(
78
+ function realpathIfPresent(filePath: string): string {
79
+ try {
80
+ return fs.realpathSync(filePath);
81
+ } catch (error) {
82
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return path.resolve(filePath);
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ function resolveAuthoritativeMemoryFile(root: string, projectName: string): string | null {
88
+ const canonicalRoot = realpathIfPresent(root);
89
+ if (!isSafeProjectName(projectName, path.resolve(root))) return null;
90
+
91
+ const projectDir = path.join(root, projectName);
92
+ let projectStat: fs.Stats;
93
+ try {
94
+ projectStat = fs.lstatSync(projectDir);
95
+ } catch (error) {
96
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
97
+ return path.join(canonicalRoot, projectName, MEMORY_FILE);
98
+ }
99
+ throw error;
100
+ }
101
+ if (projectStat.isSymbolicLink() || !projectStat.isDirectory()) return null;
102
+
103
+ const canonicalProjectDir = fs.realpathSync(projectDir);
104
+ if (path.dirname(canonicalProjectDir) !== canonicalRoot) return null;
105
+
106
+ const memoryFile = path.join(projectDir, MEMORY_FILE);
107
+ let memoryStat: fs.Stats;
108
+ try {
109
+ memoryStat = fs.lstatSync(memoryFile);
110
+ } catch (error) {
111
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
112
+ return path.join(canonicalProjectDir, MEMORY_FILE);
113
+ }
114
+ throw error;
115
+ }
116
+ if (memoryStat.isSymbolicLink() || !memoryStat.isFile()) return null;
117
+
118
+ const canonicalMemoryFile = fs.realpathSync(memoryFile);
119
+ if (path.dirname(canonicalMemoryFile) !== canonicalProjectDir || path.basename(canonicalMemoryFile) !== MEMORY_FILE) {
120
+ return null;
121
+ }
122
+ return canonicalMemoryFile;
123
+ }
124
+
125
+ function isSafeProjectName(name: string, projectsRoot: string): boolean {
126
+ if (!name || name === '.' || name === '..' || name.includes('/') || name.includes('\\') || path.isAbsolute(name)) {
127
+ return false;
128
+ }
129
+ const projectDir = path.resolve(projectsRoot, name);
130
+ return path.dirname(projectDir) === projectsRoot && path.basename(projectDir) === name;
131
+ }
132
+
133
+ export async function syncMarkdownMemoriesToSqlite(
91
134
  dbManager: DatabaseManager,
92
135
  globalDir: string,
93
136
  projectsMemoryDir?: string,
94
137
  agentRoot = AGENT_ROOT,
95
- ): BackfillCounters & { projectCount: number } {
138
+ ): Promise<BackfillCounters & { projectCount: number }> {
96
139
  const counters: BackfillCounters = {
97
140
  filesScanned: 0,
98
141
  entriesScanned: 0,
99
142
  imported: 0,
100
143
  skipped: 0,
144
+ removed: 0,
101
145
  warnings: [],
102
146
  };
103
147
 
@@ -105,27 +149,74 @@ export function syncMarkdownMemoriesToSqlite(
105
149
  const globalUserFile = path.join(globalDir, USER_FILE);
106
150
  const globalFailureFile = path.join(globalDir, 'failures.md');
107
151
 
108
- const importFile = (
109
- filePath: string,
152
+ const reconcileFile = async (
153
+ filePath: string | null,
110
154
  target: 'memory' | 'user' | 'failure',
111
155
  project: string | null = null,
112
156
  ) => {
113
- if (!fs.existsSync(filePath)) return;
114
- counters.filesScanned++;
115
- const entries = readEntries(filePath);
116
- importEntries(dbManager, counters, entries, target, project);
157
+ const reconcile = () => {
158
+ if (filePath && fs.existsSync(filePath)) counters.filesScanned++;
159
+ const entries = filePath ? readEntries(filePath) : [];
160
+ counters.entriesScanned += entries.length;
161
+ try {
162
+ const result = target === 'failure'
163
+ ? reconcileMarkdownFailureScopes(dbManager, entries)
164
+ : reconcileMarkdownMemoryScope(dbManager, entries, target, project);
165
+ counters.imported += result.inserted;
166
+ counters.skipped += result.existing;
167
+ counters.removed += result.removed;
168
+ } catch (err) {
169
+ counters.warnings.push(
170
+ `${path.basename(project ?? 'global')}/${target}: ${err instanceof Error ? err.message : String(err)}`,
171
+ );
172
+ }
173
+ };
174
+ if (filePath) await withMarkdownMutationLock(filePath, reconcile);
175
+ else reconcile();
117
176
  };
118
177
 
119
- importFile(globalMemoryFile, 'memory');
120
- importFile(globalUserFile, 'user');
121
- importFile(globalFailureFile, 'failure');
178
+ await reconcileFile(globalMemoryFile, 'memory');
179
+ await reconcileFile(globalUserFile, 'user');
180
+ await reconcileFile(globalFailureFile, 'failure');
122
181
 
123
182
  const projects = scanProjectDirs(agentRoot, globalDir, projectsMemoryDir);
124
- for (const project of projects) {
125
- importFile(project.memoryFile, 'memory', project.name);
183
+ const projectFiles = new Map(projects.map((project) => [project.name, project.memoryFile]));
184
+ const mirroredProjects = dbManager.getDb().prepare(`
185
+ SELECT DISTINCT project
186
+ FROM memories
187
+ WHERE project IS NOT NULL AND target = 'memory'
188
+ `).all() as Array<{ project: string }>;
189
+ const projectNames = new Set([
190
+ ...projectFiles.keys(),
191
+ ...mirroredProjects.map(({ project }) => project),
192
+ ]);
193
+ const projectsRoot = path.resolve(agentRoot, projectsMemoryDir ?? 'projects-memory');
194
+ for (const projectName of projectNames) {
195
+ const memoryFile = projectFiles.get(projectName)
196
+ ?? resolveAuthoritativeMemoryFile(projectsRoot, projectName);
197
+ await reconcileFile(memoryFile, 'memory', projectName);
126
198
  }
127
199
 
128
- return { ...counters, projectCount: projects.length };
200
+ return { ...counters, projectCount: projectNames.size };
201
+ }
202
+
203
+ export async function migrateThenSyncMarkdownMemories(
204
+ dbManager: DatabaseManager,
205
+ legacyGlobalDir: string | null,
206
+ globalDir: string,
207
+ projectsMemoryDir?: string,
208
+ agentRoot = AGENT_ROOT,
209
+ migrationOptions: MigrationSyncOptions = {},
210
+ ): Promise<BackfillCounters & { projectCount: number }> {
211
+ if (legacyGlobalDir) {
212
+ const migration = await migrateExtensionRoot(legacyGlobalDir, globalDir, migrationOptions);
213
+ const sessionsFailure = migration.criticalFailures.find((failure) => failure.name === 'sessions.db');
214
+ if (sessionsFailure) {
215
+ throw new Error(`sessions.db migration failed: ${sessionsFailure.message}`);
216
+ }
217
+ migrationOptions.onMigrationSucceeded?.();
218
+ }
219
+ return await syncMarkdownMemoriesToSqlite(dbManager, globalDir, projectsMemoryDir, agentRoot);
129
220
  }
130
221
 
131
222
  export function registerSyncMarkdownMemoriesCommand(
@@ -136,19 +227,20 @@ export function registerSyncMarkdownMemoriesCommand(
136
227
  agentRoot = AGENT_ROOT,
137
228
  ): void {
138
229
  pi.registerCommand('memory-sync-markdown', {
139
- description: 'Backfill Markdown memories into the SQLite search store',
230
+ description: 'Reconcile the SQLite search mirror with Markdown memories',
140
231
  handler: async (_args, ctx: ExtensionCommandContext) => {
141
- ctx.ui.notify('🔄 Scanning Markdown memory files for SQLite backfill...', 'info');
232
+ ctx.ui.notify('🔄 Reconciling the SQLite search mirror with Markdown memories...', 'info');
142
233
 
143
234
  try {
144
- const counters = syncMarkdownMemoriesToSqlite(dbManager, globalDir, projectsMemoryDir, agentRoot);
235
+ const counters = await syncMarkdownMemoriesToSqlite(dbManager, globalDir, projectsMemoryDir, agentRoot);
145
236
 
146
237
  let output = `\n✅ Markdown → SQLite sync complete!\n\n`;
147
238
  output += `📊 Results:\n`;
148
239
  output += `├─ Files scanned: ${counters.filesScanned}\n`;
149
240
  output += `├─ Entries scanned: ${counters.entriesScanned}\n`;
150
241
  output += `├─ Imported into SQLite: ${counters.imported}\n`;
151
- output += `└─ Skipped as duplicates: ${counters.skipped}\n`;
242
+ output += `├─ Skipped as duplicates: ${counters.skipped}\n`;
243
+ output += `└─ Removed orphaned rows: ${counters.removed}\n`;
152
244
 
153
245
  if (counters.projectCount > 0) {
154
246
  output += `\n📁 Project memories scanned: ${counters.projectCount}\n`;
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import * as path from "node:path";
26
+ import * as fs from "node:fs";
26
27
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
27
28
  import { MemoryStore } from "./store/memory-store.js";
28
29
  import { SkillStore } from "./store/skill-store.js";
@@ -45,14 +46,14 @@ import { registerInterviewCommand } from "./handlers/interview.js";
45
46
  import { registerSwitchProjectCommand } from "./handlers/switch-project.js";
46
47
  import { registerIndexSessionsCommand } from "./handlers/index-sessions.js";
47
48
  import { registerLearnMemoryCommand } from "./handlers/learn-memory.js";
48
- import { registerSyncMarkdownMemoriesCommand, syncMarkdownMemoriesToSqlite } from "./handlers/sync-markdown-memories.js";
49
+ import { migrateThenSyncMarkdownMemories, registerSyncMarkdownMemoriesCommand } from "./handlers/sync-markdown-memories.js";
49
50
  import { registerPreviewContextCommand } from "./handlers/preview-context.js";
50
51
  import { loadConfig } from "./config.js";
51
52
  import { detectProject, detectProjectSkills } from "./project.js";
52
53
  import { buildPromptContext } from "./prompt-context.js";
53
54
  import { migrateLegacyProjectMemoryDirs } from "./project-memory-migration.js";
54
- import { migrateExtensionRoot } from "./extension-root-migration.js";
55
55
  import { AGENT_ROOT } from "./paths.js";
56
+ import { isDatabaseMigrationPending } from "./extension-root-migration.js";
56
57
 
57
58
  export function resolveProjectSkillDiscovery(
58
59
  skillStore: SkillStore,
@@ -95,7 +96,7 @@ export default function (pi: ExtensionAPI) {
95
96
  : configuredMemoryDir;
96
97
 
97
98
  const shouldMigrateExtensionRoot = !configuredMemoryDir || pointsToLegacyMemoryDir;
98
- let extensionRootMigrated = false;
99
+ let persistenceInitialized = false;
99
100
 
100
101
  const store = new MemoryStore({ ...config, memoryDir: globalDir });
101
102
  const project = detectProject(config.projectsMemoryDir);
@@ -109,6 +110,15 @@ export default function (pi: ExtensionAPI) {
109
110
  migrationSentinelPath: path.join(globalDir, ".skills-migrated-to-extension-storage"),
110
111
  });
111
112
  const dbManager = new DatabaseManager(globalDir);
113
+ let databaseMigrationPending = shouldMigrateExtensionRoot
114
+ && isDatabaseMigrationPending(legacyGlobalDir, globalDir);
115
+ if (databaseMigrationPending) {
116
+ dbManager.setOpenGuard(() => {
117
+ if (databaseMigrationPending) {
118
+ throw new Error("Legacy sessions.db migration is pending");
119
+ }
120
+ });
121
+ }
112
122
  const sessionsDir = path.join(agentRoot, "sessions");
113
123
 
114
124
  const refreshSkillProjectContext = (cwd?: string) => {
@@ -124,12 +134,6 @@ export default function (pi: ExtensionAPI) {
124
134
  // ~/.pi/agent/<project>/ layout. This is non-destructive: legacy folders
125
135
  // remain in place while entries are copied/merged into projects-memory/.
126
136
  migrateLegacyProjectMemoryDirs(agentRoot, config.projectsMemoryDir);
127
- try {
128
- syncMarkdownMemoriesToSqlite(dbManager, globalDir, config.projectsMemoryDir, agentRoot);
129
- } catch {
130
- // Best-effort only: failed SQLite backfill should not block extension startup.
131
- }
132
-
133
137
  // Detect project from cwd using shared helper
134
138
  // Project-scoped store: ~/.pi/agent/<projectsMemoryDir>/<project_name>/
135
139
  const projectConfig = project.memoryDir
@@ -139,13 +143,25 @@ export default function (pi: ExtensionAPI) {
139
143
 
140
144
  // ── 1. Load memory from disk on session start ──
141
145
  pi.on("session_start", async (_event, ctx) => {
142
- if (shouldMigrateExtensionRoot && !extensionRootMigrated) {
146
+ if (!persistenceInitialized) {
143
147
  try {
144
- await migrateExtensionRoot(legacyGlobalDir, globalDir);
148
+ await migrateThenSyncMarkdownMemories(
149
+ dbManager,
150
+ shouldMigrateExtensionRoot ? legacyGlobalDir : null,
151
+ globalDir,
152
+ config.projectsMemoryDir,
153
+ agentRoot,
154
+ {
155
+ onMigrationSucceeded: () => {
156
+ databaseMigrationPending = false;
157
+ dbManager.setOpenGuard(null);
158
+ },
159
+ },
160
+ );
161
+ persistenceInitialized = true;
145
162
  } catch {
146
- // best effort migration only
163
+ // Best-effort only: migration or SQLite backfill must not block startup.
147
164
  }
148
- extensionRootMigrated = true;
149
165
  }
150
166
 
151
167
  refreshSkillProjectContext(ctx.cwd);
@@ -154,7 +170,7 @@ export default function (pi: ExtensionAPI) {
154
170
  await store.loadFromDisk();
155
171
  if (projectStore) await projectStore.loadFromDisk();
156
172
 
157
- scheduleSessionBackfill(dbManager, sessionsDir, {
173
+ if (persistenceInitialized) scheduleSessionBackfill(dbManager, sessionsDir, {
158
174
  notify: (message, level) => {
159
175
  const ui = (ctx as { ui?: { notify?: (message: string, level?: string) => void } }).ui;
160
176
  if (ui?.notify) {
@@ -194,7 +210,7 @@ export default function (pi: ExtensionAPI) {
194
210
  });
195
211
 
196
212
  // ── 6. Setup session-end flush ──
197
- setupSessionFlush(pi, store, projectStore, config);
213
+ setupSessionFlush(pi, store, projectStore, config, dbManager, projectName);
198
214
 
199
215
  // ── 7. Setup auto-consolidation (inject consolidator into stores) ──
200
216
  store.setConsolidator(async (target, signal) => {
@@ -206,7 +222,7 @@ export default function (pi: ExtensionAPI) {
206
222
  return triggerConsolidation(pi, projectStore, target, signal, config.consolidationTimeoutMs, toolTarget, config);
207
223
  });
208
224
  }
209
- registerConsolidateCommand(pi, store, config.consolidationTimeoutMs, projectStore, projectName, config);
225
+ registerConsolidateCommand(pi, store, config.consolidationTimeoutMs, projectStore, projectName, config, dbManager);
210
226
 
211
227
  // ── 8. Setup correction detection ──
212
228
  setupCorrectionDetector(pi, store, projectStore, config, dbManager, projectName);