@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (359) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +8 -11
  3. package/docs/prefix-cache-architecture.md +14 -0
  4. package/package.json +22 -24
  5. package/packages/ask-user/README.md +55 -107
  6. package/packages/ask-user/ask-ui.ts +306 -732
  7. package/packages/ask-user/clipboard.ts +50 -0
  8. package/packages/ask-user/config.ts +74 -78
  9. package/packages/ask-user/package.json +2 -2
  10. package/packages/ask-user/questions.ts +151 -0
  11. package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
  12. package/packages/ask-user/tools.ts +198 -432
  13. package/packages/autocomplete/package.json +1 -1
  14. package/packages/autocomplete/src/constants.ts +38 -43
  15. package/packages/autocomplete/src/sorting.ts +8 -0
  16. package/packages/background-tasks/package.json +2 -2
  17. package/packages/background-tasks/src/config.ts +0 -73
  18. package/packages/background-tasks/src/index.ts +20 -16
  19. package/packages/background-tasks/src/registry.ts +1 -171
  20. package/packages/background-tasks/src/types.ts +0 -32
  21. package/packages/btw/README.md +13 -56
  22. package/packages/btw/extensions/btw.ts +403 -1796
  23. package/packages/btw/package.json +5 -2
  24. package/packages/btw/skills/btw/SKILL.md +7 -143
  25. package/packages/compactor/README.md +56 -287
  26. package/packages/compactor/package.json +5 -9
  27. package/packages/compactor/skills/compactor/SKILL.md +13 -20
  28. package/packages/compactor/src/card.ts +90 -0
  29. package/packages/compactor/src/commands/index.ts +73 -130
  30. package/packages/compactor/src/compaction/compact-args.ts +5 -3
  31. package/packages/compactor/src/compaction/filter-noise.ts +1 -1
  32. package/packages/compactor/src/compaction/hooks.ts +487 -311
  33. package/packages/compactor/src/compaction/source.ts +178 -0
  34. package/packages/compactor/src/compaction/summarize.ts +586 -52
  35. package/packages/compactor/src/config/manager.ts +130 -127
  36. package/packages/compactor/src/config/schema.ts +32 -48
  37. package/packages/compactor/src/index.ts +40 -431
  38. package/packages/compactor/src/info-screen.ts +44 -190
  39. package/packages/compactor/src/session/recall-blocks.ts +1 -1
  40. package/packages/compactor/src/stats.ts +76 -0
  41. package/packages/compactor/src/tools/context-budget.ts +17 -34
  42. package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
  43. package/packages/compactor/src/tools/register.ts +29 -269
  44. package/packages/compactor/src/types.ts +47 -186
  45. package/packages/core/README.md +25 -0
  46. package/packages/core/child-agent.ts +1 -0
  47. package/packages/core/command-echo.ts +63 -0
  48. package/packages/core/compaction-context.ts +41 -0
  49. package/packages/core/constants.ts +8 -38
  50. package/packages/core/events.ts +0 -53
  51. package/packages/core/fusion-status.ts +4 -0
  52. package/packages/core/global.d.ts +10 -0
  53. package/packages/core/harness-messages.ts +392 -0
  54. package/packages/core/index.ts +14 -0
  55. package/packages/core/kanboard-status.ts +49 -0
  56. package/packages/core/long-horizon-owner-status.ts +46 -0
  57. package/packages/core/model-cache.ts +81 -7
  58. package/packages/core/package.json +6 -3
  59. package/packages/core/src/attach/detect.ts +163 -0
  60. package/packages/core/src/child-agent/child-args.ts +31 -0
  61. package/packages/core/src/child-agent/delivery.ts +41 -0
  62. package/packages/core/src/child-agent/index.ts +5 -0
  63. package/packages/core/src/child-agent/readers.ts +203 -0
  64. package/packages/core/src/child-agent/runtime.ts +584 -0
  65. package/packages/core/src/evidence.ts +78 -0
  66. package/packages/core/src/hints/browser.ts +278 -0
  67. package/packages/core/src/hints/crab-data.ts +19 -0
  68. package/packages/core/src/hints/index.ts +865 -0
  69. package/packages/core/src/hints/lines.ts +607 -0
  70. package/packages/core/src/hints/store.ts +120 -0
  71. package/packages/core/src/jev/chatter.ts +24 -0
  72. package/packages/core/src/jev/settings.ts +177 -14
  73. package/packages/core/src/package-colors.ts +2 -0
  74. package/packages/core/src/settings/catalog.ts +39 -15
  75. package/packages/core/src/settings/engine.ts +48 -2
  76. package/packages/core/src/settings/hub.ts +83 -20
  77. package/packages/core/src/settings/open.ts +49 -0
  78. package/packages/core/src/settings/schema.ts +27 -7
  79. package/packages/core/src/tui/hub-kit.ts +18 -5
  80. package/packages/core/src/tui/kit.ts +198 -0
  81. package/packages/core/src/tui/progress.ts +73 -0
  82. package/packages/core/src/turn/arbiter.ts +290 -0
  83. package/packages/footer/README.md +4 -6
  84. package/packages/footer/package.json +3 -3
  85. package/packages/footer/src/glance-editor.ts +19 -11
  86. package/packages/footer/src/help.ts +1 -1
  87. package/packages/footer/src/index.ts +36 -3
  88. package/packages/footer/src/presets.ts +3 -4
  89. package/packages/footer/src/rendering/icons.ts +0 -20
  90. package/packages/footer/src/segments/compactor.ts +52 -142
  91. package/packages/footer/src/segments/status-ext.ts +0 -1
  92. package/packages/fusion/README.md +14 -4
  93. package/packages/fusion/package.json +4 -3
  94. package/packages/fusion/src/child-args.ts +1 -0
  95. package/packages/fusion/src/index.ts +161 -8
  96. package/packages/fusion/src/pi-settings.ts +57 -0
  97. package/packages/fusion/src/picker.ts +186 -51
  98. package/packages/fusion/src/preset.ts +56 -2
  99. package/packages/fusion/src/prompts.ts +1 -1
  100. package/packages/fusion/src/sidekick-runtime.ts +1 -420
  101. package/packages/fusion/src/tools.ts +107 -128
  102. package/packages/fusion/src/transcript.ts +27 -85
  103. package/packages/info-screen/package.json +2 -2
  104. package/packages/input-shortcuts/README.md +18 -30
  105. package/packages/input-shortcuts/package.json +2 -2
  106. package/packages/input-shortcuts/src/burst.ts +88 -0
  107. package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
  108. package/packages/input-shortcuts/src/cleared-input.ts +97 -0
  109. package/packages/input-shortcuts/src/index.ts +99 -193
  110. package/packages/input-shortcuts/src/last-response.ts +40 -0
  111. package/packages/input-shortcuts/src/registers.ts +8 -23
  112. package/packages/input-shortcuts/src/types.ts +4 -16
  113. package/packages/input-shortcuts/src/undo-redo.ts +59 -47
  114. package/packages/kanboard/README.md +51 -47
  115. package/packages/kanboard/index.ts +293 -55
  116. package/packages/kanboard/package.json +8 -8
  117. package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
  118. package/packages/kanboard/src/badges.ts +93 -0
  119. package/packages/kanboard/src/bin.ts +4 -2
  120. package/packages/kanboard/src/commands.ts +218 -84
  121. package/packages/kanboard/src/debug.ts +20 -0
  122. package/packages/kanboard/src/guard.ts +267 -109
  123. package/packages/kanboard/src/monitor.ts +330 -0
  124. package/packages/kanboard/src/notice-buffer.ts +74 -0
  125. package/packages/kanboard/src/progress.ts +27 -0
  126. package/packages/kanboard/src/reminders.ts +234 -0
  127. package/packages/kanboard/src/settings.ts +69 -15
  128. package/packages/kanboard/src/shapes.ts +5 -0
  129. package/packages/long-horizon/index.ts +190 -13
  130. package/packages/long-horizon/package.json +3 -3
  131. package/packages/long-horizon/src/commands.ts +103 -7
  132. package/packages/long-horizon/src/compaction-brief.ts +35 -0
  133. package/packages/long-horizon/src/engine/continuation.ts +84 -15
  134. package/packages/long-horizon/src/engine/goal-state.ts +64 -13
  135. package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
  136. package/packages/long-horizon/src/engine/ralph.ts +3 -2
  137. package/packages/long-horizon/src/engine/runaway.ts +68 -11
  138. package/packages/long-horizon/src/engine/verifier.ts +19 -0
  139. package/packages/long-horizon/src/gate.ts +37 -12
  140. package/packages/long-horizon/src/judge/resolve.ts +1 -0
  141. package/packages/long-horizon/src/owner.ts +35 -2
  142. package/packages/long-horizon/src/progress.ts +87 -0
  143. package/packages/long-horizon/src/prompts/goal.ts +1 -1
  144. package/packages/long-horizon/src/runtime.ts +172 -18
  145. package/packages/long-horizon/src/settings.ts +54 -39
  146. package/packages/long-horizon/src/tools/goal.ts +6 -0
  147. package/packages/long-horizon/src/tools/graph.ts +1 -1
  148. package/packages/long-horizon/src/tools/swarm.ts +3 -3
  149. package/packages/mcp/package.json +2 -2
  150. package/packages/memory/commands.ts +7 -2
  151. package/packages/memory/index.ts +210 -32
  152. package/packages/memory/package.json +3 -3
  153. package/packages/memory/save-session.ts +264 -0
  154. package/packages/memory/settings.ts +17 -0
  155. package/packages/memory/tools.ts +253 -203
  156. package/packages/notify/package.json +2 -2
  157. package/packages/skill-registry/README.md +53 -16
  158. package/packages/skill-registry/index.ts +230 -0
  159. package/packages/skill-registry/package.json +19 -8
  160. package/packages/skill-registry/src/editor.ts +206 -0
  161. package/packages/skill-registry/src/judge.ts +289 -0
  162. package/packages/skill-registry/src/registry.ts +87 -0
  163. package/packages/skill-registry/src/settings.ts +259 -0
  164. package/packages/skill-registry/src/vault.ts +80 -0
  165. package/packages/subagents/README.md +39 -119
  166. package/packages/subagents/package.json +6 -13
  167. package/packages/subagents/skills/subagents/SKILL.md +18 -42
  168. package/packages/subagents/src/agents.ts +213 -0
  169. package/packages/subagents/src/cards.ts +182 -0
  170. package/packages/subagents/src/index.ts +609 -1084
  171. package/packages/subagents/src/manager.ts +429 -0
  172. package/packages/subagents/src/profiles.ts +130 -0
  173. package/packages/subagents/src/transcript.ts +154 -0
  174. package/packages/subagents/src/ui.ts +389 -0
  175. package/packages/unipi/bundled.js +39756 -45569
  176. package/packages/unipi/index.ts +55 -10
  177. package/packages/updater/package.json +2 -2
  178. package/packages/utility/README.md +76 -69
  179. package/packages/utility/package.json +2 -2
  180. package/packages/utility/skills/image/SKILL.md +37 -0
  181. package/packages/utility/skills/utility/SKILL.md +17 -60
  182. package/packages/utility/src/answer/extract.ts +128 -0
  183. package/packages/utility/src/answer/index.ts +213 -0
  184. package/packages/utility/src/answer/reply.ts +133 -0
  185. package/packages/utility/src/answer/web.ts +167 -0
  186. package/packages/utility/src/attach/index.ts +205 -0
  187. package/packages/utility/src/commands.ts +38 -142
  188. package/packages/utility/src/diagnostics/engine.ts +50 -5
  189. package/packages/utility/src/image/index.ts +33 -0
  190. package/packages/utility/src/image/models.ts +221 -0
  191. package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
  192. package/packages/utility/src/image/settings.ts +178 -0
  193. package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
  194. package/packages/utility/src/image/tools.ts +168 -0
  195. package/packages/utility/src/image/transports.ts +186 -0
  196. package/packages/utility/src/index.ts +68 -389
  197. package/packages/utility/src/lifecycle/cleanup.ts +116 -311
  198. package/packages/utility/src/rename/gate.ts +102 -0
  199. package/packages/utility/src/rename/index.ts +191 -0
  200. package/packages/utility/src/rename/session.ts +170 -0
  201. package/packages/utility/src/render/delegated.ts +285 -0
  202. package/packages/utility/src/render/harness.ts +683 -0
  203. package/packages/utility/src/render/parse.ts +156 -0
  204. package/packages/utility/src/render/pi-settings.ts +38 -0
  205. package/packages/utility/src/render/reply-bg.ts +307 -0
  206. package/packages/utility/src/render/simple.ts +724 -0
  207. package/packages/utility/src/render/spacing.ts +235 -0
  208. package/packages/utility/src/render/styled.ts +251 -0
  209. package/packages/utility/src/render/tools.ts +353 -0
  210. package/packages/utility/src/settings.ts +166 -233
  211. package/packages/utility/src/types.ts +1 -222
  212. package/packages/watchdog/index.ts +17 -6
  213. package/packages/watchdog/package.json +3 -3
  214. package/packages/watchdog/src/config.ts +3 -2
  215. package/packages/web-api/package.json +2 -2
  216. package/packages/workflow/README.md +3 -0
  217. package/packages/workflow/index.ts +0 -2
  218. package/packages/workflow/package.json +2 -2
  219. package/packages/workflow/src/permission/decide.ts +6 -4
  220. package/packages/workflow/src/permission/index.ts +17 -7
  221. package/packages/workflow/src/permission/settings.ts +39 -4
  222. package/packages/workflow/src/plan/index.ts +19 -4
  223. package/packages/background-tasks/extensions/delegate-child.ts +0 -1
  224. package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
  225. package/packages/background-tasks/src/context-token-budget.ts +0 -903
  226. package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
  227. package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
  228. package/packages/background-tasks/src/delegate/budget.ts +0 -415
  229. package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
  230. package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
  231. package/packages/background-tasks/src/delegate/launch.ts +0 -484
  232. package/packages/background-tasks/src/delegate/result-package.ts +0 -459
  233. package/packages/background-tasks/src/delegate/runner.ts +0 -430
  234. package/packages/background-tasks/src/delegate/seed.ts +0 -423
  235. package/packages/background-tasks/src/delegate/types.ts +0 -323
  236. package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
  237. package/packages/background-tasks/src/delegate-extension.ts +0 -654
  238. package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
  239. package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
  240. package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
  241. package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
  242. package/packages/compactor/src/compaction/build-sections.ts +0 -76
  243. package/packages/compactor/src/compaction/extract/goals.ts +0 -36
  244. package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
  245. package/packages/compactor/src/compaction/format.ts +0 -53
  246. package/packages/compactor/src/compaction/merge.ts +0 -143
  247. package/packages/compactor/src/config/presets.ts +0 -160
  248. package/packages/compactor/src/executor/executor.ts +0 -233
  249. package/packages/compactor/src/executor/runtime.ts +0 -115
  250. package/packages/compactor/src/security/evaluator.ts +0 -89
  251. package/packages/compactor/src/security/policy.ts +0 -96
  252. package/packages/compactor/src/security/scanner.ts +0 -65
  253. package/packages/compactor/src/session/auto-inject.ts +0 -60
  254. package/packages/compactor/src/session/db.ts +0 -325
  255. package/packages/compactor/src/session/extract.ts +0 -144
  256. package/packages/compactor/src/session/resume-inject.ts +0 -64
  257. package/packages/compactor/src/session/snapshot.ts +0 -327
  258. package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
  259. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
  260. package/packages/compactor/src/tools/ctx-execute.ts +0 -23
  261. package/packages/compactor/src/tools/ctx-stats.ts +0 -96
  262. package/packages/image/README.md +0 -99
  263. package/packages/image/package.json +0 -61
  264. package/packages/image/skills/image/SKILL.md +0 -76
  265. package/packages/image/src/generate.ts +0 -256
  266. package/packages/image/src/index.ts +0 -135
  267. package/packages/image/src/models.ts +0 -519
  268. package/packages/image/src/openai-images-api.ts +0 -282
  269. package/packages/image/src/register-providers.ts +0 -209
  270. package/packages/image/src/settings.ts +0 -173
  271. package/packages/image/src/tools.ts +0 -326
  272. package/packages/input-shortcuts/src/clipboard.ts +0 -119
  273. package/packages/kanboard/src/runner.ts +0 -817
  274. package/packages/skill-registry/skills/auto/SKILL.md +0 -465
  275. package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
  276. package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
  277. package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
  278. package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
  279. package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
  280. package/packages/skill-registry/skills/debug/SKILL.md +0 -228
  281. package/packages/skill-registry/skills/document/SKILL.md +0 -120
  282. package/packages/skill-registry/skills/fix/SKILL.md +0 -212
  283. package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
  284. package/packages/skill-registry/skills/plan/SKILL.md +0 -231
  285. package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
  286. package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
  287. package/packages/skill-registry/skills/research/SKILL.md +0 -251
  288. package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
  289. package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
  290. package/packages/skill-registry/skills/work/SKILL.md +0 -218
  291. package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
  292. package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
  293. package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
  294. package/packages/subagents/agents/delegate.md +0 -14
  295. package/packages/subagents/agents/oracle.md +0 -78
  296. package/packages/subagents/agents/researcher.md +0 -52
  297. package/packages/subagents/agents/reviewer.md +0 -80
  298. package/packages/subagents/agents/scout.md +0 -50
  299. package/packages/subagents/agents/worker.md +0 -59
  300. package/packages/subagents/prompts/council.md +0 -48
  301. package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
  302. package/packages/subagents/prompts/parallel-cleanup.md +0 -59
  303. package/packages/subagents/prompts/parallel-research.md +0 -50
  304. package/packages/subagents/prompts/parallel-review.md +0 -54
  305. package/packages/subagents/prompts/review-loop.md +0 -43
  306. package/packages/subagents/skills/council-mode/SKILL.md +0 -230
  307. package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
  308. package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
  309. package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
  310. package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
  311. package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
  312. package/packages/subagents/src/acceptance.ts +0 -331
  313. package/packages/subagents/src/agent-manager.ts +0 -405
  314. package/packages/subagents/src/agent-memory.ts +0 -300
  315. package/packages/subagents/src/agent-overrides.ts +0 -183
  316. package/packages/subagents/src/agent-runner.ts +0 -271
  317. package/packages/subagents/src/async-runner.ts +0 -422
  318. package/packages/subagents/src/authority-policy.ts +0 -47
  319. package/packages/subagents/src/budgets.ts +0 -285
  320. package/packages/subagents/src/child-safety.ts +0 -140
  321. package/packages/subagents/src/config.ts +0 -249
  322. package/packages/subagents/src/conversation-viewer.ts +0 -281
  323. package/packages/subagents/src/core-compat.ts +0 -94
  324. package/packages/subagents/src/custom-agents.ts +0 -343
  325. package/packages/subagents/src/file-system-retry.ts +0 -50
  326. package/packages/subagents/src/fleet-data.ts +0 -105
  327. package/packages/subagents/src/fleet-view.ts +0 -322
  328. package/packages/subagents/src/foreground-detach.ts +0 -59
  329. package/packages/subagents/src/fork-context.ts +0 -285
  330. package/packages/subagents/src/global.d.ts +0 -7
  331. package/packages/subagents/src/guide.ts +0 -129
  332. package/packages/subagents/src/mission-state.ts +0 -133
  333. package/packages/subagents/src/mission-store.ts +0 -436
  334. package/packages/subagents/src/output-limits.ts +0 -142
  335. package/packages/subagents/src/parity-types.ts +0 -315
  336. package/packages/subagents/src/pi-args.ts +0 -167
  337. package/packages/subagents/src/result-files.ts +0 -232
  338. package/packages/subagents/src/result-watcher.ts +0 -221
  339. package/packages/subagents/src/retained-children.ts +0 -176
  340. package/packages/subagents/src/run-fanout-budget.ts +0 -356
  341. package/packages/subagents/src/scheduled-runs.ts +0 -349
  342. package/packages/subagents/src/schemas.ts +0 -263
  343. package/packages/subagents/src/slash-commands.ts +0 -80
  344. package/packages/subagents/src/supervisor-channel.ts +0 -249
  345. package/packages/subagents/src/tool-handler.ts +0 -1144
  346. package/packages/subagents/src/types.ts +0 -238
  347. package/packages/subagents/src/widget.ts +0 -457
  348. package/packages/subagents/src/workflow-script.ts +0 -737
  349. package/packages/subagents/src/workflow-worker.ts +0 -384
  350. package/packages/subagents/src/worktree.ts +0 -614
  351. package/packages/utility/src/analytics/collector.ts +0 -293
  352. package/packages/utility/src/info-screen.ts +0 -80
  353. package/packages/utility/src/lifecycle/process.ts +0 -162
  354. package/packages/utility/src/prefix-cache.ts +0 -263
  355. package/packages/utility/src/skill-discovery.ts +0 -542
  356. package/packages/utility/src/tools/env.ts +0 -113
  357. package/packages/utility/src/tui/name-badge-state.ts +0 -315
  358. package/packages/utility/src/tui/name-badge.ts +0 -109
  359. /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
