@oh-my-pi/pi-coding-agent 13.12.7 → 13.12.9

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.
@@ -142,7 +142,13 @@ import {
142
142
  type PythonExecutionMessage,
143
143
  pythonExecutionToText,
144
144
  } from "./messages";
145
- import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
145
+ import type {
146
+ BranchSummaryEntry,
147
+ CompactionEntry,
148
+ NewSessionOptions,
149
+ SessionContext,
150
+ SessionManager,
151
+ } from "./session-manager";
146
152
  import { getLatestCompactionEntry } from "./session-manager";
147
153
 
148
154
  /** Session-specific events that extend the core AgentEvent */
@@ -215,8 +221,10 @@ export interface AgentSessionConfig {
215
221
  rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>;
216
222
  /** Enable hidden-by-default MCP tool discovery for this session. */
217
223
  mcpDiscoveryEnabled?: boolean;
218
- /** MCP tool names previously selected via discovery in this session. */
224
+ /** MCP tool names to activate for the current session when discovery mode is enabled. */
219
225
  initialSelectedMCPToolNames?: string[];
226
+ /** MCP tool names that should seed brand-new sessions created from this AgentSession. */
227
+ defaultSelectedMCPToolNames?: string[];
220
228
  /** TTSR manager for time-traveling stream rules */
221
229
  ttsrManager?: TtsrManager;
222
230
  /** Secret obfuscator for deobfuscating streaming edit content */
@@ -415,6 +423,8 @@ export class AgentSession {
415
423
  #discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
416
424
  #discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
417
425
  #selectedMCPToolNames = new Set<string>();
426
+ #defaultSelectedMCPToolNames = new Set<string>();
427
+ #sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
418
428
 
419
429
  // TTSR manager for time-traveling stream rules
420
430
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -464,7 +474,20 @@ export class AgentSession {
464
474
  this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
465
475
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
466
476
  this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
477
+ this.#defaultSelectedMCPToolNames = new Set(config.defaultSelectedMCPToolNames ?? []);
467
478
  this.#pruneSelectedMCPToolNames();
479
+ const persistedSelectedMCPToolNames = this.sessionManager.buildSessionContext().selectedMCPToolNames;
480
+ const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
481
+ if (
482
+ this.#mcpDiscoveryEnabled &&
483
+ !this.#selectedMCPToolNamesMatch(persistedSelectedMCPToolNames, currentSelectedMCPToolNames)
484
+ ) {
485
+ this.sessionManager.appendMCPToolSelection(currentSelectedMCPToolNames);
486
+ }
487
+ this.#rememberSessionDefaultSelectedMCPToolNames(
488
+ this.sessionManager.getSessionFile(),
489
+ this.#defaultSelectedMCPToolNames,
490
+ );
468
491
  this.#ttsrManager = config.ttsrManager;
469
492
  this.#obfuscator = config.obfuscator;
470
493
  this.agent.providerSessionState = this.#providerSessionState;
@@ -1632,23 +1655,43 @@ export class AgentSession {
1632
1655
  this.#discoverableMCPSearchIndex = null;
1633
1656
  }
1634
1657
 
1658
+ #filterSelectableMCPToolNames(toolNames: Iterable<string>): string[] {
1659
+ return Array.from(toolNames).filter(name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name));
1660
+ }
1661
+
1635
1662
  #pruneSelectedMCPToolNames(): void {
1636
- for (const name of Array.from(this.#selectedMCPToolNames)) {
1637
- if (!this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
1638
- this.#selectedMCPToolNames.delete(name);
1639
- }
1640
- }
1663
+ this.#selectedMCPToolNames = new Set(this.#filterSelectableMCPToolNames(this.#selectedMCPToolNames));
1641
1664
  }
1642
1665
 
1643
- #getVisibleMCPToolNames(): string[] {
1644
- if (!this.#mcpDiscoveryEnabled) {
1645
- return Array.from(this.#toolRegistry.keys()).filter(name => isMCPToolName(name));
1646
- }
1647
- return Array.from(this.#selectedMCPToolNames).filter(
1648
- name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name),
1666
+ #selectedMCPToolNamesMatch(left: string[], right: string[]): boolean {
1667
+ return left.length === right.length && left.every((name, index) => name === right[index]);
1668
+ }
1669
+
1670
+ #rememberSessionDefaultSelectedMCPToolNames(
1671
+ sessionFile: string | null | undefined,
1672
+ toolNames: Iterable<string>,
1673
+ ): void {
1674
+ if (!sessionFile) return;
1675
+ this.#sessionDefaultSelectedMCPToolNames.set(
1676
+ path.resolve(sessionFile),
1677
+ this.#filterSelectableMCPToolNames(toolNames),
1649
1678
  );
