@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.5

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.
Files changed (188) hide show
  1. package/CHANGELOG.md +114 -6
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/extension-flags.d.ts +36 -0
  4. package/dist/types/config/config-file.d.ts +4 -0
  5. package/dist/types/config/file-lock.d.ts +23 -0
  6. package/dist/types/config/keybindings.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +69 -65
  9. package/dist/types/edit/hashline/diff.d.ts +3 -3
  10. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  11. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  13. package/dist/types/eval/agent-bridge.d.ts +25 -0
  14. package/dist/types/eval/backend.d.ts +17 -2
  15. package/dist/types/eval/budget-bridge.d.ts +29 -0
  16. package/dist/types/eval/idle-timeout.d.ts +28 -0
  17. package/dist/types/eval/js/executor.d.ts +8 -0
  18. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  19. package/dist/types/eval/py/executor.d.ts +13 -0
  20. package/dist/types/exec/bash-executor.d.ts +1 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  22. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  23. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  24. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  25. package/dist/types/extensibility/shared-events.d.ts +2 -2
  26. package/dist/types/internal-urls/local-protocol.d.ts +19 -9
  27. package/dist/types/internal-urls/types.d.ts +14 -0
  28. package/dist/types/lsp/client.d.ts +3 -0
  29. package/dist/types/mcp/manager.d.ts +14 -5
  30. package/dist/types/memory-backend/index.d.ts +1 -1
  31. package/dist/types/memory-backend/resolve.d.ts +1 -1
  32. package/dist/types/memory-backend/types.d.ts +3 -3
  33. package/dist/types/mnemopi/backend.d.ts +4 -0
  34. package/dist/types/mnemopi/config.d.ts +29 -0
  35. package/dist/types/mnemopi/state.d.ts +72 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  37. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +6 -0
  39. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  40. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  41. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  42. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  43. package/dist/types/modes/interactive-mode.d.ts +7 -3
  44. package/dist/types/modes/magic-keywords.d.ts +14 -0
  45. package/dist/types/modes/markdown-prose.d.ts +27 -0
  46. package/dist/types/modes/orchestrate.d.ts +7 -2
  47. package/dist/types/modes/shared.d.ts +1 -1
  48. package/dist/types/modes/turn-budget.d.ts +18 -0
  49. package/dist/types/modes/types.d.ts +7 -3
  50. package/dist/types/modes/ultrathink.d.ts +7 -2
  51. package/dist/types/modes/workflow.d.ts +15 -0
  52. package/dist/types/sdk.d.ts +13 -3
  53. package/dist/types/session/agent-session.d.ts +36 -17
  54. package/dist/types/session/session-manager.d.ts +18 -0
  55. package/dist/types/session/session-storage.d.ts +6 -0
  56. package/dist/types/session/shake-types.d.ts +24 -0
  57. package/dist/types/task/executor.d.ts +2 -2
  58. package/dist/types/task/repair-args.d.ts +52 -0
  59. package/dist/types/tiny/models.d.ts +1 -1
  60. package/dist/types/tiny/title-client.d.ts +28 -2
  61. package/dist/types/tiny/title-protocol.d.ts +8 -5
  62. package/dist/types/tools/find.d.ts +1 -1
  63. package/dist/types/tools/index.d.ts +19 -3
  64. package/dist/types/tools/memory-edit.d.ts +1 -1
  65. package/dist/types/tools/path-utils.d.ts +7 -0
  66. package/dist/types/tui/output-block.d.ts +7 -7
  67. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  68. package/package.json +10 -10
  69. package/scripts/build-binary.ts +0 -1
  70. package/src/autoresearch/tools/run-experiment.ts +45 -113
  71. package/src/cli/args.ts +39 -16
  72. package/src/cli/extension-flags.ts +48 -0
  73. package/src/cli/plugin-cli.ts +11 -2
  74. package/src/cli.ts +59 -0
  75. package/src/config/config-file.ts +98 -13
  76. package/src/config/file-lock.ts +60 -17
  77. package/src/config/keybindings.ts +78 -27
  78. package/src/config/model-registry.ts +7 -1
  79. package/src/config/settings-schema.ts +73 -67
  80. package/src/config/settings.ts +22 -0
  81. package/src/edit/hashline/diff.ts +81 -24
  82. package/src/edit/renderer.ts +16 -12
  83. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  84. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  85. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  86. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  87. package/src/eval/agent-bridge.ts +295 -0
  88. package/src/eval/backend.ts +17 -2
  89. package/src/eval/budget-bridge.ts +48 -0
  90. package/src/eval/idle-timeout.ts +80 -0
  91. package/src/eval/js/executor.ts +35 -7
  92. package/src/eval/js/index.ts +2 -1
  93. package/src/eval/js/shared/prelude.txt +85 -1
  94. package/src/eval/js/tool-bridge.ts +9 -0
  95. package/src/eval/py/executor.ts +41 -14
  96. package/src/eval/py/index.ts +2 -1
  97. package/src/eval/py/prelude.py +132 -1
  98. package/src/exec/bash-executor.ts +2 -3
  99. package/src/extensibility/custom-tools/types.ts +2 -2
  100. package/src/extensibility/extensions/runner.ts +12 -2
  101. package/src/extensibility/plugins/git-url.ts +90 -4
  102. package/src/extensibility/plugins/manager.ts +103 -7
  103. package/src/extensibility/shared-events.ts +2 -2
  104. package/src/internal-urls/docs-index.generated.ts +88 -88
  105. package/src/internal-urls/local-protocol.ts +23 -11
  106. package/src/internal-urls/types.ts +15 -0
  107. package/src/lsp/client.ts +28 -5
  108. package/src/main.ts +44 -55
  109. package/src/mcp/manager.ts +87 -4
  110. package/src/memory-backend/index.ts +1 -1
  111. package/src/memory-backend/resolve.ts +3 -3
  112. package/src/memory-backend/types.ts +3 -3
  113. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  114. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  115. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  116. package/src/modes/components/agent-dashboard.ts +6 -6
  117. package/src/modes/components/custom-editor.ts +4 -11
  118. package/src/modes/components/extensions/state-manager.ts +3 -4
  119. package/src/modes/components/footer.ts +8 -9
  120. package/src/modes/components/hook-selector.ts +86 -20
  121. package/src/modes/components/oauth-selector.ts +93 -21
  122. package/src/modes/components/omfg-panel.ts +141 -0
  123. package/src/modes/components/settings-defs.ts +2 -2
  124. package/src/modes/components/tips.txt +2 -1
  125. package/src/modes/components/tool-execution.ts +38 -19
  126. package/src/modes/components/tree-selector.ts +4 -3
  127. package/src/modes/components/user-message-selector.ts +94 -19
  128. package/src/modes/components/user-message.ts +8 -1
  129. package/src/modes/controllers/command-controller.ts +25 -0
  130. package/src/modes/controllers/event-controller.ts +68 -3
  131. package/src/modes/controllers/input-controller.ts +14 -11
  132. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  133. package/src/modes/controllers/omfg-controller.ts +283 -0
  134. package/src/modes/controllers/omfg-rule.ts +647 -0
  135. package/src/modes/controllers/selector-controller.ts +1 -0
  136. package/src/modes/gradient-highlight.ts +23 -6
  137. package/src/modes/interactive-mode.ts +41 -7
  138. package/src/modes/magic-keywords.ts +20 -0
  139. package/src/modes/markdown-prose.ts +247 -0
  140. package/src/modes/orchestrate.ts +17 -11
  141. package/src/modes/shared.ts +3 -11
  142. package/src/modes/turn-budget.ts +31 -0
  143. package/src/modes/types.ts +7 -1
  144. package/src/modes/ultrathink.ts +16 -10
  145. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  146. package/src/modes/workflow.ts +42 -0
  147. package/src/prompts/system/omfg-user.md +51 -0
  148. package/src/prompts/system/project-prompt.md +3 -2
  149. package/src/prompts/system/subagent-system-prompt.md +12 -8
  150. package/src/prompts/system/system-prompt.md +9 -6
  151. package/src/prompts/system/workflow-notice.md +70 -0
  152. package/src/prompts/tools/eval.md +13 -1
  153. package/src/prompts/tools/memory-edit.md +1 -1
  154. package/src/sdk.ts +63 -33
  155. package/src/session/agent-session.ts +290 -55
  156. package/src/session/session-manager.ts +32 -0
  157. package/src/session/session-storage.ts +68 -8
  158. package/src/session/shake-types.ts +43 -0
  159. package/src/slash-commands/builtin-registry.ts +39 -16
  160. package/src/task/executor.ts +17 -7
  161. package/src/task/index.ts +9 -8
  162. package/src/task/repair-args.ts +117 -0
  163. package/src/tiny/models.ts +2 -2
  164. package/src/tiny/title-client.ts +133 -43
  165. package/src/tiny/title-protocol.ts +10 -5
  166. package/src/tiny/worker.ts +3 -46
  167. package/src/tools/ast-edit.ts +3 -0
  168. package/src/tools/ast-grep.ts +3 -0
  169. package/src/tools/eval.ts +202 -26
  170. package/src/tools/find.ts +20 -6
  171. package/src/tools/gh.ts +1 -0
  172. package/src/tools/grouped-file-output.ts +9 -2
  173. package/src/tools/index.ts +17 -5
  174. package/src/tools/memory-edit.ts +4 -4
  175. package/src/tools/memory-recall.ts +5 -5
  176. package/src/tools/memory-reflect.ts +5 -5
  177. package/src/tools/memory-retain.ts +4 -4
  178. package/src/tools/path-utils.ts +13 -2
  179. package/src/tools/read.ts +1 -0
  180. package/src/tools/render-utils.ts +2 -1
  181. package/src/tools/search.ts +491 -76
  182. package/src/tui/output-block.ts +37 -75
  183. package/src/utils/git.ts +9 -3
  184. package/dist/types/mnemosyne/backend.d.ts +0 -4
  185. package/dist/types/mnemosyne/config.d.ts +0 -29
  186. package/dist/types/mnemosyne/state.d.ts +0 -72
  187. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  188. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -1,5 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import * as fs from "node:fs/promises";