@@ -7,20 +7,29 @@
7
7
  */
8
8
 
9
9
  import * as fs from "node:fs";
10
+ import * as os from "node:os";
10
11
  import { dirname, join } from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
13
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
- import { Text } from "@earendil-works/pi-tui";
14
14
  import {
15
15
  UNIPI_EVENTS,
16
16
  MODULES,
17
17
  emitEvent,
18
18
  getPackageVersion,
19
+ type KitTheme,
20
+ formatTokens,
21
+ harnessMetadata,
19
22
  } from "@pi-unipi/core";
20
23
 
21
24
  import { createSessionBackend, type SessionBackend } from "./session.js";
22
- import { registerMemoryTools, MEMORY_TOOLS, memoryCard } from "./tools.js";
25
+ import { registerMemoryTools, MEMORY_TOOLS, memoryCard, type RailRow } from "./tools.js";
23
26
  import { registerMemoryCommands, type SessionOverrides } from "./commands.js";
27
+ import {
28
+ SAVE_CARD_TYPE,
29
+ shouldRunSideSave,
30
+ startMemorySave,
31
+ type MemorySaveRun,
32
+ } from "./save-session.js";
24
33
  import { readMemoryConfig, agentHooksEnabled } from "./settings.js";
25
34
  import { replayPending, pendingCount, type PendingOp } from "./pending.js";