1650
1679
  }
1651
1680
 
1681
+ #getSessionDefaultSelectedMCPToolNames(sessionFile: string | null | undefined): string[] {
1682
+ if (!sessionFile) return [];
1683
+ return this.#sessionDefaultSelectedMCPToolNames.get(path.resolve(sessionFile)) ?? [];
1684
+ }
1685
+
1686
+ #persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames: string[]): void {
1687
+ if (!this.#mcpDiscoveryEnabled) return;
1688
+ const nextSelectedMCPToolNames = this.getSelectedMCPToolNames();
1689
+ if (this.#selectedMCPToolNamesMatch(previousSelectedMCPToolNames, nextSelectedMCPToolNames)) {
1690
+ return;
1691
+ }
1692
+ this.sessionManager.appendMCPToolSelection(nextSelectedMCPToolNames);
1693
+ }
1694
+
1652
1695
  #getActiveNonMCPToolNames(): string[] {
1653
1696
  return this.getActiveToolNames().filter(name => !isMCPToolName(name) && this.#toolRegistry.has(name));
1654
1697
  }
@@ -1699,35 +1742,35 @@ export class AgentSession {
1699
1742
  if (!this.#mcpDiscoveryEnabled) {
1700
1743
  return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
1701
1744
  }
1702
- return Array.from(this.#selectedMCPToolNames).filter(
1703
- name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name),
1704
- );
1745
+ return this.#filterSelectableMCPToolNames(this.#selectedMCPToolNames);
1705
1746
  }
1706
1747
 
1707
1748
  async activateDiscoveredMCPTools(toolNames: string[]): Promise<string[]> {
1749
+ const nextSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
1708
1750
  const activated: string[] = [];
1709
1751
  for (const name of toolNames) {
1710
1752
  if (!isMCPToolName(name) || !this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
1711
1753
  continue;
1712
1754
  }
1713
- this.#selectedMCPToolNames.add(name);
1755
+ nextSelectedMCPToolNames.add(name);
1714
1756
  activated.push(name);
1715
1757
  }
1716
1758
  if (activated.length === 0) {
1717
1759
  return [];
1718
1760
  }
1719
- const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.#getVisibleMCPToolNames()];
1761
+ const nextActive = [
1762
+ ...this.#getActiveNonMCPToolNames(),
1763
+ ...this.#filterSelectableMCPToolNames(nextSelectedMCPToolNames),
1764
+ ];
1720
1765
  await this.setActiveToolsByName(nextActive);
1721
1766
  return [...new Set(activated)];
1722
1767
  }
1723
1768
 
1724
- /**
1725
- * Set active tools by name.
1726
- * Only tools in the registry can be enabled. Unknown tool names are ignored.
1727
- * Also rebuilds the system prompt to reflect the new tool set.
1728
- * Changes take effect before the next model call.
1729
- */
1730
- async setActiveToolsByName(toolNames: string[]): Promise<void> {
1769
+ async #applyActiveToolsByName(
1770
+ toolNames: string[],
1771
+ options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
1772
+ ): Promise<void> {
1773
+ const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
1731
1774
  const tools: AgentTool[] = [];
1732
1775
  const validToolNames: string[] = [];
1733
1776
  for (const name of toolNames) {
@@ -1751,8 +1794,36 @@ export class AgentSession {
1751
1794
  this.#baseSystemPrompt = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
1752
1795
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
1753
1796
  }
1797
+ if (options?.persistMCPSelection !== false) {
1798
+ this.#persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames);
1799
+ }
1754
1800
  }
1755
1801
 
1802
+ /**
1803
+ * Set active tools by name.
1804
+ * Only tools in the registry can be enabled. Unknown tool names are ignored.
1805
+ * Also rebuilds the system prompt to reflect the new tool set.
1806
+ * Changes take effect before the next model call.
1807
+ */
1808
+ async setActiveToolsByName(toolNames: string[]): Promise<void> {
1809
+ await this.#applyActiveToolsByName(toolNames);
1810
+ }
1811
+
1812
+ async #restoreMCPSelectionsForSessionContext(
1813
+ sessionContext: SessionContext,
1814
+ options?: { fallbackSelectedMCPToolNames?: Iterable<string> },
1815
+ ): Promise<void> {
1816
+ if (!this.#mcpDiscoveryEnabled) return;
1817
+ const nextActiveNonMCPToolNames = this.#getActiveNonMCPToolNames();
1818
+ const fallbackSelectedMCPToolNames = options?.fallbackSelectedMCPToolNames ?? this.#defaultSelectedMCPToolNames;
1819
+ const restoredMCPToolNames = sessionContext.hasPersistedMCPToolSelection
1820
+ ? this.#filterSelectableMCPToolNames(sessionContext.selectedMCPToolNames)
1821
+ : this.#filterSelectableMCPToolNames(fallbackSelectedMCPToolNames);
1822
+ this.#rememberSessionDefaultSelectedMCPToolNames(this.sessionFile, restoredMCPToolNames);
1823
+ await this.#applyActiveToolsByName([...nextActiveNonMCPToolNames, ...restoredMCPToolNames], {
1824
+ persistMCPSelection: false,
1825
+ });
1826
+ }
1756
1827
  /** Rebuild the base system prompt using the current active tool set. */
