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

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 (160) hide show
  1. package/CHANGELOG.md +75 -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 +88 -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/memory-backend/index.d.ts +1 -1
  27. package/dist/types/memory-backend/resolve.d.ts +1 -1
  28. package/dist/types/memory-backend/types.d.ts +3 -3
  29. package/dist/types/mnemopi/backend.d.ts +4 -0
  30. package/dist/types/mnemopi/config.d.ts +29 -0
  31. package/dist/types/mnemopi/state.d.ts +72 -0
  32. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  33. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  34. package/dist/types/modes/controllers/command-controller.d.ts +7 -0
  35. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  36. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  37. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  38. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  39. package/dist/types/modes/interactive-mode.d.ts +7 -3
  40. package/dist/types/modes/magic-keywords.d.ts +14 -0
  41. package/dist/types/modes/markdown-prose.d.ts +27 -0
  42. package/dist/types/modes/orchestrate.d.ts +7 -2
  43. package/dist/types/modes/shared.d.ts +1 -1
  44. package/dist/types/modes/turn-budget.d.ts +18 -0
  45. package/dist/types/modes/types.d.ts +7 -3
  46. package/dist/types/modes/ultrathink.d.ts +7 -2
  47. package/dist/types/modes/workflow.d.ts +15 -0
  48. package/dist/types/sdk.d.ts +13 -3
  49. package/dist/types/session/agent-session.d.ts +40 -17
  50. package/dist/types/session/session-manager.d.ts +18 -0
  51. package/dist/types/session/session-storage.d.ts +6 -0
  52. package/dist/types/session/shake-types.d.ts +24 -0
  53. package/dist/types/task/executor.d.ts +2 -2
  54. package/dist/types/tiny/models.d.ts +15 -1
  55. package/dist/types/tiny/title-protocol.d.ts +4 -0
  56. package/dist/types/tools/index.d.ts +19 -3
  57. package/dist/types/tools/memory-edit.d.ts +1 -1
  58. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  59. package/package.json +10 -10
  60. package/src/autoresearch/tools/run-experiment.ts +45 -113
  61. package/src/cli/args.ts +39 -16
  62. package/src/cli/extension-flags.ts +48 -0
  63. package/src/cli/plugin-cli.ts +11 -2
  64. package/src/config/config-file.ts +98 -13
  65. package/src/config/file-lock.ts +60 -17
  66. package/src/config/keybindings.ts +78 -27
  67. package/src/config/model-registry.ts +7 -1
  68. package/src/config/settings-schema.ts +94 -67
  69. package/src/config/settings.ts +12 -0
  70. package/src/edit/hashline/diff.ts +81 -24
  71. package/src/edit/renderer.ts +16 -12
  72. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  73. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  74. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  75. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  76. package/src/eval/agent-bridge.ts +295 -0
  77. package/src/eval/backend.ts +17 -2
  78. package/src/eval/budget-bridge.ts +48 -0
  79. package/src/eval/idle-timeout.ts +80 -0
  80. package/src/eval/js/executor.ts +35 -7
  81. package/src/eval/js/index.ts +2 -1
  82. package/src/eval/js/shared/prelude.txt +85 -1
  83. package/src/eval/js/tool-bridge.ts +9 -0
  84. package/src/eval/py/executor.ts +41 -14
  85. package/src/eval/py/index.ts +2 -1
  86. package/src/eval/py/prelude.py +132 -1
  87. package/src/exec/bash-executor.ts +2 -3
  88. package/src/extensibility/custom-tools/types.ts +2 -2
  89. package/src/extensibility/extensions/runner.ts +12 -2
  90. package/src/extensibility/plugins/git-url.ts +90 -4
  91. package/src/extensibility/plugins/manager.ts +103 -7
  92. package/src/extensibility/shared-events.ts +2 -2
  93. package/src/internal-urls/docs-index.generated.ts +88 -88
  94. package/src/main.ts +44 -55
  95. package/src/memory-backend/index.ts +1 -1
  96. package/src/memory-backend/resolve.ts +3 -3
  97. package/src/memory-backend/types.ts +3 -3
  98. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  99. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  100. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  101. package/src/modes/components/agent-dashboard.ts +6 -6
  102. package/src/modes/components/custom-editor.ts +4 -11
  103. package/src/modes/components/extensions/state-manager.ts +3 -4
  104. package/src/modes/components/footer.ts +8 -9
  105. package/src/modes/components/hook-selector.ts +86 -20
  106. package/src/modes/components/oauth-selector.ts +93 -21
  107. package/src/modes/components/omfg-panel.ts +141 -0
  108. package/src/modes/components/settings-defs.ts +2 -2
  109. package/src/modes/components/tips.txt +2 -1
  110. package/src/modes/components/tool-execution.ts +38 -19
  111. package/src/modes/components/tree-selector.ts +4 -3
  112. package/src/modes/components/user-message-selector.ts +94 -19
  113. package/src/modes/components/user-message.ts +8 -1
  114. package/src/modes/controllers/command-controller.ts +57 -0
  115. package/src/modes/controllers/event-controller.ts +60 -2
  116. package/src/modes/controllers/input-controller.ts +14 -11
  117. package/src/modes/controllers/omfg-controller.ts +283 -0
  118. package/src/modes/controllers/omfg-rule.ts +647 -0
  119. package/src/modes/controllers/selector-controller.ts +1 -0
  120. package/src/modes/gradient-highlight.ts +23 -6
  121. package/src/modes/interactive-mode.ts +41 -7
  122. package/src/modes/magic-keywords.ts +20 -0
  123. package/src/modes/markdown-prose.ts +247 -0
  124. package/src/modes/orchestrate.ts +17 -11
  125. package/src/modes/shared.ts +3 -11
  126. package/src/modes/turn-budget.ts +31 -0
  127. package/src/modes/types.ts +7 -1
  128. package/src/modes/ultrathink.ts +16 -10
  129. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  130. package/src/modes/workflow.ts +42 -0
  131. package/src/prompts/system/omfg-user.md +51 -0
  132. package/src/prompts/system/system-prompt.md +1 -0
  133. package/src/prompts/system/workflow-notice.md +70 -0
  134. package/src/prompts/tools/eval.md +13 -1
  135. package/src/prompts/tools/memory-edit.md +1 -1
  136. package/src/sdk.ts +63 -33
  137. package/src/session/agent-session.ts +373 -56
  138. package/src/session/session-manager.ts +32 -0
  139. package/src/session/session-storage.ts +68 -8
  140. package/src/session/shake-types.ts +44 -0
  141. package/src/slash-commands/builtin-registry.ts +41 -16
  142. package/src/task/executor.ts +3 -3
  143. package/src/task/index.ts +6 -6
  144. package/src/tiny/models.ts +30 -2
  145. package/src/tiny/title-protocol.ts +11 -1
  146. package/src/tiny/worker.ts +19 -7
  147. package/src/tools/eval.ts +202 -26
  148. package/src/tools/grouped-file-output.ts +9 -2
  149. package/src/tools/index.ts +17 -5
  150. package/src/tools/memory-edit.ts +4 -4
  151. package/src/tools/memory-recall.ts +5 -5
  152. package/src/tools/memory-reflect.ts +5 -5
  153. package/src/tools/memory-retain.ts +4 -4
  154. package/src/tools/render-utils.ts +2 -1
  155. package/src/tools/search.ts +480 -76
  156. package/dist/types/mnemosyne/backend.d.ts +0 -4
  157. package/dist/types/mnemosyne/config.d.ts +0 -29
  158. package/dist/types/mnemosyne/state.d.ts +0 -72
  159. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  160. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -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
 