26
35
  import { adoptLooseFiles, needsMigration, runConversion, readConversionState } from "./convert.js";
@@ -45,6 +54,42 @@ const RECALL_CUSTOM_TYPE = "unipi-memory-recall-reminder";
45
54
  const RETRO_CUSTOM_TYPE = "unipi-memory-retro-reminder";
46
55
  const CARD_CUSTOM_TYPE = "unipi-memory-session-card";
47
56
 
57
+ /** Human-readable save-pass usage line (UI only — persisted usage stays raw). */
58
+ export function saveUsageLine(usage: {
59
+ input: number;
60
+ cacheRead: number;
61
+ cacheWrite: number;
62
+ output: number;
63
+ }): string {
64
+ return `save pass · input ${formatTokens(usage.input)} · cache read ${formatTokens(usage.cacheRead)} / write ${formatTokens(usage.cacheWrite)} · output ${formatTokens(usage.output)} tokens`;
65
+ }
66
+
67
+ /** ~/.unipi/logs/memory.log lines when UNIPI_DEBUG_MEMORY=1 (silent otherwise). */
68
+ export function memoryDebug(line: string): void {
69
+ if (process.env.UNIPI_DEBUG_MEMORY !== "1") return;
70
+ try {
71
+ const dir = join(os.homedir(), ".unipi", "logs");
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ fs.appendFileSync(join(dir, "memory.log"), `${new Date().toISOString()} ${line}\n`);
74
+ } catch {
75
+ // best-effort
76
+ }
77
+ }
78
+
79
+ /** True when the branch segment AFTER the last compaction already carries the
80
+ * recall reminder — a /reload or resume must not inject it twice. */
81
+ export function branchHasRecallReminder(
82
+ branch: Array<{ type: string; customType?: string }>,
83
+ ): boolean {
84
+ let lastCompact = -1;
85
+ branch.forEach((entry, index) => {
86
+ if (entry.type === "compaction") lastCompact = index;
87
+ });
88
+ return branch
89
+ .slice(lastCompact + 1)
90
+ .some((entry) => entry.type === "custom_message" && entry.customType === RECALL_CUSTOM_TYPE);
91
+ }
92
+
48
93
  export function buildMemoryRecallReminder(input: {
49
94
  projectName: string;
50
95
  memories: Array<{ title: string }>;
@@ -84,6 +129,23 @@ export function buildMemoryRecallReminder(input: {
84
129
  return lines.join("\n");
85
130
  }
86
131
 
132
+ /** One-queued-reminder guard (exported for tests): queue() latches until the
133
+ * reminder lands (clear on before_agent_start). */
134
+ export function retroReminderGuard(): { readonly queued: boolean; queue(): void; clear(): void } {
135
+ let queued = false;
136
+ return {
137
+ get queued() {
138
+ return queued;
139
+ },
140
+ queue() {
141
+ queued = true;
142
+ },
143
+ clear() {
144
+ queued = false;
145
+ },
146
+ };
147
+ }
148
+
87
149
  export default function (pi: ExtensionAPI) {
88
150
  let backend: SessionBackend | null = null;
89
151
  let recallDone = false;
@@ -121,15 +183,14 @@ export default function (pi: ExtensionAPI) {
121
183
 
122
184
  // Render the recall/end reminders as small badges (UI-only).
123
185
  try {
124
- const badge = (icon: string, label: string) =>
125
- (message: { content?: unknown }, _options: unknown, theme: { bg?: (c: string, t: string) => string; fg?: (c: string, t: string) => string; bold?: (t: string) => string }) => {
126
- const text = String(message.content ?? "").split("\n")[0] ?? label;
127
- const inner = theme.fg?.("syntaxKeyword", theme.bold?.(` ${icon} ${label} `) ?? ` ${icon} ${label} `) ?? ` ${label} `;
128
- const b = theme.bg?.("customMessageBg", inner) ?? inner;
129
- return new Text(`${b} ${theme.fg?.("dim", text.replace(/[*#🧠]/gu, "").trim()) ?? text}`, 0, 0);
186
+ // Same rail as every memory surface: `▌ Memory recall <first line>`.
187
+ const reminder = (label: string) =>
188
+ (message: { content?: unknown }, _options: unknown, theme: KitTheme) => {
189
+ const text = (String(message.content ?? "").split("\n")[0] ?? "").replace(/[*#🧠]/gu, "").trim();
190
+ return memoryCard(theme, [`${theme.bold(label)} ${theme.fg("dim", text)}`]);
130
191
  };
131
- pi.registerMessageRenderer?.(RECALL_CUSTOM_TYPE, badge("◈", "memory recall") as never);
132
- pi.registerMessageRenderer?.(RETRO_CUSTOM_TYPE, badge("◈", "memory save?") as never);
192
+ pi.registerMessageRenderer?.(RECALL_CUSTOM_TYPE, reminder("Memory recall") as never);
193
+ pi.registerMessageRenderer?.(RETRO_CUSTOM_TYPE, reminder("Memory save?") as never);
133
194
  } catch { /* renderer registration is UI-dependent */ }
134
195
 
135
196
  // Session card (UI-only entry + renderer).
@@ -142,16 +203,17 @@ export default function (pi: ExtensionAPI) {
142
203
  const cfg = readMemoryConfig();
143
204
  const recall = cfg.recallAtStart && overrides.recall !== false ? "on" : "off";
144
205
  const write = cfg.write && overrides.write !== false ? "on" : "off";
145
- const lines = [
146
- `◈ memory palace · ${d.project}`,
147
- `${d.count} memories · recall ${recall} · write ${write}${d.wakeUp ? " · wake-up ready (expand)" : ""}`,
148
- ...(d.issue ? [d.issue] : []),
149
- ...(d.needsMigrate ? ["v2 memories found — run /unipi:memory migrate"] : []),
206
+ const t = theme as unknown as KitTheme;
207
+ const rows: RailRow[] = [
208
+ { left: `${t.bold("Memory palace")} ${d.project}`, right: t.fg("dim", `${String(d.count)} ${d.count === 1 ? "memory" : "memories"}`) },
209
+ t.fg("dim", `recall ${recall} · write ${write}${d.wakeUp ? " · wake-up ready (expand)" : ""}`),
210
+ ...(d.issue ? [t.fg("warning", d.issue)] : []),
211
+ ...(d.needsMigrate ? [t.fg("warning", "v2 memories found — run /unipi:memory migrate")] : []),
150
212
  ];
151
213
  if ((_options as { expanded?: boolean } | undefined)?.expanded && d.wakeUp) {
152
- lines.push("", ...d.wakeUp.split("\n"));
214
+ rows.push("", ...d.wakeUp.split("\n").map((l) => t.fg("muted", l)));
153
215
  }
154
- return memoryCard(theme as never, lines);
216
+ return memoryCard(t, rows, d.issue ? "warning" : "memory");
155
217
  },
156
218
  );
157
219
  } catch { /* UI-dependent */ }
@@ -159,6 +221,13 @@ export default function (pi: ExtensionAPI) {
159
221
  pi.on("session_start", async (_event, ctx) => {
160
222
  recallDone = false;
161
223
  storeDone = false;
224
+ // Reload/resume: the reminder is already in this branch's context tail —
225
+ // injecting again would just repeat it.
226
+ try {
227
+ if (branchHasRecallReminder(ctx.sessionManager.getBranch())) recallDone = true;
228
+ } catch {
229
+ // branch read is best-effort
230
+ }
162
231
  overrides.recall = undefined;
163
232
  overrides.write = undefined;
164
233
  wakeUpPromise = null;
@@ -410,6 +479,9 @@ export default function (pi: ExtensionAPI) {
410
479
  ])) ?? undefined;
411
480
  }
412
481
 
482
+ // Injected once per session (and once after each compaction — the
483
+ // session_compact / COMPACTOR_COMPACTED handlers below reset the flag).
484
+ recallDone = true;
413
485
  return {
414
486
  message: {
415
487
  customType: RECALL_CUSTOM_TYPE,
@@ -421,36 +493,142 @@ export default function (pi: ExtensionAPI) {
421
493
  wakeUp: wake,
422
494
  }),
423
495
  display: true,
496
+ details: { unipiHarness: harnessMetadata({ source: "Memory", title: "Memory recall", synopsis: "Recalled at session start" }, "before_agent_start") },
424
497
  },
425
498
  };
426
499
  });
427
500
 
428
- pi.on("agent_end", async (_event, _ctx) => {
429
- if (storeDone || !recallDone) return;
430
- if (!readMemoryConfig().write || overrides.write === false) return;
431
- if (!agentHooksEnabled()) return;
432
- if (!pi.getActiveTools().includes(MEMORY_TOOLS.STORE)) return;
433
- pi.sendMessage(
434
- {
435
- customType: RETRO_CUSTOM_TYPE,
436
- content: [
437
- "**🧠 Memory reminder:** If you learned something non-obvious in this task,",
438
- "call `memory_store` to save it as a memory for future sessions.",
439
- "Update existing memories instead of creating duplicates.",
440
- ].join(" "),
441
- display: true,
501
+ // Save pass outcome card — UI-only entry, never model context.
502
+ try {
503
+ pi.registerEntryRenderer?.<{ titles?: string[]; usage?: { input: number; cacheRead: number; cacheWrite: number; output: number } }>(
504
+ SAVE_CARD_TYPE,
505
+ (entry, _options, theme) => {
506
+ const d = entry.data;
507
+ if (!d?.titles?.length) return undefined;
508
+ const t = theme as unknown as KitTheme;
509
+ const rows: RailRow[] = [
510
+ { left: `${t.bold("Memory saved")} ${d.titles.join(", ")}` },
511
+ ];
512
+ if (d.usage) {
513
+ rows.push(t.fg("dim", saveUsageLine(d.usage)));
514
+ }
515
+ return memoryCard(t, rows);
442
516
  },
443
- { deliverAs: "nextTurn" },
444
517
  );
518
+ } catch { /* UI-dependent */ }
519
+
520
+ // Two agent runs can end before the next user prompt (e.g. a background-task
521
+ // completion triggers a follow-up turn) — keep at most one queued reminder.
522
+ const retro = retroReminderGuard();
523
+
524
+ pi.on("before_agent_start", () => {
525
+ retro.clear();
526
+ });
527
+
528
+ // Per-run activity for the side save pass: reset on agent_start, counted on
529
+ // tool_call. The side session runs on its own AgentSession — its calls never
530
+ // reach these handlers.
531
+ let runToolCalls = 0;
532
+ let runHadWrite = false;
533
+ let runStoredMemory = false;
534
+ let saveRun: MemorySaveRun | null = null;
535
+
536
+ pi.on("agent_start", () => {
537
+ runToolCalls = 0;
538
+ runHadWrite = false;
539
+ runStoredMemory = false;
540
+ });
541
+ pi.on("tool_call", (event) => {
542
+ runToolCalls += 1;
543
+ if (event.toolName === "edit" || event.toolName === "write") runHadWrite = true;
544
+ if (event.toolName === MEMORY_TOOLS.STORE || event.toolName === MEMORY_TOOLS.DELETE) {
545
+ runStoredMemory = true;
546
+ }
547
+ });
548
+
549
+ pi.on("agent_end", async (_event, ctx) => {
550
+ const cfg = readMemoryConfig(ctx.cwd);
551
+ const mode = cfg.saveMode;
552
+
553
+ if (mode === "reminder") {
554
+ // Exactly the pre-side behaviour: one nextTurn nudge per finished run.
555
+ if (storeDone || !recallDone) return;
556
+ if (retro.queued) return;
557
+ if (!cfg.write || overrides.write === false) return;
558
+ if (!agentHooksEnabled()) return;
559
+ if (!pi.getActiveTools().includes(MEMORY_TOOLS.STORE)) return;
560
+ retro.queue();
561
+ pi.sendMessage(
562
+ {
563
+ customType: RETRO_CUSTOM_TYPE,
564
+ content: [
565
+ "**🧠 Memory reminder:** If you learned something non-obvious in this task,",
566
+ "call `memory_store` to save it as a memory for future sessions.",
567
+ "Update existing memories instead of creating duplicates.",
568
+ ].join(" "),
569
+ display: true,
570
+ details: { unipiHarness: harnessMetadata({ source: "Memory", title: "Memory save?", synopsis: "Save reminder after a finished run" }, "nextTurn") },
571
+ },
572
+ { deliverAs: "nextTurn" },
573
+ );
574
+ return;
575
+ }
576
+
577
+ if (mode !== "side") return; // "off"
578
+ if (
579
+ !shouldRunSideSave({
580
+ write: cfg.write,
581
+ writeOverride: overrides.write,
582
+ hooksEnabled: agentHooksEnabled(),
583
+ storeActive: pi.getActiveTools().includes(MEMORY_TOOLS.STORE),
584
+ runToolCalls,
585
+ runHadWrite,
586
+ runStoredMemory,
587
+ running: saveRun !== null,
588
+ })
589
+ ) {
590
+ return;
591
+ }
592
+
593
+ const run = startMemorySave(pi, ctx, () => backend);
594
+ saveRun = run;
595
+ void run.finished
596
+ .then((outcome) => {
597
+ const usage = outcome.usage;
598
+ memoryDebug(
599
+ `save: stored=[${outcome.stored.join(", ")}]` +
600
+ (usage ? ` usage in=${usage.input} cacheRead=${usage.cacheRead} cacheWrite=${usage.cacheWrite} out=${usage.output}` : "") +
601
+ (outcome.error ? ` error=${outcome.error}` : ""),
602
+ );
603
+ if (outcome.stored.length > 0) {
604
+ // A store happened — a later reminder-mode switch shouldn't re-nag.
605
+ storeDone = true;
606
+ try {
607
+ pi.appendEntry(SAVE_CARD_TYPE, { titles: outcome.stored, usage });
608
+ } catch {
609
+ // card is cosmetic
610
+ }
611
+ }
612
+ })
613
+ .catch((error) => memoryDebug(`save: ${error instanceof Error ? error.message : String(error)}`))
614
+ .finally(() => {
615
+ if (saveRun === run) saveRun = null;
616
+ });
445
617
  });
446
618
 
447
619
  pi.on("session_compact", async () => {
448
620
  recallDone = false;
449
621
  });
622
+ // Boundary compactions (compactor's percentage trigger) skip session_compact.
623
+ pi.events?.on?.(UNIPI_EVENTS.COMPACTOR_COMPACTED, () => {
624
+ recallDone = false;
625
+ });
450
626
 
451
627
  pi.on("session_shutdown", async () => {
452
628
  if (pendingTimer) { clearInterval(pendingTimer); pendingTimer = null; }
453
629
  replayKick = () => {};
630
+ saveRun?.abort();
631
+ saveRun = null;
454
632
  backend?.close();
455
633
  backend = null;
456
634
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "3.0.0-alpha.2",
3
+ "version": "3.0.0-alpha.21",
4
4
  "description": "Persistent cross-session memory on MemPalace (auto-installed): daemon-routed writes, shared palace with other agents, for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -42,8 +42,8 @@
42
42
  "README.md"
43
43
  ],
44
44
  "dependencies": {
45
- "@pi-unipi/core": "3.0.0-alpha.2",
46
- "@pi-unipi/info-screen": "3.0.0-alpha.2",
45
+ "@pi-unipi/core": "3.0.0-alpha.21",
46
+ "@pi-unipi/info-screen": "3.0.0-alpha.21",
47
47
  "js-yaml": "^4.1.0"
48
48
  },
49
49
  "peerDependencies": {
@@ -0,0 +1,264 @@
1
+ /**
2
+ * @unipi/memory — background save session (saveMode "side")
3
+ *
4
+ * On agent_end a one-shot AgentSession is seeded from the main session's
5
+ * current branch (SessionManager.inMemory — the same path /unipi:btw uses)
6
+ * and asked to store anything durable. Nothing reaches the main context:
7
+ * no sendMessage, only a UI-only appendEntry card when something was stored.
8
+ *
9
+ * Prefix-cache parity is the point: same model, same thinking level, the
10
+ * verbatim system prompt (ctx.getSystemPrompt(), no appends, no stripping),
11
+ * and the same tool list — every ACTIVE tool is re-exposed as a customTools
12
+ * stub carrying the identical name/description/parameters from
13
+ * pi.getAllTools(). Non-memory stubs refuse; the memory tools call the real
14
+ * executors from tools.ts.
15
+ */
16
+
17
+ import {
18
+ createAgentSession,
19
+ createExtensionRuntime,
20
+ SessionManager,
21
+ type AgentSession,
22
+ type ExtensionAPI,
23
+ type ExtensionContext,
24
+ type FileEntry,
25
+ type ModelRuntime,
26
+ type ResourceLoader,
27
+ type ToolDefinition,
28
+ } from "@earendil-works/pi-coding-agent";
29
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
30
+ import type { SessionBackend } from "./session.js";
31
+ import { MEMORY_TOOLS, memoryExecutors } from "./tools.js";
32
+
33
+ export const SAVE_CARD_TYPE = "unipi-memory-save-card";
34
+ export const SAVE_PROMPT =
35
+ "Memory save pass. Review the task that just finished in this conversation. " +
36
+ "If you learned something non-obvious and durable (decision, pitfall + fix, project pattern, user preference), " +
37
+ "call memory_search to check for an existing memory, then memory_store (update rather than duplicate). " +
38
+ "If nothing qualifies, reply exactly NONE. Do not call any other tool.";
39
+
40
+ const MAX_SAVE_TOOL_CALLS = 8;
41
+
42
+ export interface SaveUsage {
43
+ input: number;
44
+ cacheRead: number;
45
+ cacheWrite: number;
46
+ output: number;
47
+ }
48
+
49
+ export interface MemorySaveOutcome {
50
+ /** Titles stored or updated during the pass. */
51
+ stored: string[];
52
+ /** Summed usage across the side session's assistant messages. */
53
+ usage?: SaveUsage;
54
+ error?: string;
55
+ }
56
+
57
+ export interface MemorySaveRun {
58
+ abort(): void;
59
+ finished: Promise<MemorySaveOutcome>;
60
+ }
61
+
62
+ /** Whether a finished main-session run qualifies for the background save pass. */
63
+ export function shouldRunSideSave(input: {
64
+ write: boolean;
65
+ writeOverride?: boolean;
66
+ hooksEnabled: boolean;
67
+ storeActive: boolean;
68
+ /** Tool calls in the run that just ended (all tools). */
69
+ runToolCalls: number;
70
+ /** Any edit/write in the run that just ended. */
71
+ runHadWrite: boolean;
72
+ /** The main agent stored/deleted a memory itself during the run. */
73
+ runStoredMemory: boolean;
74
+ /** A save session is already in flight. */
75
+ running: boolean;
76
+ }): boolean {
77
+ if (!input.write || input.writeOverride === false) return false;
78
+ if (!input.hooksEnabled) return false;
79
+ if (!input.storeActive) return false;
80
+ if (input.runStoredMemory) return false;
81
+ if (input.runToolCalls < 3 && !input.runHadWrite) return false;
82
+ if (input.running) return false; // one at a time — skip, never queue
83
+ return true;
84
+ }
85
+
86
+ /** Tool names that reach the real implementations inside the save session.
87
+ * memory_delete stays a stub: a background pass may add knowledge, never
88
+ * remove it. */
89
+ const REAL_MEMORY_EXECUTES = new Set<string>([
90
+ MEMORY_TOOLS.STORE,
91
+ MEMORY_TOOLS.SEARCH,
92
+ MEMORY_TOOLS.LIST,
93
+ MEMORY_TOOLS.GLOBAL_SEARCH,
94
+ MEMORY_TOOLS.GLOBAL_LIST,
95
+ ]);
96
+
97
+ const STUB_REFUSAL = "Not available in the memory save session.";
98
+
99
+ /**
100
+ * customTools mirroring the main session's active tools: identical names,
101
+ * descriptions and parameter schemas so the serialized request prefix matches
102
+ * what the main agent just sent. Order follows pi.getAllTools() (builtins keep
103
+ * their positions — a same-named customTool overrides in place).
104
+ */
105
+ export function buildSaveTools(
106
+ pi: Pick<ExtensionAPI, "getAllTools" | "getActiveTools">,
107
+ backend: () => SessionBackend | null,
108
+ onStored?: (title: string) => void,
109
+ ): ToolDefinition[] {
110
+ const active = new Set(pi.getActiveTools());
111
+ const ex = memoryExecutors(pi as never, backend);
112
+ const real = (name: string) => {
113
+ switch (name) {
114
+ case MEMORY_TOOLS.STORE:
115
+ return async (_id: string, params: { title: string; content: string; tags?: string[]; type?: string }, _s: unknown, _o: unknown, ctx: { cwd: string }) => {
116
+ const res = await ex.store(params, ctx);
117
+ const action = (res.details as { action?: string } | undefined)?.action;
118
+ if (action === "created" || action === "updated") onStored?.(params.title);
119
+ return res;
120
+ };
121
+ case MEMORY_TOOLS.SEARCH:
122
+ return async (_id: string, params: { query: string; limit?: number; scope?: string }) => ex.search(params);
123
+ case MEMORY_TOOLS.GLOBAL_SEARCH:
124
+ return async (_id: string, params: { query: string; limit?: number }) =>
125
+ ex.search({ query: params.query, limit: params.limit, scope: "all" });
126
+ case MEMORY_TOOLS.LIST:
127
+ return async () => ex.list();
128
+ case MEMORY_TOOLS.GLOBAL_LIST:
129
+ return async () => ex.globalList();
130
+ default:
131
+ return undefined;
132
+ }
133
+ };
134
+ return pi
135
+ .getAllTools()
136
+ .filter((info) => active.has(info.name))
137
+ .map(
138
+ (info): ToolDefinition => ({
139
+ name: info.name,
140
+ label: info.name,
141
+ description: info.description,
142
+ promptGuidelines: info.promptGuidelines,
143
+ parameters: info.parameters,
144
+ execute:
145
+ real(info.name) ??
146
+ (async () => ({
147
+ content: [{ type: "text" as const, text: STUB_REFUSAL }],
148
+ details: {},
149
+ })),
150
+ }),
151
+ );
152
+ }
153
+
154
+ /** Header + current branch, verbatim — the seeded side session sees exactly
155
+ * the context the main agent just worked in (max prefix overlap). */
156
+ export function buildSeedEntries(ctx: {
157
+ sessionManager: {
158
+ getHeader(): { type: string } | null;
159
+ getBranch(): Array<{ type: string; customType?: string }>;
160
+ };
161
+ }): FileEntry[] {
162
+ const header = ctx.sessionManager.getHeader();
163
+ const branch = ctx.sessionManager.getBranch();
164
+ return header
165
+ ? [header as FileEntry, ...(branch as FileEntry[])]
166
+ : (branch as FileEntry[]);
167
+ }
168
+
169
+ /** Identical system prompt to the main session — no append, no stripping. */
170
+ function createSaveResourceLoader(ctx: ExtensionContext): ResourceLoader {
171
+ const extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() };
172
+ const systemPrompt = ctx.getSystemPrompt();
173
+ return {
174
+ getExtensions: () => extensionsResult,
175
+ getSkills: () => ({ skills: [], diagnostics: [] }),
176
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
177
+ getThemes: () => ({ themes: [], diagnostics: [] }),
178
+ getAgentsFiles: () => ({ agentsFiles: [] }),
179
+ getSystemPrompt: () => systemPrompt,
180
+ getSystemPromptSource: () => undefined,
181
+ getAppendSystemPrompt: () => [],
182
+ getAppendSystemPromptSources: () => [],
183
+ extendResources: () => {},
184
+ reload: async () => {},
185
+ };
186
+ }
187
+
188
+ function sumUsage(session: AgentSession): SaveUsage | undefined {
189
+ let usage: SaveUsage | undefined;
190
+ for (const message of session.state.messages) {
191
+ if (message.role !== "assistant") continue;
192
+ const u = (message as AssistantMessage).usage;
193
+ if (!u) continue;
194
+ usage ??= { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
195
+ usage.input += u.input;
196
+ usage.cacheRead += u.cacheRead;
197
+ usage.cacheWrite += u.cacheWrite;
198
+ usage.output += u.output;
199
+ }
200
+ return usage;
201
+ }
202
+
203
+ /**
204
+ * Start the background save pass. Caller owns gating (shouldRunSideSave) and
205
+ * the one-at-a-time check — this just runs.
206
+ */
207
+ export function startMemorySave(
208
+ pi: ExtensionAPI,
209
+ ctx: ExtensionContext,
210
+ backend: () => SessionBackend | null,
211
+ ): MemorySaveRun {
212
+ let session: AgentSession | null = null;
213
+ const stored: string[] = [];
214
+
215
+ const finished = (async (): Promise<MemorySaveOutcome> => {
216
+ const model = ctx.model;
217
+ if (!model) return { stored, error: "no active model" };
218
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
219
+ if (!auth.ok || !auth.apiKey) {
220
+ return { stored, error: auth.ok ? "no credentials" : (auth.error ?? "no credentials") };
221
+ }
222
+ try {
223
+ const seeded = await createAgentSession({
224
+ sessionManager: SessionManager.inMemory(ctx.cwd, undefined, buildSeedEntries(ctx)),
225
+ model,
226
+ modelRuntime: (ctx.modelRegistry as unknown as { runtime: ModelRuntime }).runtime,
227
+ thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel(),
228
+ customTools: buildSaveTools(pi, backend, (title) => stored.push(title)),
229
+ tools: pi.getActiveTools(),
230
+ resourceLoader: createSaveResourceLoader(ctx),
231
+ });
232
+ session = seeded.session;
233
+ let toolCalls = 0;
234
+ const unsubscribe = session.subscribe((event) => {
235
+ if (event.type === "tool_execution_start") {
236
+ toolCalls += 1;
237
+ if (toolCalls >= MAX_SAVE_TOOL_CALLS) void session?.abort().catch(() => {});
238
+ }
239
+ });
240
+ try {
241
+ await session.prompt(SAVE_PROMPT, { source: "extension" });
242
+ } finally {
243
+ unsubscribe();
244
+ }
245
+ return { stored, usage: sumUsage(session) };
246
+ } catch (error) {
247
+ return { stored, error: error instanceof Error ? error.message : String(error) };
248
+ } finally {
249
+ try {
250
+ session?.dispose();
251
+ } catch {
252
+ /* already disposed */
253
+ }
254
+ session = null;
255
+ }
256
+ })();
257
+
258
+ return {
259
+ abort() {
260
+ void session?.abort().catch(() => {});
261
+ },
262
+ finished,
263
+ };
264
+ }
@@ -18,6 +18,9 @@ export interface MemoryConfig {
18
18
  write: boolean;
19
19
  /** Include `mempalace wake-up` output in the start reminder. */
20
20
  wakeUp: boolean;
21
+ /** How the end-of-task save happens: a background side session, the inline
22
+ * nextTurn reminder, or nothing. */
23
+ saveMode: "side" | "reminder" | "off";
21
24
  /** Start the MemPalace daemon automatically when it isn't running. */
22
25
  autoStartDaemon: boolean;
23
26
  /** Keep the MemPalace install current via a daily PyPI check + uv upgrade. */
@@ -28,6 +31,7 @@ export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
28
31
  recallAtStart: true,
29
32
  write: true,
30
33
  wakeUp: true,
34
+ saveMode: "side",
31
35
  autoStartDaemon: false,
32
36
  mempalaceAutoUpdate: true,
33
37
  };
@@ -58,6 +62,19 @@ registerSettings({
58
62
  label: "Wake-up summary at start",
59
63
  description: "Include `mempalace wake-up --wing <project>` output in the start reminder.",
60
64
  },
65
+ {
66
+ key: "saveMode",
67
+ type: "enum",
68
+ label: "Save mode",
69
+ description:
70
+ "side: a background side session reviews the finished run and stores what's worth keeping (nothing enters the conversation). " +
71
+ "reminder: an end-of-task note asks the agent to save. off: no save pass at all.",
72
+ options: [
73
+ { value: "side", label: "side agent" },
74
+ { value: "reminder", label: "reminder" },
75
+ { value: "off", label: "off" },
76
+ ],
77
+ },
61
78
  {
62
79
  key: "autoStartDaemon",
63
80
  type: "boolean",