1757
1828
  async refreshBaseSystemPrompt(): Promise<void> {
1758
1829
  if (!this.#rebuildSystemPrompt) return;
@@ -1766,6 +1837,7 @@ export class AgentSession {
1766
1837
  * This allows /mcp add/remove/reauth to take effect without restarting the session.
1767
1838
  */
1768
1839
  async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
1840
+ const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
1769
1841
  const existingNames = Array.from(this.#toolRegistry.keys());
1770
1842
  for (const name of existingNames) {
1771
1843
  if (isMCPToolName(name)) {
@@ -1796,7 +1868,7 @@ export class AgentSession {
1796
1868
  this.#pruneSelectedMCPToolNames();
1797
1869
 
1798
1870
  const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.getSelectedMCPToolNames()];
1799
- await this.setActiveToolsByName(nextActive);
1871
+ await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
1800
1872
  }
1801
1873
 
1802
1874
  /** Whether auto-compaction is currently running */
@@ -2747,6 +2819,12 @@ export class AgentSession {
2747
2819
  */
2748
2820
  async newSession(options?: NewSessionOptions): Promise<boolean> {
2749
2821
  const previousSessionFile = this.sessionFile;
2822
+ const nextDiscoverySessionToolNames = this.#mcpDiscoveryEnabled
2823
+ ? [
2824
+ ...this.#getActiveNonMCPToolNames(),
2825
+ ...this.#filterSelectableMCPToolNames(this.#defaultSelectedMCPToolNames),
2826
+ ]
2827
+ : undefined;
2750
2828
 
2751
2829
  // Emit session_before_switch event with reason "new" (can be cancelled)
2752
2830
  if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
@@ -2774,6 +2852,13 @@ export class AgentSession {
2774
2852
 
2775
2853
  this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
2776
2854
  this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
2855
+ if (nextDiscoverySessionToolNames) {
2856
+ await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
2857
+ if (this.getSelectedMCPToolNames().length > 0) {
2858
+ this.sessionManager.appendMCPToolSelection(this.getSelectedMCPToolNames());
2859
+ }
2860
+ }
2861
+ this.#rememberSessionDefaultSelectedMCPToolNames(this.sessionFile, this.#defaultSelectedMCPToolNames);
2777
2862
 
2778
2863
  this.#todoReminderCount = 0;
2779
2864
  this.#planReferenceSent = false;
@@ -4862,6 +4947,8 @@ export class AgentSession {
4862
4947
 
4863
4948
  // Reload messages
4864
4949
  const sessionContext = this.sessionManager.buildSessionContext();
4950
+ const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
4951
+ await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
4865
4952
 
4866
4953
  // Emit session_switch event to hooks
4867
4954
  if (this.#extensionRunner) {
@@ -4965,6 +5052,8 @@ export class AgentSession {
4965
5052
  // Reload messages from entries (works for both file and in-memory mode)
4966
5053
  const sessionContext = this.sessionManager.buildSessionContext();
4967
5054
 
5055
+ await this.#restoreMCPSelectionsForSessionContext(sessionContext);
5056
+
4968
5057
  // Emit session_branch event to hooks (after branch completes)
4969
5058
  if (this.#extensionRunner) {
4970
5059
  await this.#extensionRunner.emit({
@@ -5128,6 +5217,7 @@ export class AgentSession {
5128
5217
 
5129
5218
  // Update agent state
5130
5219
  const sessionContext = this.sessionManager.buildSessionContext();
5220
+ await this.#restoreMCPSelectionsForSessionContext(sessionContext);
5131
5221
  this.agent.replaceMessages(sessionContext.messages);
5132
5222
  this.#syncTodoPhasesFromBranch();
5133
5223
  this.#closeCodexProviderSessionsForHistoryRewrite();
@@ -18,7 +18,8 @@ type ModelUsageRow = {
18
18
  };
19
19
 
20
20
  /** Bump when schema changes require migration */
21
- const SCHEMA_VERSION = 4;
21
+ const SCHEMA_VERSION = 5;
22
+ const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
22
23
 
23
24
  /** Singleton instances per database path */
24
25
  const instances = new Map<string, AgentStorage>();
@@ -59,9 +60,8 @@ export class AgentStorage {
59
60
  this.#authStore = new AuthCredentialStore(this.#db);
60
61
 
61
62
  this.#listSettingsStmt = this.#db.prepare("SELECT key, value FROM settings");
62
-
63
63
  this.#upsertModelUsageStmt = this.#db.prepare(
64
- "INSERT INTO model_usage (model_key, last_used_at) VALUES (?, unixepoch()) ON CONFLICT(model_key) DO UPDATE SET last_used_at = unixepoch()",
64
+ `INSERT INTO model_usage (model_key, last_used_at) VALUES (?, ${SQLITE_NOW_EPOCH}) ON CONFLICT(model_key) DO UPDATE SET last_used_at = ${SQLITE_NOW_EPOCH}`,
65
65
  );
66
66
  this.#listModelUsageStmt = this.#db.prepare(
67
67
  "SELECT model_key, last_used_at FROM model_usage ORDER BY last_used_at DESC",
@@ -80,7 +80,7 @@ PRAGMA busy_timeout=5000;
80
80
 
81
81
  CREATE TABLE IF NOT EXISTS model_usage (
82
82
  model_key TEXT PRIMARY KEY,
83
- last_used_at INTEGER NOT NULL DEFAULT (unixepoch())
83
+ last_used_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
84
84
  );
85
85
 
86
86
  CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
@@ -96,7 +96,7 @@ CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
96
96
  CREATE TABLE settings (
97
97
  key TEXT PRIMARY KEY,
98
98
  value TEXT NOT NULL,
99
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
99
+ updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
100
100
  );
101
101
  `);
102
102
  } else if (!hasKey || !hasValue) {
@@ -122,12 +122,12 @@ CREATE TABLE settings (
122
122
  CREATE TABLE settings (
123
123
  key TEXT PRIMARY KEY,
124
124
  value TEXT NOT NULL,
125
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
125
+ updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
126
126
  );
127
127
  `);
128
128
  if (settings) {
129
129
  const insert = this.#db.prepare(
130
- "INSERT INTO settings (key, value, updated_at) VALUES (?, ?, unixepoch())",
130
+ `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ${SQLITE_NOW_EPOCH})`,
131
131
  );
132
132
  for (const [key, value] of Object.entries(settings)) {
133
133
  if (value === undefined) continue;
@@ -144,12 +144,15 @@ CREATE TABLE settings (
144
144
  const versionRow = this.#db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get() as
145
145
  | { version?: number }
146
146
  | undefined;
147
+ const schemaVersion = typeof versionRow?.version === "number" ? versionRow.version : 0;
147
148
  if (versionRow?.version !== undefined && versionRow.version !== SCHEMA_VERSION) {
148
149
  logger.warn("AgentStorage schema version mismatch", {
149
150
  current: versionRow.version,
150
151
  expected: SCHEMA_VERSION,
151
152
  });
152
- this.#migrateSchema(versionRow.version);
153
+ }
154
+ if (schemaVersion < SCHEMA_VERSION) {
155
+ this.#migrateSchema(schemaVersion);
153
156
  }
154
157
  this.#db.prepare("INSERT OR REPLACE INTO schema_version(version) VALUES (?)").run(SCHEMA_VERSION);
155
158
  }
@@ -159,6 +162,43 @@ CREATE TABLE settings (
159
162
  // v3 → v4: Add disabled column to auth_credentials (handled by AuthCredentialStore)
160
163
  // Nothing to do here - AuthCredentialStore will handle this migration
161
164
  }
165
+ if (fromVersion < 5) {
166
+ this.#migrateSchemaV4ToV5();
167
+ }
168
+ }
169
+
170
+ #migrateSchemaV4ToV5(): void {
171
+ const migrate = this.#db.transaction(() => {
172
+ this.#db.exec("ALTER TABLE settings RENAME TO settings_legacy");
173
+ this.#db.exec(`
174
+ CREATE TABLE settings (
175
+ key TEXT PRIMARY KEY,
176
+ value TEXT NOT NULL,
177
+ updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
178
+ );
179
+ `);
180
+ this.#db.exec(`
181
+ INSERT INTO settings (key, value, updated_at)
182
+ SELECT key, value, updated_at
183
+ FROM settings_legacy
184
+ `);
185
+ this.#db.exec("DROP TABLE settings_legacy");
186
+
187
+ this.#db.exec("ALTER TABLE model_usage RENAME TO model_usage_legacy");
188
+ this.#db.exec(`
189
+ CREATE TABLE model_usage (
190
+ model_key TEXT PRIMARY KEY,
191
+ last_used_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
192
+ );
193
+ `);
194
+ this.#db.exec(`
195
+ INSERT INTO model_usage (model_key, last_used_at)
196
+ SELECT model_key, last_used_at
197
+ FROM model_usage_legacy
198
+ `);
199
+ this.#db.exec("DROP TABLE model_usage_legacy");
200
+ });
201
+ migrate();
162
202
  }
163
203
 
164
204
  /**
@@ -17,6 +17,8 @@ type HistoryRow = {
17
17
  cwd: string | null;
18
18
  };
19
19
 
20
+ const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
21
+
20
22
  export class HistoryStorage {
21
23
  #db: Database;
22
24
  static #instance?: HistoryStorage;
@@ -45,7 +47,7 @@ PRAGMA busy_timeout=5000;
45
47
  CREATE TABLE IF NOT EXISTS history (
46
48
  id INTEGER PRIMARY KEY AUTOINCREMENT,
47
49
  prompt TEXT NOT NULL,
48
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
50
+ created_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH}),
49
51
  cwd TEXT
50
52
  );
51
53
  CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
@@ -54,8 +56,12 @@ CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(prompt, content='histo
54
56
 
55
57
  CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
56
58
  INSERT INTO history_fts(rowid, prompt) VALUES (new.id, new.prompt);
57
- END;
58
- `);
59
+ END;
60
+ `);
61
+
62
+ if (this.#historySchemaUsesUnixEpoch()) {
63
+ this.#migrateHistorySchema();
64
+ }
59
65
 
60
66
  if (!hasFts) {
61
67
  try {
@@ -135,6 +141,41 @@ END;
135
141
  fs.mkdirSync(dir, { recursive: true });
136
142
  }
137
143
 
144
+ #historySchemaUsesUnixEpoch(): boolean {
145
+ const row = this.#db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'history'").get() as
146
+ | { sql?: string | null }
147
+ | undefined;
148
+ return row?.sql?.includes("unixepoch(") ?? false;
149
+ }
150
+
151
+ #migrateHistorySchema(): void {
152
+ const migrate = this.#db.transaction(() => {
153
+ this.#db.exec("ALTER TABLE history RENAME TO history_legacy");
154
+ this.#db.exec("DROP INDEX IF EXISTS idx_history_created_at");
155
+ this.#db.exec("DROP TRIGGER IF EXISTS history_ai");
156
+ this.#db.exec("DROP TABLE IF EXISTS history_fts");
157
+ this.#db.exec(`
158
+ CREATE TABLE history (
159
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
160
+ prompt TEXT NOT NULL,
161
+ created_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH}),
162
+ cwd TEXT
163
+ );
164
+ CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
165
+ INSERT INTO history (id, prompt, created_at, cwd)
166
+ SELECT id, prompt, created_at, cwd
167
+ FROM history_legacy;
168
+ DROP TABLE history_legacy;
169
+ CREATE VIRTUAL TABLE history_fts USING fts5(prompt, content='history', content_rowid='id');
170
+ CREATE TRIGGER history_ai AFTER INSERT ON history BEGIN
171
+ INSERT INTO history_fts(rowid, prompt) VALUES (new.id, new.prompt);
172
+ END;
173
+ `);
174
+ this.#db.run("INSERT INTO history_fts(history_fts) VALUES('rebuild')");
175
+ });
176
+ migrate();
177
+ }
178
+
138
179
  #normalizeLimit(limit: number): number {
139
180
  if (!Number.isFinite(limit)) return 0;
140
181
  const clamped = Math.max(0, Math.floor(limit));