@@ -14,9 +14,12 @@ import {
14
14
  import {
15
15
  AUTO_THINKING_MODEL_OPTIONS,
16
16
  AUTO_THINKING_MODEL_VALUES,
17
+ DEFAULT_SHAKE_SUMMARY_MODEL_KEY,
17
18
  ONLINE_AUTO_THINKING_MODEL_KEY,
18
19
  ONLINE_MEMORY_MODEL_KEY,
19
20
  ONLINE_TINY_TITLE_MODEL_KEY,
21
+ SHAKE_SUMMARY_MODEL_OPTIONS,
22
+ SHAKE_SUMMARY_MODEL_VALUES,
20
23
  TINY_MEMORY_MODEL_OPTIONS,
21
24
  TINY_MEMORY_MODEL_VALUES,
22
25
  TINY_TITLE_MODEL_OPTIONS,
@@ -1136,12 +1139,13 @@ export const SETTINGS_SCHEMA = {
1136
1139
 
1137
1140
  "compaction.strategy": {
1138
1141
  type: "enum",
1139
- values: ["context-full", "handoff", "off"] as const,
1142
+ values: ["context-full", "handoff", "shake", "shake-summary", "off"] as const,
1140
1143
  default: "context-full",
1141
1144
  ui: {
1142
1145
  tab: "context",
1143
1146
  label: "Compaction Strategy",
1144
- description: "Choose in-place context-full maintenance, auto-handoff, or disable auto maintenance (off)",
1147
+ description:
1148
+ "Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), shake with local-model summaries, or disable auto maintenance (off)",
1145
1149
  options: [
1146
1150
  {
1147
1151
  value: "context-full",
@@ -1149,6 +1153,16 @@ export const SETTINGS_SCHEMA = {
1149
1153
  description: "Summarize in-place and keep the current session",
1150
1154
  },
1151
1155
  { value: "handoff", label: "Handoff", description: "Generate handoff and continue in a new session" },
1156
+ {
1157
+ value: "shake",
1158
+ label: "Shake",
1159
+ description: "Drop heavy content (tool results + large blocks) in place; recover via artifact",
1160
+ },
1161
+ {
1162
+ value: "shake-summary",
1163
+ label: "Shake (summary)",
1164
+ description: "Shake, but compress heavy regions with a local on-device model instead of dropping",
1165
+ },
1152
1166
  {
1153
1167
  value: "off",
1154
1168
  label: "Off",
@@ -1327,70 +1341,70 @@ export const SETTINGS_SCHEMA = {
1327
1341
  "memories.summaryInjectionTokenLimit": { type: "number", default: 5000 },
1328
1342
 
1329
1343
  // Memory backend selector — picks between local memories pipeline,
1330
- // Mnemosyne local SQLite, Hindsight remote memory, or off. Legacy
1344
+ // Mnemopi local SQLite, Hindsight remote memory, or off. Legacy
1331
1345
  // `memories.enabled` keeps gating the local backend; see config/settings.ts
1332
1346
  // migration for details.
1333
1347
  "memory.backend": {
1334
1348
  type: "enum",
1335
- values: ["off", "local", "hindsight", "mnemosyne"] as const,
1349
+ values: ["off", "local", "hindsight", "mnemopi"] as const,
1336
1350
  default: "off",
1337
1351
  ui: {
1338
1352
  tab: "memory",
1339
1353
  label: "Memory Backend",
1340
- description: "Off, local summary pipeline, Mnemosyne SQLite, or Hindsight remote memory",
1354
+ description: "Off, local summary pipeline, Mnemopi SQLite, or Hindsight remote memory",
1341
1355
  options: [
1342
1356
  { value: "off", label: "Off", description: "No memory subsystem runs" },
1343
1357
  { value: "local", label: "Local", description: "Local rollout summarisation pipeline (memory_summary.md)" },
1344
1358
  { value: "hindsight", label: "Hindsight", description: "Vectorize Hindsight remote memory service" },
1345
1359
  {
1346
- value: "mnemosyne",
1347
- label: "Mnemosyne",
1360
+ value: "mnemopi",
1361
+ label: "Mnemopi",
1348
1362
  description: "Local SQLite recall/retain backend with optional embeddings",
1349
1363
  },
1350
1364
  ],
1351
1365
  },
1352
1366
  },
1353
1367
 
1354
- // Mnemosyne local SQLite memory backend.
1355
- "mnemosyne.dbPath": {
1368
+ // Mnemopi local SQLite memory backend.
1369
+ "mnemopi.dbPath": {
1356
1370
  type: "string",
1357
1371
  default: undefined,
1358
1372
  ui: {
1359
1373
  tab: "memory",
1360
- label: "Mnemosyne DB Path",
1374
+ label: "Mnemopi DB Path",
1361
1375
  description: "Optional SQLite DB path. Defaults to the agent memories directory.",
1362
- condition: "mnemosyneActive",
1376
+ condition: "mnemopiActive",
1363
1377
  },
1364
1378
  },
1365
- "mnemosyne.bank": {
1379
+ "mnemopi.bank": {
1366
1380
  type: "string",
1367
1381
  default: undefined,
1368
1382
  ui: {
1369
1383
  tab: "memory",
1370
- label: "Mnemosyne Bank",
1384
+ label: "Mnemopi Bank",
1371
1385
  description: "Optional shared bank base name. Per-project modes derive project-local banks from it.",
1372
- condition: "mnemosyneActive",
1386
+ condition: "mnemopiActive",
1373
1387
  },
1374
1388
  },
1375
- "mnemosyne.scoping": {
1389
+ "mnemopi.scoping": {
1376
1390
  type: "enum",
1377
1391
  values: ["global", "per-project", "per-project-tagged"] as const,
1378
1392
  default: "per-project",
1379
1393
  ui: {
1380
1394
  tab: "memory",
1381
- label: "Mnemosyne Scoping",
1395
+ label: "Mnemopi Scoping",
1382
1396
  description:
1383
1397
  "global = one shared bank; per-project = isolated bank per cwd; per-project-tagged = project-local writes plus global recall visibility",
1384
1398
  options: [
1385
1399
  {
1386
1400
  value: "global",
1387
1401
  label: "Global",
1388
- description: "One shared Mnemosyne bank for every project",
1402
+ description: "One shared Mnemopi bank for every project",
1389
1403
  },
1390
1404
  {
1391
1405
  value: "per-project",
1392
1406
  label: "Per project",
1393
- description: "Project-local Mnemosyne bank per cwd basename",
1407
+ description: "Project-local Mnemopi bank per cwd basename",
1394
1408
  },
1395
1409
  {
1396
1410
  value: "per-project-tagged",
@@ -1398,121 +1412,121 @@ export const SETTINGS_SCHEMA = {
1398
1412
  description: "Write to a project-local bank but merge project + shared recall results",
1399
1413
  },
1400
1414
  ],
1401
- condition: "mnemosyneActive",
1415
+ condition: "mnemopiActive",
1402
1416
  },
1403
1417
  },
1404
- "mnemosyne.autoRecall": {
1418
+ "mnemopi.autoRecall": {
1405
1419
  type: "boolean",
1406
1420
  default: true,
1407
1421
  ui: {
1408
1422
  tab: "memory",
1409
- label: "Mnemosyne Auto Recall",
1423
+ label: "Mnemopi Auto Recall",
1410
1424
  description: "Recall local memories into the first turn of each session",
1411
- condition: "mnemosyneActive",
1425
+ condition: "mnemopiActive",
1412
1426
  },
1413
1427
  },
1414
- "mnemosyne.autoRetain": {
1428
+ "mnemopi.autoRetain": {
1415
1429
  type: "boolean",
1416
1430
  default: true,
1417
1431
  ui: {
1418
1432
  tab: "memory",
1419
- label: "Mnemosyne Auto Retain",
1420
- description: "Retain completed conversation turns into local Mnemosyne memory",
1421
- condition: "mnemosyneActive",
1433
+ label: "Mnemopi Auto Retain",
1434
+ description: "Retain completed conversation turns into local Mnemopi memory",
1435
+ condition: "mnemopiActive",
1422
1436
  },
1423
1437
  },
1424
- "mnemosyne.noEmbeddings": {
1438
+ "mnemopi.noEmbeddings": {
1425
1439
  type: "boolean",
1426
1440
  default: false,
1427
1441
  ui: {
1428
1442
  tab: "memory",
1429
- label: "Mnemosyne Disable Embeddings",
1443
+ label: "Mnemopi Disable Embeddings",
1430
1444
  description: "Force deterministic FTS-only recall instead of vector embeddings",
1431
- condition: "mnemosyneActive",
1445
+ condition: "mnemopiActive",
1432
1446
  },
1433
1447
  },
1434
- "mnemosyne.embeddingModel": {
1448
+ "mnemopi.embeddingModel": {
1435
1449
  type: "string",
1436
1450
  default: undefined,
1437
1451
  ui: {
1438
1452
  tab: "memory",
1439
- label: "Mnemosyne Embedding Model",
1440
- description: "Optional embedding model override passed to Mnemosyne",
1441
- condition: "mnemosyneActive",
1453
+ label: "Mnemopi Embedding Model",
1454
+ description: "Optional embedding model override passed to Mnemopi",
1455
+ condition: "mnemopiActive",
1442
1456
  },
1443
1457
  },
1444
- "mnemosyne.embeddingApiUrl": {
1458
+ "mnemopi.embeddingApiUrl": {
1445
1459
  type: "string",
1446
1460
  default: undefined,
1447
1461
  ui: {
1448
1462
  tab: "memory",
1449
- label: "Mnemosyne Embedding API URL",
1450
- description: "Optional OpenAI-compatible embedding endpoint passed to Mnemosyne",
1451
- condition: "mnemosyneActive",
1463
+ label: "Mnemopi Embedding API URL",
1464
+ description: "Optional OpenAI-compatible embedding endpoint passed to Mnemopi",
1465
+ condition: "mnemopiActive",
1452
1466
  },
1453
1467
  },
1454
- "mnemosyne.embeddingApiKey": {
1468
+ "mnemopi.embeddingApiKey": {
1455
1469
  type: "string",
1456
1470
  default: undefined,
1457
1471
  ui: {
1458
1472
  tab: "memory",
1459
- label: "Mnemosyne Embedding API Key",
1460
- description: "Optional embedding API key passed to Mnemosyne",
1461
- condition: "mnemosyneActive",
1473
+ label: "Mnemopi Embedding API Key",
1474
+ description: "Optional embedding API key passed to Mnemopi",
1475
+ condition: "mnemopiActive",
1462
1476
  },
1463
1477
  },
1464
- "mnemosyne.llmMode": {
1478
+ "mnemopi.llmMode": {
1465
1479
  type: "enum",
1466
1480
  values: ["none", "smol", "remote"] as const,
1467
1481
  default: "smol",
1468
1482
  ui: {
1469
1483
  tab: "memory",
1470
- label: "Mnemosyne LLM Mode",
1484
+ label: "Mnemopi LLM Mode",
1471
1485
  description: "Use no LLM, the configured smol model, or a remote OpenAI-compatible endpoint",
1472
- condition: "mnemosyneActive",
1486
+ condition: "mnemopiActive",
1473
1487
  options: [
1474
- { value: "none", label: "None", description: "Disable Mnemosyne LLM-backed extraction" },
1488
+ { value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
1475
1489
  { 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" },
1490
+ { value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
1477
1491
  ],
1478
1492
  },
1479
1493
  },
1480
- "mnemosyne.llmBaseUrl": {
1494
+ "mnemopi.llmBaseUrl": {
1481
1495
  type: "string",
1482
1496
  default: undefined,
1483
1497
  ui: {
1484
1498
  tab: "memory",
1485
- label: "Mnemosyne LLM Base URL",
1486
- description: "Optional OpenAI-compatible LLM endpoint for Mnemosyne remote mode",
1487
- condition: "mnemosyneActive",
1499
+ label: "Mnemopi LLM Base URL",
1500
+ description: "Optional OpenAI-compatible LLM endpoint for Mnemopi remote mode",
1501
+ condition: "mnemopiActive",
1488
1502
  },
1489
1503
  },
1490
- "mnemosyne.llmApiKey": {
1504
+ "mnemopi.llmApiKey": {
1491
1505
  type: "string",
1492
1506
  default: undefined,
1493
1507
  ui: {
1494
1508
  tab: "memory",
1495
- label: "Mnemosyne LLM API Key",
1496
- description: "Optional LLM API key for Mnemosyne remote mode",
1497
- condition: "mnemosyneActive",
1509
+ label: "Mnemopi LLM API Key",
1510
+ description: "Optional LLM API key for Mnemopi remote mode",
1511
+ condition: "mnemopiActive",
1498
1512
  },
1499
1513
  },
1500
- "mnemosyne.llmModel": {
1514
+ "mnemopi.llmModel": {
1501
1515
  type: "string",
1502
1516
  default: undefined,
1503
1517
  ui: {
1504
1518
  tab: "memory",
1505
- label: "Mnemosyne LLM Model",
1506
- description: "Optional LLM model name for Mnemosyne remote mode",
1507
- condition: "mnemosyneActive",
1519
+ label: "Mnemopi LLM Model",
1520
+ description: "Optional LLM model name for Mnemopi remote mode",
1521
+ condition: "mnemopiActive",
1508
1522
  },
1509
1523
  },
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 },
1524
+ "mnemopi.retainEveryNTurns": { type: "number", default: 4 },
1525
+ "mnemopi.recallLimit": { type: "number", default: 8 },
1526
+ "mnemopi.recallContextTurns": { type: "number", default: 3 },
1527
+ "mnemopi.recallMaxQueryChars": { type: "number", default: 4000 },
1528
+ "mnemopi.injectionTokenLimit": { type: "number", default: 5000 },
1529
+ "mnemopi.debug": { type: "boolean", default: false },
1516
1530
 
1517
1531
  // Hindsight (https://hindsight.vectorize.io)
1518
1532
  "hindsight.apiUrl": {
@@ -2984,8 +2998,8 @@ export const SETTINGS_SCHEMA = {
2984
2998
  tab: "memory",
2985
2999
  label: "Memory Model",
2986
3000
  description:
2987
- "Mnemosyne LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
2988
- condition: "mnemosyneActive",
3001
+ "Mnemopi LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
3002
+ condition: "mnemopiActive",
2989
3003
  options: TINY_MEMORY_MODEL_OPTIONS,
2990
3004
  },
2991
3005
  },
@@ -3004,6 +3018,19 @@ export const SETTINGS_SCHEMA = {
3004
3018
  },
3005
3019
  },
3006
3020
 
3021
+ "providers.shakeSummaryModel": {
3022
+ type: "enum",
3023
+ values: SHAKE_SUMMARY_MODEL_VALUES,
3024
+ default: DEFAULT_SHAKE_SUMMARY_MODEL_KEY,
3025
+ ui: {
3026
+ tab: "context",
3027
+ label: "Shake Summary Model",
3028
+ description:
3029
+ "Local on-device model used by /shake summary and the shake-summary compaction strategy to compress heavy regions. Runs entirely on-device; downloads on first use. Falls back to plain elide when unavailable.",
3030
+ options: SHAKE_SUMMARY_MODEL_OPTIONS,
3031
+ },
3032
+ },
3033
+
3007
3034
  "providers.kimiApiFormat": {
3008
3035
  type: "enum",
3009
3036
  values: ["openai", "anthropic"] as const,
@@ -3292,7 +3319,7 @@ export type TreeFilterMode = SettingValue<"treeFilterMode">;
3292
3319
 
3293
3320
  export interface CompactionSettings {
3294
3321
  enabled: boolean;
3295
- strategy: "context-full" | "handoff" | "off";
3322
+ strategy: "context-full" | "handoff" | "shake" | "shake-summary" | "off";
3296
3323
  thresholdPercent: number;
3297
3324
  thresholdTokens: number;
3298
3325
  reserveTokens: number;
@@ -692,6 +692,18 @@ export class Settings {
692
692
  raw.memory = memoryRoot;
693
693
  }
694
694
 
695
+ // Rename the legacy local `mnemosyne` memory backend to `mnemopi`.
696
+ // - `memory.backend: "mnemosyne"` now selects the renamed backend.
697
+ // - the top-level `mnemosyne` settings object becomes `mnemopi`.
698
+ // Idempotent: skips the object move once `mnemopi` is materialised.
699
+ if (memoryBackendObj && memoryBackendObj.backend === "mnemosyne") {
700
+ memoryBackendObj.backend = "mnemopi";
701
+ }
702
+ if ("mnemosyne" in raw && !("mnemopi" in raw)) {
703
+ raw.mnemopi = raw.mnemosyne;
704
+ delete raw.mnemosyne;
705
+ }
706
+
695
707
  // hindsight: dynamicBankId/agentName -> scoping enum + bankId
696
708
  // - dynamicBankId=true → scoping="per-project" (closest semantic match;
697
709
  // the legacy `agent::project::channel::user` tuple was per-project in
@@ -5,17 +5,25 @@
5
5
  * pair to {@link generateDiffString} so the renderer can show the diff
6
6
  * while the tool call is still streaming.
7
7
  *
8
- * Validation is intentionally light: only the section snapshot tag is checked
9
- * (so the preview goes red when anchors are stale), no plan-mode guards
10
- * and no auto-generated-file refusal those belong on the write path.
8
+ * Uses the same snapshot-tag semantics as the apply path: a live content-hash
9
+ * match is accepted even when the tag was minted by a source that did not keep
10
+ * history, and stale tags recover through the session snapshot store when possible.
11
11
  */
12
12
  import {
13
+ type ApplyResult,
14
+ applyEdits,
15
+ computeFileHash,
16
+ type Edit,
13
17
  Patch as HashlinePatch,
18
+ hasBlockEdit,
19
+ MismatchError,
14
20
  missingSnapshotTagMessage,
15
21
  normalizeToLF,
16
22
  type Patch,
17
23
  type PatchSection,
18
- type Snapshot,
24
+ parsePatchStreaming,
25
+ Recovery,
26
+ resolveBlockEdits,
19
27
  type SnapshotStore,
20
28
  stripBom,
21
29
  } from "@oh-my-pi/hashline";
@@ -48,24 +56,79 @@ async function readSectionText(absolutePath: string, sectionPath: string): Promi
48
56
  }
49
57
  }
50
58
 
51
- function snapshotMatchesCurrent(snapshot: Snapshot, currentText: string): boolean {
52
- return snapshot.text === currentText;
59
+ function hasAnchorScopedEdit(edits: readonly Edit[]): boolean {
60
+ return edits.some(edit => {
61
+ if (edit.kind === "delete") return true;
62
+ if (edit.kind === "block") return true;
63
+ return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
64
+ });
53
65
  }
54
- function validateSectionHash(
66
+
67
+ function createMismatchError(
55
68
  section: PatchSection,
56
69
  absolutePath: string,
57
- text: string,
70
+ normalized: string,
58
71
  snapshots: SnapshotStore,
59
- ): string | null {
60
- if (section.fileHash === undefined) {
61
- // The snapshot tag is mandatory on every section — head/tail inserts
62
- // included — to keep this preview path in lockstep with the apply path
63
- // (`Patcher.prepare`), which rejects tagless sections unconditionally.
64
- return missingSnapshotTagMessage(section.path);
72
+ expected: string,
73
+ ): MismatchError {
74
+ return new MismatchError({
75
+ path: section.path,
76
+ expectedFileHash: expected,
77
+ actualFileHash: computeFileHash(normalized),
78
+ fileLines: normalized.split("\n"),
79
+ anchorLines: section.collectAnchorLines(),
80
+ hashRecognized: snapshots.byHash(absolutePath, expected) !== null,
81
+ });
82
+ }
83
+
84
+ function parsePreviewEdits(section: PatchSection, streaming: boolean | undefined): readonly Edit[] {
85
+ return streaming ? parsePatchStreaming(section.diff).edits : section.edits;
86
+ }
87
+
88
+ function resolvePreviewEdits(args: {
89
+ section: PatchSection;
90
+ absolutePath: string;
91
+ normalized: string;
92
+ snapshots: SnapshotStore;
93
+ expected: string | undefined;
94
+ liveMatches: boolean;
95
+ edits: readonly Edit[];
96
+ }): readonly Edit[] {
97
+ const { section, absolutePath, normalized, snapshots, expected, liveMatches, edits } = args;
98
+ if (!hasBlockEdit(edits)) return edits;
99
+ const baseText = expected === undefined || liveMatches ? normalized : snapshots.byHash(absolutePath, expected)?.text;
100
+ if (baseText === undefined) {
101
+ throw createMismatchError(section, absolutePath, normalized, snapshots, expected ?? "");
65
102
  }
66
- const snapshot = snapshots.byHash(absolutePath, section.fileHash);
67
- if (snapshot && snapshotMatchesCurrent(snapshot, text)) return null;
68
- return `Hashline snapshot tag mismatch for ${section.path}: section is bound to #${section.fileHash}, but current file does not match that snapshot; re-read and try again.`;
103
+ return resolveBlockEdits(edits, baseText, section.path, nativeBlockResolver, { onUnresolved: "throw" });
104
+ }
105
+
106
+ function applyPreviewEdits(args: {
107
+ section: PatchSection;
108
+ absolutePath: string;
109
+ normalized: string;
110
+ snapshots: SnapshotStore;
111
+ options: HashlineDiffOptions;
112
+ }): ApplyResult {
113
+ const { section, absolutePath, normalized, snapshots, options } = args;
114
+ const expected = section.fileHash;
115
+ if (!options.skipHashValidation && expected === undefined) {
116
+ throw new Error(missingSnapshotTagMessage(section.path));
117
+ }
118
+ const liveMatches = expected !== undefined && computeFileHash(normalized) === expected;
119
+ const edits = parsePreviewEdits(section, options.streaming);
120
+ const resolved = resolvePreviewEdits({ section, absolutePath, normalized, snapshots, expected, liveMatches, edits });
121
+ if (options.skipHashValidation || expected === undefined || liveMatches) return applyEdits(normalized, resolved);
122
+ if (!hasAnchorScopedEdit(resolved)) return applyEdits(normalized, resolved);
123
+
124
+ const recovered = new Recovery(snapshots).tryRecover({
125
+ path: absolutePath,
126
+ currentText: normalized,
127
+ fileHash: expected,
128
+ edits: resolved,
129
+ });
130
+ if (recovered) return recovered;
131
+ throw createMismatchError(section, absolutePath, normalized, snapshots, expected);
69
132
  }
70
133
 
71
134
  export async function computeHashlineSectionDiff(
@@ -79,13 +142,7 @@ export async function computeHashlineSectionDiff(
79
142
  const rawContent = await readSectionText(absolutePath, section.path);
80
143
  const { text: content } = stripBom(rawContent);
81
144
  const normalized = normalizeToLF(content);
82
- if (!options.skipHashValidation) {
83
- const hashError = validateSectionHash(section, absolutePath, normalized, snapshots);
84
- if (hashError) return { error: hashError };
85
- }
86
- const result = options.streaming
87
- ? section.applyPartialTo(normalized, nativeBlockResolver)
88
- : section.applyTo(normalized, nativeBlockResolver);
145
+ const result = applyPreviewEdits({ section, absolutePath, normalized, snapshots, options });
89
146
  if (normalized === result.text) return { error: `No changes would be made to ${section.path}.` };
90
147
  return generateDiffString(normalized, result.text);
91
148
  } catch (err) {
@@ -235,23 +235,27 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
235
235
 
236
236
  function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
237
237
  if (!diff) return "";
238
- // Hunk-aware truncation keeps the change rows themselves visible. Tail-mode
239
- // pins the visible window to the bottom of the diff so newly streamed
240
- // hunks stay on screen as more arrives, instead of leaving the user stuck
241
- // staring at the head of the file while the tail scrolls offscreen.
242
- const {
243
- text: truncatedDiff,
244
- hiddenHunks,
245
- hiddenLines,
246
- } = truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, EDIT_STREAMING_PREVIEW_LINES, { fromTail: true });
238
+ // "Cursor" tail window: pin the last EDIT_STREAMING_PREVIEW_LINES rows to the
239
+ // bottom of the diff so freshly streamed changes stay on screen, and accept
240
+ // the trailing rows "from the back" once the diff outgrows the window. The
241
+ // whole-file diff is recomputed on every streamed chunk and its Myers
242
+ // alignment is not monotonic in payload length, so a hunk-aware window that
243
+ // kept whole change segments gained and lost rows tick to tick — the box
244
+ // stuttered, and the earlier high-water fix traded that for a half-empty
245
+ // rectangle. A strict fixed-height window keeps the box steady and always
246
+ // full of real diff context instead of blank padding.
247
+ const allLines = diff.replace(/\n+$/u, "").split("\n");
248
+ const hiddenLines = Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
249
+ const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
247
250
  let text = "\n\n";
248
- if (hiddenHunks > 0 || hiddenLines > 0) {
251
+ if (hiddenLines > 0) {
252
+ const hiddenHunks = getDiffStats(allLines.slice(0, hiddenLines).join("\n")).hunks;
249
253
  const remainder: string[] = [];
250
254
  if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
251
- if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
255
+ remainder.push(`${hiddenLines} more lines`);
252
256
  text += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
253
257
  }
254
- text += renderDiffColored(truncatedDiff, { filePath: rawPath });
258
+ text += renderDiffColored(visible.join("\n"), { filePath: rawPath });
255
259
  text += uiTheme.fg("dim", `\n(${label})`);
256
260
  return text;
257
261
  }