2
- import { isEnoent } from "@oh-my-pi/pi-utils";
3
+ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
3
4
 
4
5
  export interface FileLockOptions {
5
6
  staleMs?: number;
@@ -16,14 +17,15 @@ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
16
17
  interface LockInfo {
17
18
  pid: number;
18
19
  timestamp: number;
20
+ token: string;
19
21
  }
20
22
 
21
23
  function getLockPath(filePath: string): string {
22
24
  return `${filePath}.lock`;
23
25
  }
24
26
 
25
- async function writeLockInfo(lockPath: string): Promise<void> {
26
- const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
27
+ async function writeLockInfo(lockPath: string, token: string): Promise<void> {
28
+ const info: LockInfo = { pid: process.pid, timestamp: Date.now(), token };
27
29
  await Bun.write(`${lockPath}/info`, JSON.stringify(info));
28
30
  }
29
31
 
@@ -47,33 +49,58 @@ function isProcessAlive(pid: number): boolean {
47
49
 
48
50
  async function isLockStale(lockPath: string, staleMs: number): Promise<boolean> {
49
51
  const info = await readLockInfo(lockPath);
50
- if (!info) return true;
51
-
52
- if (!isProcessAlive(info.pid)) return true;
53
-
54
- if (Date.now() - info.timestamp > staleMs) return true;
52
+ if (info) {
53
+ if (!isProcessAlive(info.pid)) return true;
54
+ if (Date.now() - info.timestamp > staleMs) return true;
55
+ return false;
56
+ }
55
57
 
56
- return false;
58
+ // No info file. Either the lock holder is between mkdir and writeLockInfo
59
+ // (fresh dir, do not reap) or the dir was already removed (also do not
60
+ // reap — there is nothing to clean up, and an unguarded fs.rm here would
61
+ // race with another contender's successful mkdir and wipe their dir).
62
+ try {
63
+ const stat = await fs.stat(lockPath);
64
+ return Date.now() - stat.mtimeMs > staleMs;
65
+ } catch (err) {
66
+ if (isEnoent(err)) return false;
67
+ throw err;
68
+ }
57
69
  }
58
70
 
59
- async function tryAcquireLock(lockPath: string): Promise<boolean> {
71
+ async function tryAcquireLock(lockPath: string): Promise<string | null> {
60
72
  try {
61
73
  await fs.mkdir(lockPath);
62
- await writeLockInfo(lockPath);
63
- return true;
74
+ const token = randomUUID();
75
+ await writeLockInfo(lockPath, token);
76
+ return token;
64
77
  } catch (error) {
65
78
  if ((error as NodeJS.ErrnoException).code === "EEXIST") {
66
- return false;
79
+ return null;
67
80
  }
68
81
  throw error;
69
82
  }
70
83
  }
71
84
 
72
- async function releaseLock(lockPath: string): Promise<void> {
85
+ async function releaseLock(lockPath: string, expectedToken?: string): Promise<void> {
73
86
  try {
87
+ if (expectedToken !== undefined) {
88
+ const info = await readLockInfo(lockPath);
89
+ if (!info || info.token !== expectedToken) {
90
+ // We are not the owner. The lock either expired and was reaped
91
+ // or another process has reclaimed it. Do nothing — releasing
92
+ // here would wipe the rightful owner's lock.
93
+ logger.debug("file-lock: skipping release for non-owned lock", {
94
+ lockPath,
95
+ expectedToken,
96
+ actualToken: info?.token,
97
+ });
98
+ return;
99
+ }
100
+ }
74
101
  await fs.rm(lockPath, { recursive: true });
75
102
  } catch {
76
- // Ignore errors on release
103
+ // Ignore errors on release.
77
104
  }
78
105
  }
79
106
 
@@ -92,11 +119,14 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
92
119
  const lockPath = getLockPath(filePath);
93
120
 
94
121
  for (let attempt = 0; attempt < opts.retries; attempt++) {
95
- if (await tryAcquireLock(lockPath)) {
96
- return () => releaseLock(lockPath);
122
+ const token = await tryAcquireLock(lockPath);
123
+ if (token !== null) {
124
+ return () => releaseLock(lockPath, token);
97
125
  }
98
126
 
99
127
  if ((await lockExists(lockPath)) && (await isLockStale(lockPath, opts.staleMs))) {
128
+ // Reaping a stale lock — no token because we didn't acquire it. The
129
+ // rightful owner is presumed dead; rm without ownership check.
100
130
  await releaseLock(lockPath);
101
131
  continue;
102
132
  }
@@ -119,3 +149,16 @@ export async function withFileLock<T>(
119
149
  await release();
120
150
  }
121
151
  }
152
+
153
+ /**
154
+ * Test-only handles for the internal lock primitives. These are NOT part of
155
+ * the public API — they exist so the contract tests can validate token-keyed
156
+ * release semantics and the mkdir-race window without re-implementing them.
157
+ */
158
+ export const __internalsForTesting = {
159
+ tryAcquireLock,
160
+ releaseLock,
161
+ readLockInfo,
162
+ isLockStale,
163
+ getLockPath,
164
+ };
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import {
4
4
  type Keybinding,
@@ -10,6 +10,7 @@ import {
10
10
  KeybindingsManager as TuiKeybindingsManager,
11
11
  } from "@oh-my-pi/pi-tui";
12
12
  import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
13
+ import { YAML } from "bun";
13
14
 
14
15
  /**
15
16
  * Application-level keybindings (coding agent specific).
@@ -330,17 +331,29 @@ function orderKeybindingsConfig(config: KeybindingsConfig): KeybindingsConfig {
330
331
  return ordered;
331
332
  }
332
333
 
334
+ const KEYBINDINGS_YML = "keybindings.yml";
335
+ const KEYBINDINGS_YAML = "keybindings.yaml";
336
+ const LEGACY_KEYBINDINGS_JSON = "keybindings.json";
337
+
338
+ interface KeybindingsConfigPaths {
339
+ readPath: string;
340
+ writeBackPath: string;
341
+ }
342
+
333
343
  /**
334
344
  * Load raw config from a file synchronously.
335
- * Returns parsed JSON or null if file doesn't exist or is invalid.
345
+ * Returns parsed JSON/YAML or null if file doesn't exist or is invalid.
336
346
  */
337
347
  function loadRawConfig(filePath: string): unknown {
338
348
  try {
339
- if (!existsSync(filePath)) {
340
- return null;
349
+ const content = fs.readFileSync(filePath, "utf-8");
350
+ if (filePath.endsWith(".json")) {
351
+ return JSON.parse(content);
352
+ }
353
+ if (filePath.endsWith(".yml") || filePath.endsWith(".yaml")) {
354
+ return YAML.parse(content);
341
355
  }
342
- const content = readFileSync(filePath, "utf-8");
343
- return JSON.parse(content);
356
+ throw new Error(`Unsupported keybindings config extension: ${filePath}`);
344
357
  } catch (error) {
345
358
  if (isEnoent(error)) {
346
359
  return null;
@@ -350,34 +363,67 @@ function loadRawConfig(filePath: string): unknown {
350
363
  }
351
364
  }
352
365
 
366
+ function writeKeybindingsConfig(filePath: string, config: KeybindingsConfig): boolean {
367
+ try {
368
+ fs.writeFileSync(filePath, YAML.stringify(config, null, 2), "utf-8");
369
+ logger.debug("Migrated keybindings config", { path: filePath });
370
+ return true;
371
+ } catch (error) {
372
+ logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
373
+ return false;
374
+ }
375
+ }
376
+
377
+ function resolveKeybindingsConfigPaths(agentDir: string): KeybindingsConfigPaths {
378
+ const ymlPath = path.join(agentDir, KEYBINDINGS_YML);
379
+ if (fs.existsSync(ymlPath)) {
380
+ return { readPath: ymlPath, writeBackPath: ymlPath };
381
+ }
382
+
383
+ const yamlPath = path.join(agentDir, KEYBINDINGS_YAML);
384
+ if (fs.existsSync(yamlPath)) {
385
+ return { readPath: yamlPath, writeBackPath: yamlPath };
386
+ }
387
+
388
+ const jsonPath = path.join(agentDir, LEGACY_KEYBINDINGS_JSON);
389
+ if (fs.existsSync(jsonPath)) {
390
+ return { readPath: jsonPath, writeBackPath: ymlPath };
391
+ }
392
+
393
+ return { readPath: ymlPath, writeBackPath: ymlPath };
394
+ }
395
+
353
396
  /**
354
- * Migrate keybindings config file from old format to new.
355
- * Reads from agentDir/keybindings.json, migrates old names, and writes back.
397
+ * Load and migrate keybindings config.
398
+ * Legacy JSON is read for compatibility, but successful write-back goes to YAML.
356
399
  */
357
- function loadKeybindingsConfig(filePath: string, writeBack: boolean): KeybindingsConfig {
400
+ function loadKeybindingsConfig(
401
+ filePath: string,
402
+ writeBackPath: string | undefined,
403
+ ): {
404
+ config: KeybindingsConfig;
405
+ persistedPath: string;
406
+ } {
358
407
  const rawConfig = loadRawConfig(filePath);
359
408
 
360
409
  if (rawConfig === null) {
361
- return {};
410
+ return { config: {}, persistedPath: filePath };
362
411
  }
363
412
 
364
413
  const { config: migratedConfig, migrated } = migrateKeybindingNames(rawConfig);
365
- if (writeBack && migrated) {
414
+ const shouldWriteBack = writeBackPath !== undefined && (migrated || writeBackPath !== filePath);
415
+ if (shouldWriteBack) {
366
416
  const ordered = orderKeybindingsConfig(migratedConfig);
367
- try {
368
- writeFileSync(filePath, `${JSON.stringify(ordered, null, 2)}\n`, "utf-8");
369
- logger.debug("Migrated keybindings config", { path: filePath });
370
- } catch (error) {
371
- logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
372
- }
417
+ const persistedPath = writeKeybindingsConfig(writeBackPath, ordered) ? writeBackPath : filePath;
418
+ return { config: migratedConfig, persistedPath };
373
419
  }
374
420
 
375
- return migratedConfig;
421
+ return { config: migratedConfig, persistedPath: filePath };
376
422
  }
377
423
 
378
424
  function migrateKeybindingsConfigFile(agentDir: string): void {
379
- const configPath = path.join(agentDir, "keybindings.json");
380
- loadKeybindingsConfig(configPath, true);
425
+ const { readPath, writeBackPath } = resolveKeybindingsConfigPaths(agentDir);
426
+ loadKeybindingsConfig(readPath, writeBackPath);
381
427
  }
382
428
 
383
429
  /**
@@ -393,12 +439,13 @@ export class KeybindingsManager extends TuiKeybindingsManager {
393
439
  }
394
440
 
395
441
  /**
396
- * Create from config file at agentDir/keybindings.json.
442
+ * Create from config file at agentDir/keybindings.yml.
443
+ * Legacy keybindings.json is migrated to keybindings.yml on load.
397
444
  */
398
445
  static create(agentDir: string = getAgentDir()): KeybindingsManager {
399
- const configPath = path.join(agentDir, "keybindings.json");
400
- const userBindings = KeybindingsManager.#loadFromFile(configPath);
401
- const manager = new KeybindingsManager(userBindings, configPath);
446
+ const { readPath, writeBackPath } = resolveKeybindingsConfigPaths(agentDir);
447
+ const { config: userBindings, persistedPath } = KeybindingsManager.#loadFromFile(readPath, writeBackPath);
448
+ const manager = new KeybindingsManager(userBindings, persistedPath);
402
449
  // Set globally so getKeybindings() returns this manager
403
450
  setKeybindings(manager);
404
451
  return manager;
@@ -416,7 +463,8 @@ export class KeybindingsManager extends TuiKeybindingsManager {
416
463
  */
417
464
  reload(): void {
418
465
  if (!this.#configPath) return;
419
- this.setUserBindings(KeybindingsManager.#loadFromFile(this.#configPath));
466
+ const { config } = KeybindingsManager.#loadFromFile(this.#configPath);
467
+ this.setUserBindings(config);
420
468
  }
421
469
 
422
470
  /**
@@ -437,8 +485,11 @@ export class KeybindingsManager extends TuiKeybindingsManager {
437
485
  /**
438
486
  * Load user bindings from a file, migrating old names if needed.
439
487
  */
440
- static #loadFromFile(filePath: string): KeybindingsConfig {
441
- return loadKeybindingsConfig(filePath, true);
488
+ static #loadFromFile(
489
+ filePath: string,
490
+ writeBackPath?: string,
491
+ ): { config: KeybindingsConfig; persistedPath: string } {
492
+ return loadKeybindingsConfig(filePath, writeBackPath);
442
493
  }
443
494
  }
444
495
 
@@ -851,6 +851,12 @@ export class ModelRegistry {
851
851
 
852
852
  /**
853
853
  * @param authStorage - Auth storage for API key resolution
854
+ *
855
+ * Sync constructor — eagerly loads bundled + cached models so tests and
856
+ * synchronous callers see a fully-populated registry immediately. Production
857
+ * boot paths SHOULD prefer {@link ModelRegistry.create} so the YAML/JSONC
858
+ * migration step lands off the event loop's hot path before the first
859
+ * `tryLoad()` runs.
854
860
  */
855
861
  constructor(
856
862
  readonly authStorage: AuthStorage,
@@ -866,7 +872,7 @@ export class ModelRegistry {
866
872
  }
867
873
  return undefined;
868
874
  });
869
- // Load models synchronously in constructor
875
+ // Load models synchronously in constructor.
870
876
  this.#loadModels();
871
877
  }
872
878
 
@@ -1136,12 +1136,13 @@ export const SETTINGS_SCHEMA = {
1136
1136
 
1137
1137
  "compaction.strategy": {
1138
1138
  type: "enum",
1139
- values: ["context-full", "handoff", "off"] as const,
1139
+ values: ["context-full", "handoff", "shake", "off"] as const,
1140
1140
  default: "context-full",
1141
1141
  ui: {
1142
1142
  tab: "context",
1143
1143
  label: "Compaction Strategy",
1144
- description: "Choose in-place context-full maintenance, auto-handoff, or disable auto maintenance (off)",
1144
+ description:
1145
+ "Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), or disable auto maintenance (off)",
1145
1146
  options: [
1146
1147
  {
1147
1148
  value: "context-full",
@@ -1149,6 +1150,11 @@ export const SETTINGS_SCHEMA = {
1149
1150
  description: "Summarize in-place and keep the current session",
1150
1151
  },
1151
1152
  { value: "handoff", label: "Handoff", description: "Generate handoff and continue in a new session" },
1153
+ {
1154
+ value: "shake",
1155
+ label: "Shake",
1156
+ description: "Drop heavy content (tool results + large blocks) in place; recover via artifact",
1157
+ },
1152
1158
  {
1153
1159
  value: "off",
1154
1160
  label: "Off",
@@ -1327,70 +1333,70 @@ export const SETTINGS_SCHEMA = {
1327
1333
  "memories.summaryInjectionTokenLimit": { type: "number", default: 5000 },
1328
1334
 
1329
1335
  // Memory backend selector — picks between local memories pipeline,
1330
- // Mnemosyne local SQLite, Hindsight remote memory, or off. Legacy
1336
+ // Mnemopi local SQLite, Hindsight remote memory, or off. Legacy
1331
1337
  // `memories.enabled` keeps gating the local backend; see config/settings.ts
1332
1338
  // migration for details.
1333
1339
  "memory.backend": {
1334
1340
  type: "enum",
1335
- values: ["off", "local", "hindsight", "mnemosyne"] as const,
1341
+ values: ["off", "local", "hindsight", "mnemopi"] as const,
1336
1342
  default: "off",
1337
1343
  ui: {
1338
1344
  tab: "memory",
1339
1345
  label: "Memory Backend",
1340
- description: "Off, local summary pipeline, Mnemosyne SQLite, or Hindsight remote memory",
1346
+ description: "Off, local summary pipeline, Mnemopi SQLite, or Hindsight remote memory",
1341
1347
  options: [
1342
1348
  { value: "off", label: "Off", description: "No memory subsystem runs" },
1343
1349
  { value: "local", label: "Local", description: "Local rollout summarisation pipeline (memory_summary.md)" },
1344
1350
  { value: "hindsight", label: "Hindsight", description: "Vectorize Hindsight remote memory service" },
1345
1351
  {
1346
- value: "mnemosyne",
1347
- label: "Mnemosyne",
1352
+ value: "mnemopi",
1353
+ label: "Mnemopi",
1348
1354
  description: "Local SQLite recall/retain backend with optional embeddings",
1349
1355
  },
1350
1356
  ],
1351
1357
  },
1352
1358
  },
1353
1359
 
1354
- // Mnemosyne local SQLite memory backend.
1355
- "mnemosyne.dbPath": {
1360
+ // Mnemopi local SQLite memory backend.
1361
+ "mnemopi.dbPath": {
1356
1362
  type: "string",
1357
1363
  default: undefined,
1358
1364
  ui: {
1359
1365
  tab: "memory",
1360
- label: "Mnemosyne DB Path",
1366
+ label: "Mnemopi DB Path",
1361
1367
  description: "Optional SQLite DB path. Defaults to the agent memories directory.",
1362
- condition: "mnemosyneActive",
1368
+ condition: "mnemopiActive",
1363
1369
  },
1364
1370
  },
1365
- "mnemosyne.bank": {
1371
+ "mnemopi.bank": {
1366
1372
  type: "string",
1367
1373
  default: undefined,
1368
1374
  ui: {
1369
1375
  tab: "memory",
1370
- label: "Mnemosyne Bank",
1376
+ label: "Mnemopi Bank",
1371
1377
  description: "Optional shared bank base name. Per-project modes derive project-local banks from it.",
1372
- condition: "mnemosyneActive",
1378
+ condition: "mnemopiActive",
1373
1379
  },
1374
1380
  },
1375
- "mnemosyne.scoping": {
1381
+ "mnemopi.scoping": {
1376
1382
  type: "enum",
1377
1383
  values: ["global", "per-project", "per-project-tagged"] as const,
1378
1384
  default: "per-project",
1379
1385
  ui: {
1380
1386
  tab: "memory",
1381
- label: "Mnemosyne Scoping",
1387
+ label: "Mnemopi Scoping",
1382
1388
  description:
1383
1389
  "global = one shared bank; per-project = isolated bank per cwd; per-project-tagged = project-local writes plus global recall visibility",
1384
1390
  options: [
1385
1391
  {
1386
1392
  value: "global",
1387
1393
  label: "Global",
1388
- description: "One shared Mnemosyne bank for every project",
1394
+ description: "One shared Mnemopi bank for every project",
1389
1395
  },
1390
1396
  {
1391
1397
  value: "per-project",
1392
1398
  label: "Per project",
1393
- description: "Project-local Mnemosyne bank per cwd basename",
1399
+ description: "Project-local Mnemopi bank per cwd basename",
1394
1400
  },
1395
1401
  {
1396
1402
  value: "per-project-tagged",
@@ -1398,121 +1404,121 @@ export const SETTINGS_SCHEMA = {
1398
1404
  description: "Write to a project-local bank but merge project + shared recall results",
1399
1405
  },
1400
1406
  ],
1401
- condition: "mnemosyneActive",
1407
+ condition: "mnemopiActive",
1402
1408
  },
1403
1409
  },
1404
- "mnemosyne.autoRecall": {
1410
+ "mnemopi.autoRecall": {
1405
1411
  type: "boolean",
1406
1412
  default: true,
1407
1413
  ui: {
1408
1414
  tab: "memory",
1409
- label: "Mnemosyne Auto Recall",
1415
+ label: "Mnemopi Auto Recall",
1410
1416
  description: "Recall local memories into the first turn of each session",
1411
- condition: "mnemosyneActive",
1417
+ condition: "mnemopiActive",
1412
1418
  },
1413
1419
  },
1414
- "mnemosyne.autoRetain": {
1420
+ "mnemopi.autoRetain": {
1415
1421
  type: "boolean",
1416
1422
  default: true,
1417
1423
  ui: {
1418
1424
  tab: "memory",
1419
- label: "Mnemosyne Auto Retain",
1420
- description: "Retain completed conversation turns into local Mnemosyne memory",
1421
- condition: "mnemosyneActive",
1425
+ label: "Mnemopi Auto Retain",
1426
+ description: "Retain completed conversation turns into local Mnemopi memory",
1427
+ condition: "mnemopiActive",
1422
1428
  },
1423
1429
  },
1424
- "mnemosyne.noEmbeddings": {
1430
+ "mnemopi.noEmbeddings": {
1425
1431
  type: "boolean",
1426
1432
  default: false,
1427
1433
  ui: {
1428
1434
  tab: "memory",
1429
- label: "Mnemosyne Disable Embeddings",
1435
+ label: "Mnemopi Disable Embeddings",
1430
1436
  description: "Force deterministic FTS-only recall instead of vector embeddings",
1431
- condition: "mnemosyneActive",
1437
+ condition: "mnemopiActive",
1432
1438
  },
1433
1439
  },
1434
- "mnemosyne.embeddingModel": {
1440
+ "mnemopi.embeddingModel": {
1435
1441
  type: "string",
1436
1442
  default: undefined,
1437
1443
  ui: {
1438
1444
  tab: "memory",
1439
- label: "Mnemosyne Embedding Model",
1440
- description: "Optional embedding model override passed to Mnemosyne",
1441
- condition: "mnemosyneActive",
1445
+ label: "Mnemopi Embedding Model",
1446
+ description: "Optional embedding model override passed to Mnemopi",
1447
+ condition: "mnemopiActive",
1442
1448
  },
1443
1449
  },
1444
- "mnemosyne.embeddingApiUrl": {
1450
+ "mnemopi.embeddingApiUrl": {
1445
1451
  type: "string",
1446
1452
  default: undefined,
1447
1453
  ui: {
1448
1454
  tab: "memory",
1449
- label: "Mnemosyne Embedding API URL",
1450
- description: "Optional OpenAI-compatible embedding endpoint passed to Mnemosyne",
1451
- condition: "mnemosyneActive",
1455
+ label: "Mnemopi Embedding API URL",
1456
+ description: "Optional OpenAI-compatible embedding endpoint passed to Mnemopi",
1457
+ condition: "mnemopiActive",
1452
1458
  },
1453
1459
  },
1454
- "mnemosyne.embeddingApiKey": {
1460
+ "mnemopi.embeddingApiKey": {
1455
1461
  type: "string",
1456
1462
  default: undefined,
1457
1463
  ui: {
1458
1464
  tab: "memory",
1459
- label: "Mnemosyne Embedding API Key",
1460
- description: "Optional embedding API key passed to Mnemosyne",
1461
- condition: "mnemosyneActive",
1465
+ label: "Mnemopi Embedding API Key",
1466
+ description: "Optional embedding API key passed to Mnemopi",
1467
+ condition: "mnemopiActive",
1462
1468
  },
1463
1469
  },
1464
- "mnemosyne.llmMode": {
1470
+ "mnemopi.llmMode": {
1465
1471
  type: "enum",
1466
1472
  values: ["none", "smol", "remote"] as const,
1467
1473
  default: "smol",
1468
1474
  ui: {
1469
1475
  tab: "memory",
1470
- label: "Mnemosyne LLM Mode",
1476
+ label: "Mnemopi LLM Mode",
1471
1477
  description: "Use no LLM, the configured smol model, or a remote OpenAI-compatible endpoint",
1472
- condition: "mnemosyneActive",
1478
+ condition: "mnemopiActive",
1473
1479
  options: [
1474
- { value: "none", label: "None", description: "Disable Mnemosyne LLM-backed extraction" },
1480
+ { value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
1475
1481
  { value: "smol", label: "Smol", description: "Use the configured pi-ai smol model" },
1476
- { value: "remote", label: "Remote", description: "Use the Mnemosyne remote LLM settings below" },
1482
+ { value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
1477
1483
  ],
1478
1484
  },
1479
1485
  },
1480
- "mnemosyne.llmBaseUrl": {
1486
+ "mnemopi.llmBaseUrl": {
1481
1487
  type: "string",
1482
1488
  default: undefined,
1483
1489
  ui: {
1484
1490
  tab: "memory",
1485
- label: "Mnemosyne LLM Base URL",
1486
- description: "Optional OpenAI-compatible LLM endpoint for Mnemosyne remote mode",
1487
- condition: "mnemosyneActive",
1491
+ label: "Mnemopi LLM Base URL",
1492
+ description: "Optional OpenAI-compatible LLM endpoint for Mnemopi remote mode",
1493
+ condition: "mnemopiActive",
1488
1494
  },
1489
1495
  },
1490
- "mnemosyne.llmApiKey": {
1496
+ "mnemopi.llmApiKey": {
1491
1497
  type: "string",
1492
1498
  default: undefined,
1493
1499
  ui: {
1494
1500
  tab: "memory",
1495
- label: "Mnemosyne LLM API Key",
1496
- description: "Optional LLM API key for Mnemosyne remote mode",
1497
- condition: "mnemosyneActive",
1501
+ label: "Mnemopi LLM API Key",
1502
+ description: "Optional LLM API key for Mnemopi remote mode",
1503
+ condition: "mnemopiActive",
1498
1504
  },
1499
1505
  },
1500
- "mnemosyne.llmModel": {
1506
+ "mnemopi.llmModel": {
1501
1507
  type: "string",
1502
1508
  default: undefined,
1503
1509
  ui: {
1504
1510
  tab: "memory",
1505
- label: "Mnemosyne LLM Model",
1506
- description: "Optional LLM model name for Mnemosyne remote mode",
1507
- condition: "mnemosyneActive",
1511
+ label: "Mnemopi LLM Model",
1512
+ description: "Optional LLM model name for Mnemopi remote mode",
1513
+ condition: "mnemopiActive",
1508
1514
  },
1509
1515
  },
1510
- "mnemosyne.retainEveryNTurns": { type: "number", default: 4 },
1511
- "mnemosyne.recallLimit": { type: "number", default: 8 },
1512
- "mnemosyne.recallContextTurns": { type: "number", default: 3 },
1513
- "mnemosyne.recallMaxQueryChars": { type: "number", default: 4000 },
1514
- "mnemosyne.injectionTokenLimit": { type: "number", default: 5000 },
1515
- "mnemosyne.debug": { type: "boolean", default: false },
1516
+ "mnemopi.retainEveryNTurns": { type: "number", default: 4 },
1517
+ "mnemopi.recallLimit": { type: "number", default: 8 },
1518
+ "mnemopi.recallContextTurns": { type: "number", default: 3 },
1519
+ "mnemopi.recallMaxQueryChars": { type: "number", default: 4000 },
1520
+ "mnemopi.injectionTokenLimit": { type: "number", default: 5000 },
1521
+ "mnemopi.debug": { type: "boolean", default: false },
1516
1522
 
1517
1523
  // Hindsight (https://hindsight.vectorize.io)
1518
1524
  "hindsight.apiUrl": {
@@ -2984,8 +2990,8 @@ export const SETTINGS_SCHEMA = {
2984
2990
  tab: "memory",
2985
2991
  label: "Memory Model",
2986
2992
  description:
2987
- "Mnemosyne LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
2988
- condition: "mnemosyneActive",
2993
+ "Mnemopi LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
2994
+ condition: "mnemopiActive",
2989
2995
  options: TINY_MEMORY_MODEL_OPTIONS,
2990
2996
  },
2991
2997
  },
@@ -3292,7 +3298,7 @@ export type TreeFilterMode = SettingValue<"treeFilterMode">;
3292
3298
 
3293
3299
  export interface CompactionSettings {
3294
3300
  enabled: boolean;
3295
- strategy: "context-full" | "handoff" | "off";
3301
+ strategy: "context-full" | "handoff" | "shake" | "off";
3296
3302
  thresholdPercent: number;
3297
3303
  thresholdTokens: number;
3298
3304
  reserveTokens: number;
@@ -663,6 +663,16 @@ export class Settings {
663
663
  raw["edit.mode"] = "hashline";
664
664
  }
665
665
 
666
+ // compaction.strategy: removed local-model shake-summary mode; plain shake
667
+ // keeps the same mechanical artifact-backed reduction without background CPU.
668
+ const compactionObj = raw.compaction as Record<string, unknown> | undefined;
669
+ if (compactionObj?.strategy === "shake-summary") {
670
+ compactionObj.strategy = "shake";
671
+ }
672
+ if (raw["compaction.strategy"] === "shake-summary") {
673
+ raw["compaction.strategy"] = "shake";
674
+ }
675
+
666
676
  // statusLine: rename "plan_mode" segment to "mode"
667
677
  const statusLineObj = raw.statusLine as Record<string, unknown> | undefined;
668
678
  if (statusLineObj) {
@@ -692,6 +702,18 @@ export class Settings {
692
702
  raw.memory = memoryRoot;
693
703
  }
694
704
 
705
+ // Rename the legacy local `mnemosyne` memory backend to `mnemopi`.
706
+ // - `memory.backend: "mnemosyne"` now selects the renamed backend.
707
+ // - the top-level `mnemosyne` settings object becomes `mnemopi`.
708
+ // Idempotent: skips the object move once `mnemopi` is materialised.
709
+ if (memoryBackendObj && memoryBackendObj.backend === "mnemosyne") {
710
+ memoryBackendObj.backend = "mnemopi";
711
+ }
712
+ if ("mnemosyne" in raw && !("mnemopi" in raw)) {
713
+ raw.mnemopi = raw.mnemosyne;
714
+ delete raw.mnemosyne;
715
+ }
716
+
695
717
  // hindsight: dynamicBankId/agentName -> scoping enum + bankId
696
718
  // - dynamicBankId=true → scoping="per-project" (closest semantic match;
697
719
  // the legacy `agent::project::channel::user` tuple was per-project in