@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
@@ -1,379 +1,555 @@
1
1
  /**
2
- * Hook integration — session_before_compact + session_compact
3
- * (parity-aligned with pi-vcc before-compact.ts: token calibration, smart keep,
4
- * budget-cut rescue, keep:N parsing, invisible auto-continue)
2
+ * Compaction hooks — every compaction (Pi's automatic ones, Pi's /compact,
3
+ * UniPi's commands, the percentage trigger) goes through here and is routed
4
+ * to one of two methods:
5
+ *
6
+ * vcc lossless zero-LLM summary (summarize.ts), rebuilt from full history
7
+ * llm Pi's model-written summary, with the active-work block appended
8
+ *
9
+ * The percentage trigger compacts at `turn_end` by returning a compaction
10
+ * draft — Pi applies it at the turn boundary without aborting the run, so
11
+ * goal / ralph / kanboard loops keep going. Pi 0.87 already continues after
12
+ * its own automatic compactions, so nothing here re-triggers the agent.
5
13
  */
6
14
 
7
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
- import { convertToLlm } from "@earendil-works/pi-coding-agent";
9
- import type {
10
- SessionEntry,
11
- SessionMessageEntry,
12
- SessionBeforeCompactEvent,
13
- SessionCompactEvent,
14
- } from "@earendil-works/pi-coding-agent";
15
- import { compileRanked } from "./summarize.js";
15
+ import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent, SessionCompactEvent } from "@earendil-works/pi-coding-agent";
16
+ import { compact as piCompact, generateSummaryWithUsage, DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
17
+ import { collectCompactionContext, emitEvent, formatTokens, UNIPI_EVENTS } from "@pi-unipi/core";
16
18
  import { loadConfig } from "../config/manager.js";
17
- import {
18
- buildOwnCut,
19
- resolveSmartKeepUserTurns,
20
- applyTailBudget,
21
- MAX_SMART_TAIL_TOKENS,
22
- } from "./cut.js";
19
+ import { autoCompactionOf } from "../config/schema.js";
20
+ import { buildOwnCut, resolveSmartKeepUserTurns, applyTailBudget, MAX_SMART_TAIL_TOKENS } from "./cut.js";
23
21
  import { parseCompactionInstructions } from "./compact-args.js";
22
+ import { calibrateCharsPerToken, estimateMessageContentChars, estimateTokensFromChars } from "./token-estimate.js";
23
+ import { collectSummarySource, ORIGIN_ENTRY_TYPE, originKey } from "./source.js";
24
+ import { autoBudgetTokens, buildLosslessSummary, type LosslessSummaryInput } from "./summarize.js";
25
+ import { CARD_TYPE, type CompactionCardData, type CompactionTrigger } from "../card.js";
24
26
  import {
25
- calibrateCharsPerToken,
26
- estimateMessageContentChars,
27
- estimateTokensFromChars,
28
- } from "./token-estimate.js";
29
- import type { CompactionStats } from "../types.js";
30
- import type { SessionDB } from "../session/db.js";
31
-
32
- import { COMPACTOR_INSTRUCTION, formatTokens } from "@pi-unipi/core";
27
+ createAutoCompactionState,
28
+ decideAutoCompaction,
29
+ markAutoCompactionComplete,
30
+ markAutoCompactionError,
31
+ type AutoCompactionState,
32
+ } from "./auto-trigger.js";
33
+ import type { BudgetCutKind, CompactionMethod, CompactionStats, CompactorConfig, FileOps, RuntimeCounters } from "../types.js";
34
+
35
+ export const COMPACTOR_ID = "@pi-unipi/compactor";
36
+ const SUMMARY_CHARS_PER_TOKEN = 4;
37
+ /** Kept-tail sizing against the model window: recut above LIMIT, down to SHARE. */
38
+ const TAIL_WINDOW_LIMIT = 0.4;
39
+ const TAIL_WINDOW_SHARE = 0.25;
40
+
41
+ /** Custom types from the pre-rework compactor, still present in old sessions. */
42
+ const LEGACY_HIDDEN_TYPES = new Set(["compactor-auto-continue", "unipi-compactor-resume"]);
33
43
 
34
44
  let lastStats: CompactionStats | null = null;
35
- let lastCompactWasCompactor = false;
45
+ /** Details of our last plan (sections, jev outcome) for the compaction card. */
46
+ let lastDetails: Record<string, unknown> | null = null;
47
+ let pendingMethod: CompactionMethod | null = null;
48
+ /** The command that started the next compaction, for the card. */
49
+ let pendingCommand: string | null = null;
50
+ let lastCommand: string | null = null;
51
+ /** Why Pi compacted (manual / threshold / overflow), captured before compaction. */
52
+ let lastReason: "manual" | "threshold" | "overflow" | undefined;
36
53
  let pendingFollowUpPrompt: string | null = null;
37
- let pendingAutoContinueTimer: ReturnType<typeof setTimeout> | null = null;
38
- export const getLastCompactionStats = () => lastStats;
39
- export const consumePendingFollowUpPrompt = (): string | null => {
40
- const p = pendingFollowUpPrompt;
41
- pendingFollowUpPrompt = null;
42
- return p;
43
- };
54
+ /** Set when a compaction was started by a UniPi command (it shows its own notice). */
55
+ let commandCompaction = false;
44
56
 
45
- // ── Invisible auto-continue (pi-vcc parity) ──────────────────────────────
46
- // Resume the agent after threshold/overflow compaction without polluting the
47
- // LLM context: send a custom message marked with a dedicated customType
48
- // (content:[], display:false, triggerTurn:true, deliverAs:'followUp') so Pi's
49
- // queue/busy-state stays coherent; the on('context') filter registered below
50
- // removes that message (by customType ONLY) from the LLM payload — the model
51
- // simply continues from the compaction summary.
52
- export const AUTO_CONTINUE_CUSTOM_TYPE = "compactor-auto-continue";
53
-
54
- const clearPendingAutoContinue = () => {
55
- if (pendingAutoContinueTimer) {
56
- clearTimeout(pendingAutoContinueTimer);
57
- pendingAutoContinueTimer = null;
58
- }
59
- };
57
+ export const getLastCompactionStats = () => lastStats;
60
58
 
61
- export const triggerInvisibleContinue = (pi: ExtensionAPI): void => {
62
- pi.sendMessage(
63
- {
64
- customType: AUTO_CONTINUE_CUSTOM_TYPE,
65
- content: [],
66
- display: false,
67
- details: undefined,
68
- },
69
- {
70
- triggerTurn: true,
71
- deliverAs: "followUp",
72
- },
73
- );
74
- };
59
+ /** Route the next compaction to a method (used by /unipi:compact-vcc and -by-llm). */
60
+ export function setPendingCompaction(method: CompactionMethod | null, fromCommand = true, command?: string): void {
61
+ pendingMethod = method;
62
+ commandCompaction = method !== null && fromCommand;
63
+ pendingCommand = method !== null ? (command ?? null) : null;
64
+ }
75
65
 
76
- const scheduleAutoContinue = (pi: ExtensionAPI) => {
77
- clearPendingAutoContinue();
78
- pendingAutoContinueTimer = setTimeout(() => {
79
- pendingAutoContinueTimer = null;
80
- try {
81
- triggerInvisibleContinue(pi);
82
- } catch {}
83
- }, 0);
84
- };
66
+ /** Card data for a compaction we (or Pi) just made. */
67
+ export function buildCardData(opts: {
68
+ method: CompactionMethod;
69
+ trigger: CompactionTrigger;
70
+ tokensBefore: number;
71
+ stats: CompactionStats | null;
72
+ details: Record<string, unknown> | null;
73
+ summary?: string;
74
+ command?: string | null;
75
+ percent?: number;
76
+ threshold?: number;
77
+ }): CompactionCardData {
78
+ const { stats, details } = opts;
79
+ const summaryChars = typeof opts.summary === "string" ? opts.summary.length : 0;
80
+ return {
81
+ method: opts.method,
82
+ trigger: opts.trigger,
83
+ ...(opts.command ? { command: opts.command } : {}),
84
+ tokensBefore: opts.tokensBefore,
85
+ ...(stats?.tokensAfterEst != null ? { tokensAfter: stats.tokensAfterEst } : {}),
86
+ ...(summaryChars ? { summaryTokens: Math.ceil(summaryChars / SUMMARY_CHARS_PER_TOKEN) } : {}),
87
+ ...(stats ? { keptTurns: stats.keptUserTurns, totalTurns: stats.totalUserTurns, keptTokens: stats.keptTokensEst } : {}),
88
+ ...(Array.isArray(details?.sections) ? { sections: details!.sections as string[] } : {}),
89
+ ...(opts.percent != null ? { percent: opts.percent } : {}),
90
+ ...(opts.threshold != null ? { threshold: opts.threshold } : {}),
91
+ };
92
+ }
85
93
 
86
- const REASON_MESSAGES: Record<string, string> = {
87
- no_live_messages: "compactor: Nothing to compact (no live messages)",
88
- too_few_live_messages: "compactor: Too few messages to compact",
94
+ export const formatCompactionStats = (stats: CompactionStats): string => {
95
+ const after = stats.tokensAfterEst != null ? ` → ~${formatTokens(stats.tokensAfterEst)}` : "";
96
+ const before = stats.tokensBefore != null ? `${formatTokens(stats.tokensBefore)}${after} tok, ` : "";
97
+ if (stats.budgetCut) {
98
+ const reason = stats.budgetCut === "no_anchor" ? "no user turn to anchor on" : "oversized tail";
99
+ return `compactor: ${before}kept ~${formatTokens(stats.keptTokensEst)} tok tail (${reason}), summarized ${stats.summarized}.`;
100
+ }
101
+ const notes = [`summarized ${stats.summarized}`];
102
+ if (stats.smartKeepAdjusted) notes.push("smart-keep");
103
+ return `compactor: ${before}kept ${stats.keptUserTurns}/${stats.totalUserTurns} turns (${notes.join(", ")}).`;
89
104
  };
90
105
 
91
106
  const dbg = (debug: boolean, data: Record<string, unknown>) => {
92
107
  if (!debug) return;
93
- try {
94
- // Lazy import so the debug path never loads node:fs in hot paths.
95
- import("node:fs").then(({ writeFileSync }) =>
96
- writeFileSync("/tmp/compactor-debug.json", JSON.stringify(data, null, 2)),
97
- ).catch(() => {});
98
- } catch {}
108
+ import("node:fs")
109
+ .then(({ writeFileSync }) => writeFileSync("/tmp/compactor-debug.json", JSON.stringify(data, null, 2)))
110
+ .catch(() => {});
99
111
  };
100
112
 
101
- const previewContent = (content: unknown): string => {
102
- if (typeof content === "string") return content.slice(0, 300);
103
- if (Array.isArray(content)) {
104
- return content
105
- .map((c: any) => {
106
- if (c?.type === "text") return c.text ?? "";
107
- if (c?.type === "toolCall") return `[toolCall:${c.name}]`;
108
- if (c?.type === "thinking") return `[thinking]`;
109
- if (c?.type === "image") return `[image:${c.mimeType}]`;
110
- return `[${c?.type ?? "unknown"}]`;
111
- })
112
- .join("\n")
113
- .slice(0, 300);
114
- }
115
- return "";
113
+ const readReason = (event: unknown): "manual" | "threshold" | "overflow" | undefined => {
114
+ const reason = (event as { reason?: unknown }).reason;
115
+ return reason === "manual" || reason === "threshold" || reason === "overflow" ? reason : undefined;
116
116
  };
117
117
 
118
- /** Format the post-compaction toast (pi-vcc formatCompactionStats parity). */
119
- export const formatCompactionStats = (stats: CompactionStats): string => {
120
- if (stats.budgetCut) {
121
- const reason = stats.budgetCut === "no_anchor" ? "no user anchor" : "oversized tail";
122
- return `compactor: kept ~${formatTokens(stats.keptTokensEst)} tok tail (mid-turn cut, ${reason}), summarized ${stats.summarized}.`;
123
- }
124
- const notes: string[] = [`summarized ${stats.summarized}`];
125
- if (stats.smartKeepAdjusted) {
126
- notes.push("smart-keep");
127
- }
128
- return `compactor: kept ${stats.keptUserTurns}/${stats.totalUserTurns} turns, ~${formatTokens(stats.keptTokensEst)} tok (${notes.join(", ")}).`;
118
+ const activeWorkText = (): string => {
119
+ const blocks = collectCompactionContext();
120
+ return blocks.length === 0 ? "" : `[Active Work]\n${blocks.map((b) => b.text).join("\n\n")}`;
129
121
  };
130
122
 
131
- /** Count chars in a content part array (legacy helper, kept for stats parity). */
132
- function contentPartsChars(parts: Array<{ text?: string; name?: string; input?: unknown; content?: unknown }>): number {
133
- return parts.reduce((s: number, p) => {
134
- if (p.text) return s + p.text.length;
135
- if (p.name) {
136
- const inputStr = typeof p.input === "string" ? p.input : JSON.stringify(p.input ?? "");
137
- return s + p.name.length + inputStr.length;
138
- }
139
- if (p.content !== undefined) {
140
- const contentStr = typeof p.content === "string" ? p.content : JSON.stringify(p.content ?? "");
141
- return s + contentStr.length;
142
- }
143
- return s;
144
- }, 0);
123
+ // ── lossless plan ────────────────────────────────────────
124
+
125
+ export interface LosslessPlanInput {
126
+ branchEntries: readonly any[];
127
+ tokensBefore?: number;
128
+ previousSummary?: string;
129
+ fileOps?: FileOps;
130
+ keepUserTurns?: number | null;
131
+ keepExplicit?: boolean;
132
+ /** The model's context window; the kept tail is sized to fit it. */
133
+ contextWindow?: number;
134
+ config: CompactorConfig;
135
+ cwd?: string;
136
+ reason?: string;
145
137
  }
146
138
 
147
- /** Estimate char count for an AgentMessage (unwrapped — has role + content directly) */
148
- export function messageChars(msg: { content: unknown }): number {
149
- const c = msg.content;
150
- if (typeof c === "string") return c.length;
151
- if (Array.isArray(c)) return contentPartsChars(c as Array<{ text?: string; name?: string; input?: unknown; content?: unknown }>);
152
- return 0;
139
+ export type LosslessPlan =
140
+ | { ok: true; summary: string; firstKeptEntryId: string; details: Record<string, unknown>; stats: CompactionStats; messageCount: number }
141
+ | { ok: false; reason: "no_live_messages" | "too_few_live_messages" };
142
+
143
+ /** Build a lossless compaction for the given branch (pure apart from active-work providers). */
144
+ export function planLosslessCompaction(input: LosslessPlanInput): LosslessPlan {
145
+ const branch = input.branchEntries as any[];
146
+ const { config } = input;
147
+ const explicit = input.keepExplicit === true;
148
+
149
+ // Calibrate chars/token from Pi's real token count vs the live message chars.
150
+ const calibrationCut = buildOwnCut(branch, 0);
151
+ const calibrationChars = calibrationCut.ok
152
+ ? calibrationCut.messages.reduce((sum: number, m: any) => sum + estimateMessageContentChars(m.content), 0)
153
+ : 0;
154
+ const tokenEstimate = calibrateCharsPerToken(calibrationChars + (input.previousSummary?.length ?? 0), input.tokensBefore);
155
+ const cpt = tokenEstimate.charsPerToken;
156
+
157
+ const smartKeep = resolveSmartKeepUserTurns({
158
+ branchEntries: branch,
159
+ requestedKeepUserTurns: explicit ? (input.keepUserTurns ?? 1) : null,
160
+ explicit,
161
+ smartKeepTail: config.smartKeepTail,
162
+ charsPerToken: cpt,
163
+ });
164
+ let cut = buildOwnCut(branch, smartKeep.keepUserTurns);
165
+ if (cut.ok && !explicit) cut = applyTailBudget(branch, cut, { charsPerToken: cpt });
166
+ // Whatever keep asked for, the tail must leave room for the system prompt,
167
+ // the summary and the next turns: recut above 40% of the window (to 25%).
168
+ if (cut.ok && input.contextWindow && input.contextWindow > 0) {
169
+ cut = applyTailBudget(branch, cut, {
170
+ charsPerToken: cpt,
171
+ maxTokens: Math.floor(input.contextWindow * TAIL_WINDOW_SHARE),
172
+ oversizedFactor: TAIL_WINDOW_LIMIT / TAIL_WINDOW_SHARE,
173
+ });
174
+ }
175
+ if (!cut.ok) return { ok: false, reason: cut.reason };
176
+
177
+ const keptIdx = cut.firstKeptEntryId ? branch.findIndex((e) => e.id === cut.firstKeptEntryId) : -1;
178
+ const endIdx = keptIdx >= 0 ? keptIdx : branch.length;
179
+ const keptChars = keptIdx >= 0
180
+ ? branch.slice(keptIdx).reduce((sum, e) => sum + (e.type === "message" ? estimateMessageContentChars(e.message?.content) : 0), 0)
181
+ : 0;
182
+ const keptTokens = estimateTokensFromChars(keptChars, cpt);
183
+
184
+ const source = collectSummarySource(branch, endIdx);
185
+ const wanted = config.summaryBudgetTokens > 0 ? config.summaryBudgetTokens : autoBudgetTokens(source.blocks.length);
186
+ // Small-context models: the summary may not crowd out the work (≤8% of the window).
187
+ const budgetTokens = input.contextWindow && input.contextWindow > 0 ? Math.min(wanted, Math.max(800, Math.floor(input.contextWindow * 0.08))) : wanted;
188
+ const summaryInput: LosslessSummaryInput = {
189
+ source,
190
+ activeWork: config.sections.activeWork ? collectCompactionContext() : [],
191
+ // Summary text is plain prose: a fixed ratio, not the session's calibration
192
+ // (which images and code skew).
193
+ budgetChars: budgetTokens * SUMMARY_CHARS_PER_TOKEN,
194
+ cwd: input.cwd,
195
+ fileOps: input.fileOps,
196
+ sections: config.sections,
197
+ };
198
+ const summary = buildLosslessSummary(summaryInput);
199
+
200
+ const tokensAfter = keptTokens + estimateTokensFromChars(summary.text.length, SUMMARY_CHARS_PER_TOKEN);
201
+ const keptMessages = keptIdx >= 0 ? branch.slice(keptIdx).filter((e) => e.type === "message").length : 0;
202
+ const stats: CompactionStats = {
203
+ summarized: cut.messages.length,
204
+ kept: keptMessages,
205
+ totalMessages: cut.messages.length + keptMessages,
206
+ tokensBefore: input.tokensBefore,
207
+ tokensAfterEst: tokensAfter,
208
+ keptUserTurns: cut.keptUserTurns,
209
+ totalUserTurns: cut.totalUserTurns,
210
+ requestedKeepUserTurns: cut.requestedKeepUserTurns,
211
+ keepUserTurnsExplicit: explicit,
212
+ keepFallbackToCompactAll: cut.keepFallbackToCompactAll,
213
+ keptTokensEst: keptTokens,
214
+ smartKeepAdjusted: smartKeep.smartAdjusted,
215
+ smartFromKeep: smartKeep.fromKeep,
216
+ budgetCut: cut.budgetCut as BudgetCutKind | undefined,
217
+ };
218
+ return {
219
+ ok: true,
220
+ summary: summary.text,
221
+ firstKeptEntryId: cut.firstKeptEntryId,
222
+ messageCount: cut.messages.length,
223
+ stats,
224
+ details: {
225
+ compactor: COMPACTOR_ID,
226
+ version: 2,
227
+ method: "vcc",
228
+ sections: summary.sections,
229
+ sourceMessageCount: cut.messages.length,
230
+ tokensAfter,
231
+ budgetTokens,
232
+ reason: input.reason,
233
+ budgetCut: cut.budgetCut,
234
+ },
235
+ };
153
236
  }
154
237
 
155
- /** Filter entries to only SessionMessageEntry */
156
- function filterMessageEntries(entries: SessionEntry[]): SessionMessageEntry[] {
157
- return entries.filter((e): e is SessionMessageEntry => e.type === "message");
238
+ // ── model summary ────────────────────────────────────────
239
+
240
+ async function requestAuth(ctx: ExtensionContext) {
241
+ const model = ctx.model;
242
+ if (!model) throw new Error("no model selected");
243
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
244
+ if (!auth.ok) throw new Error(auth.error);
245
+ return {
246
+ model: auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model,
247
+ apiKey: auth.apiKey,
248
+ headers: auth.headers as Record<string, string> | undefined,
249
+ env: auth.env,
250
+ };
158
251
  }
159
252
 
160
- const readCompactionEventContext = (event: unknown): { reason?: "manual" | "threshold" | "overflow"; willRetry: boolean } => {
161
- const raw = event as { reason?: unknown; willRetry?: unknown };
162
- const reason = raw.reason === "manual" || raw.reason === "threshold" || raw.reason === "overflow"
163
- ? raw.reason
164
- : undefined;
165
- return { reason, willRetry: raw.willRetry === true };
253
+ const joinInstructions = (...parts: Array<string | null | undefined>): string | undefined => {
254
+ const text = parts.map((p) => p?.trim()).filter(Boolean).join("\n\n");
255
+ return text || undefined;
166
256
  };
167
257
 
168
- export function registerCompactionHooks(
258
+ const withActiveWork = (summary: string): string => {
259
+ const active = activeWorkText();
260
+ return active ? `${active}\n\n${summary}` : summary;
261
+ };
262
+
263
+ /** Pi's own summarizer over Pi's preparation, with active work prepended. */
264
+ async function llmFromPreparation(
169
265
  pi: ExtensionAPI,
170
- deps?: { getSessionDB?: () => SessionDB | null; getSessionId?: () => string },
171
- ): void {
172
- // Filter our invisible-continue marker out of the LLM context payload so the
173
- // model just continues from the compaction summary (matched by customType ONLY).
174
- // This replaces the old dead sanitizer branch that read a nonexistent
175
- // event.context string (prefix-cache audit finding).
176
- pi.on("context", (event) => {
177
- const messages = (event as { messages: Array<{ role?: string; customType?: string }> }).messages.filter((message) => {
178
- if (message.role !== "custom") return true;
179
- return message.customType !== AUTO_CONTINUE_CUSTOM_TYPE;
180
- });
181
- if (messages.length !== (event as { messages: unknown[] }).messages.length) {
182
- return { messages } as any;
183
- }
184
- });
266
+ event: SessionBeforeCompactEvent,
267
+ ctx: ExtensionContext,
268
+ instructions: string | undefined,
269
+ ) {
270
+ const { model, apiKey, headers, env } = await requestAuth(ctx);
271
+ const result = await piCompact(
272
+ event.preparation,
273
+ model,
274
+ apiKey,
275
+ headers,
276
+ instructions,
277
+ event.signal,
278
+ pi.getThinkingLevel?.(),
279
+ undefined,
280
+ env,
281
+ );
282
+ return {
283
+ summary: withActiveWork(result.summary),
284
+ firstKeptEntryId: result.firstKeptEntryId,
285
+ tokensBefore: result.tokensBefore,
286
+ usage: result.usage,
287
+ details: { ...(result.details as object | undefined), compactor: COMPACTOR_ID, version: 2, method: "llm" },
288
+ };
289
+ }
185
290
 
186
- pi.on("before_agent_start", () => {
187
- clearPendingAutoContinue();
188
- });
291
+ // ── registration ─────────────────────────────────────────
189
292
 
190
- pi.on("session_before_compact", (event: SessionBeforeCompactEvent, ctx) => {
191
- const { preparation, branchEntries, customInstructions } = event;
192
- const config = loadConfig();
193
- const { isCompactor, keepUserTurns, keepUserTurnsExplicit, followUpPrompt } =
194
- parseCompactionInstructions(customInstructions);
293
+ export interface CompactionHookDeps {
294
+ counters: RuntimeCounters;
295
+ }
195
296
 
196
- // Always handle explicit compactor marker.
197
- // Otherwise, only handle when user opted in via settings.
198
- if (!isCompactor && !config.overrideDefaultCompaction) {
199
- return;
200
- }
297
+ export function registerCompactionHooks(pi: ExtensionAPI, deps: CompactionHookDeps): void {
298
+ let autoState: AutoCompactionState = createAutoCompactionState();
299
+
300
+ const afterCompaction = (stats: CompactionStats | null, method: CompactionMethod) => {
301
+ deps.counters.compactions++;
302
+ emitEvent(pi, UNIPI_EVENTS.COMPACTOR_COMPACTED, {
303
+ method,
304
+ summarized: stats?.summarized ?? 0,
305
+ kept: stats?.kept ?? 0,
306
+ tokensBefore: stats?.tokensBefore ?? 0,
307
+ tokensAfter: stats?.tokensAfterEst ?? 0,
308
+ tokensSaved: Math.max(0, (stats?.tokensBefore ?? 0) - (stats?.tokensAfterEst ?? 0)),
309
+ });
310
+ };
201
311
 
202
- pendingFollowUpPrompt = null;
312
+ // Old sessions may still carry the removed auto-continue marker and the
313
+ // (huge) continuity snapshot; neither belongs in the model's context.
314
+ pi.on("context", (event) => {
315
+ const messages = (event as { messages: Array<{ role?: string; customType?: string }> }).messages;
316
+ const filtered = messages.filter((m) => m.role !== "custom" || !LEGACY_HIDDEN_TYPES.has(m.customType ?? ""));
317
+ if (filtered.length !== messages.length) return { messages: filtered } as any;
318
+ });
203
319
 
204
- // Calibrate chars/token from Pi's real token count vs actual message chars.
205
- const calibrationCut = buildOwnCut(branchEntries as any[], 0);
206
- const calibrationMessageChars = calibrationCut.ok
207
- ? calibrationCut.messages.reduce(
208
- (sum: number, message) => sum + estimateMessageContentChars((message as any).content),
209
- 0,
210
- )
211
- : 0;
212
- const calibrationSummaryChars = typeof preparation.previousSummary === "string"
213
- ? preparation.previousSummary.length
214
- : 0;
215
- const tokenEstimate = calibrateCharsPerToken(
216
- calibrationMessageChars + calibrationSummaryChars,
217
- preparation.tokensBefore,
218
- );
219
-
220
- // Smart keep-tail: boost default keep when the tail is small.
221
- // Explicit keep:N from the user is always respected (resolver no-ops).
222
- const smartKeep = resolveSmartKeepUserTurns({
223
- branchEntries: branchEntries as any[],
224
- requestedKeepUserTurns: keepUserTurnsExplicit ? keepUserTurns : null,
225
- explicit: keepUserTurnsExplicit,
226
- smartKeepTail: config.smartKeepTail,
227
- charsPerToken: tokenEstimate.charsPerToken,
228
- });
229
- let ownCut = buildOwnCut(branchEntries as any[], smartKeep.keepUserTurns);
230
- // Default path only: rescue autonomous / oversized-tail sessions with a
231
- // token-budget cut. Explicit keep:N is respected absolutely (no-op here).
232
- if (ownCut.ok && !keepUserTurnsExplicit) {
233
- ownCut = applyTailBudget(branchEntries as any[], ownCut, { charsPerToken: tokenEstimate.charsPerToken });
234
- }
235
- if (!ownCut.ok) {
236
- if (!isCompactor && (readCompactionEventContext(event).reason === "overflow")) {
237
- return; // let pi core retry
238
- }
239
- try {
240
- ctx?.ui?.notify?.(REASON_MESSAGES[ownCut.reason] ?? ownCut.reason, "warning");
241
- } catch {}
242
- dbg(config.debug, { cancelled: true, reason: ownCut.reason });
243
- return { cancel: true };
320
+ // Mark user messages that extensions send, so summaries can tell them from
321
+ // the user's own words (see source.ts).
322
+ pi.on("input", (event) => {
323
+ if (event.source !== "extension" || !event.text?.trim()) return;
324
+ try {
325
+ pi.appendEntry(ORIGIN_ENTRY_TYPE, { key: originKey(event.text) });
326
+ } catch {
327
+ // Never block input on bookkeeping.
244
328
  }
329
+ });
245
330
 
246
- pendingFollowUpPrompt = followUpPrompt;
247
- const agentMessages = ownCut.messages;
248
- const firstKeptEntryId = ownCut.firstKeptEntryId;
249
- const messages = convertToLlm(agentMessages as any);
250
-
251
- // Count kept messages and estimate tokens
252
- const keptIdx = (branchEntries as SessionEntry[]).findIndex((e: SessionEntry) => e.id === firstKeptEntryId);
253
- const keptMessageEntries: SessionMessageEntry[] = keptIdx >= 0
254
- ? filterMessageEntries((branchEntries as SessionEntry[]).slice(keptIdx))
255
- : [];
256
- const keptChars = keptMessageEntries.reduce(
257
- (sum: number, e: SessionMessageEntry) => sum + estimateMessageContentChars((e.message as any)?.content),
258
- 0,
259
- );
260
-
261
- const tokensBefore = preparation.tokensBefore;
262
- lastStats = {
263
- summarized: agentMessages.length,
264
- kept: keptMessageEntries.length,
265
- totalMessages: agentMessages.length + keptMessageEntries.length,
266
- tokensBefore,
267
- tokensAfterEst: estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken),
268
- keptUserTurns: ownCut.keptUserTurns,
269
- totalUserTurns: ownCut.totalUserTurns,
270
- requestedKeepUserTurns: ownCut.requestedKeepUserTurns,
271
- keepUserTurnsExplicit,
272
- keepFallbackToCompactAll: ownCut.keepFallbackToCompactAll,
273
- keptTokensEst: estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken),
274
- smartKeepAdjusted: smartKeep.smartAdjusted,
275
- smartFromKeep: smartKeep.fromKeep,
276
- budgetCut: ownCut.budgetCut,
277
- };
331
+ pi.on("session_start", () => {
332
+ autoState = createAutoCompactionState();
333
+ pendingMethod = null;
334
+ commandCompaction = false;
335
+ });
278
336
 
279
- // Persist cumulative compaction stats
280
- const sessionDB = deps?.getSessionDB?.();
281
- if (sessionDB && deps?.getSessionId) {
337
+ pi.on("session_before_compact", async (event: SessionBeforeCompactEvent, ctx) => {
338
+ const cwd = ctx?.cwd ?? process.cwd();
339
+ const config = loadConfig(cwd);
340
+ const reason = readReason(event);
341
+ const parsed = parseCompactionInstructions(event.customInstructions);
342
+ const method: CompactionMethod = pendingMethod
343
+ ?? (parsed.isCompactor
344
+ ? "vcc"
345
+ : reason === "manual" && config.piCompact !== "follow"
346
+ ? config.piCompact
347
+ : config.method);
348
+ const fromCommand = pendingMethod !== null && commandCompaction;
349
+ lastReason = reason;
350
+ lastCommand = fromCommand ? pendingCommand : null;
351
+ lastDetails = null;
352
+ pendingMethod = null;
353
+ pendingCommand = null;
354
+ pendingFollowUpPrompt = null;
355
+
356
+ if (method === "llm") {
357
+ const userInstructions = parsed.isCompactor ? undefined : event.customInstructions;
282
358
  try {
283
- const sessionId = deps.getSessionId();
284
- const summarizedChars = agentMessages.reduce((sum, msg) => sum + messageChars(msg as any), 0);
285
- sessionDB.addCompactionStats(sessionId, summarizedChars, keptChars, agentMessages.length);
286
- } catch {
287
- // non-fatal
359
+ const compaction = await llmFromPreparation(pi, event, ctx, joinInstructions(userInstructions, config.llmInstructions));
360
+ lastStats = null;
361
+ return { compaction };
362
+ } catch (err) {
363
+ if (event.signal?.aborted) return { cancel: true };
364
+ // Fall back to Pi's own path (same summarizer, Pi's auth handling).
365
+ dbg(config.debug, { method: "llm", fallback: true, error: String(err) });
366
+ return;
288
367
  }
289
368
  }
290
369
 
291
- // Ranked compaction: keep the highest-signal blocks under a token budget
292
- // instead of the old unranked compile() (fixed 120-line cap). The token
293
- // budget is converted to a char budget via the session's calibrated
294
- // charsPerToken so the summary targets ~RANKED_BRIEF_BUDGET_TOKENS tokens
295
- // regardless of content density. The budget is SIZE-RELATIVE: it scales
296
- // with transcript length between a floor and a ceiling at
297
- // RANKED_BRIEF_CHARS_PER_BLOCK per normalized block (pi-vcc parity).
298
- const RANKED_BRIEF_BUDGET_TOKENS = 1100;
299
- const RANKED_BRIEF_CEILING_TOKENS = 2000;
300
- const RANKED_BRIEF_TOKENS_PER_BLOCK = 15;
301
- const summary = compileRanked({
302
- messages: messages as any,
303
- previousSummary: preparation.previousSummary,
304
- fileOps: preparation.fileOps
370
+ const planInput: LosslessPlanInput = {
371
+ branchEntries: event.branchEntries as any[],
372
+ tokensBefore: event.preparation.tokensBefore,
373
+ previousSummary: event.preparation.previousSummary,
374
+ fileOps: event.preparation.fileOps
305
375
  ? {
306
- readFiles: [...preparation.fileOps.read],
307
- modifiedFiles: [...preparation.fileOps.written, ...preparation.fileOps.edited],
376
+ readFiles: [...event.preparation.fileOps.read],
377
+ modifiedFiles: [...event.preparation.fileOps.written, ...event.preparation.fileOps.edited],
308
378
  }
309
379
  : undefined,
310
- ranking: {
311
- maxBriefChars: Math.round(RANKED_BRIEF_BUDGET_TOKENS * tokenEstimate.charsPerToken),
312
- maxBriefCharsCeiling: Math.round(RANKED_BRIEF_CEILING_TOKENS * tokenEstimate.charsPerToken),
313
- briefCharsPerBlock: Math.round(RANKED_BRIEF_TOKENS_PER_BLOCK * tokenEstimate.charsPerToken),
314
- },
315
- });
316
-
317
- const details = {
318
- compactor: "@pi-unipi/compactor",
319
- version: 1,
320
- sections: [...summary.matchAll(/^\[(.+?)\]/gm)].map((m) => m[1]),
321
- sourceMessageCount: agentMessages.length,
322
- previousSummaryUsed: Boolean(preparation.previousSummary),
323
- reason: readCompactionEventContext(event).reason,
324
- budgetCut: ownCut.budgetCut,
380
+ keepUserTurns: parsed.keepUserTurns,
381
+ keepExplicit: parsed.keepUserTurnsExplicit,
382
+ config,
383
+ cwd,
384
+ reason,
385
+ contextWindow: ctx?.model?.contextWindow,
325
386
  };
387
+ const plan = planLosslessCompaction(planInput);
326
388
 
327
- lastCompactWasCompactor = isCompactor;
328
-
329
- dbg(config.debug, {
330
- usedOwnCut: true,
331
- budgetCut: ownCut.budgetCut,
332
- messagesToSummarize: agentMessages.length,
333
- firstKeptEntryId,
334
- tokensBefore,
335
- tokenEstimate,
336
- smartKeep,
337
- summaryLength: summary.length,
338
- summaryPreview: summary.slice(0, 500),
339
- sections: details.sections,
340
- });
389
+ if (!plan.ok) {
390
+ // Overflow must still recover: let Pi's own summarizer handle it.
391
+ if (reason === "overflow") return;
392
+ if (reason === "manual" || fromCommand) {
393
+ try {
394
+ ctx?.ui?.notify?.(plan.reason === "no_live_messages" ? "compactor: nothing to compact" : "compactor: too few messages to compact", "warning");
395
+ } catch {}
396
+ }
397
+ dbg(config.debug, { cancelled: true, reason: plan.reason });
398
+ return { cancel: true };
399
+ }
341
400
 
401
+ lastStats = plan.stats;
402
+ lastDetails = plan.details;
403
+ if (!parsed.isCompactor && parsed.followUpPrompt && reason === "manual") pendingFollowUpPrompt = parsed.followUpPrompt;
404
+ dbg(config.debug, { method: "vcc", reason, stats: plan.stats, details: plan.details, summaryPreview: plan.summary.slice(0, 800) });
342
405
  return {
343
406
  compaction: {
344
- summary,
345
- details,
346
- tokensBefore: preparation.tokensBefore,
347
- firstKeptEntryId,
407
+ summary: plan.summary,
408
+ details: plan.details,
409
+ tokensBefore: event.preparation.tokensBefore,
410
+ firstKeptEntryId: plan.firstKeptEntryId,
348
411
  },
349
412
  };
350
413
  });
351
414
 
352
415
  pi.on("session_compact", (event: SessionCompactEvent, ctx) => {
353
- if (!event.fromExtension) return;
354
- if (lastCompactWasCompactor) return; // /unipi:compact handles its own toast
355
- const stats = lastStats;
356
- if (!stats) return;
357
- const { reason, willRetry } = readCompactionEventContext(event);
358
- if (willRetry) return;
359
- const followUpPrompt = consumePendingFollowUpPrompt();
360
- const shouldContinueAfterAutoCompact =
361
- (reason === "threshold" || reason === "overflow") && loadConfig().continueAfterThresholdCompact;
362
- setTimeout(() => {
416
+ const details = (event.compactionEntry as { details?: { compactor?: string; method?: CompactionMethod } } | undefined)?.details;
417
+ const ours = details?.compactor === COMPACTOR_ID;
418
+ const method: CompactionMethod = !ours ? "llm" : details?.method === "llm" ? "llm" : "vcc";
419
+ afterCompaction(ours ? lastStats : null, method);
420
+
421
+ const wasCommand = commandCompaction;
422
+ commandCompaction = false;
423
+ const followUp = pendingFollowUpPrompt;
424
+ pendingFollowUpPrompt = null;
425
+ // One line in the transcript (details on ctrl+o) instead of a toast.
426
+ // Commands always show it; automatic compactions follow Notifications.
427
+ if (wasCommand || loadConfig(ctx?.cwd ?? process.cwd()).notify) {
428
+ const entry = event.compactionEntry as { tokensBefore?: number; summary?: string } | undefined;
363
429
  try {
364
- ctx?.ui?.notify?.(formatCompactionStats(stats), "info");
430
+ pi.appendEntry(
431
+ CARD_TYPE,
432
+ buildCardData({
433
+ method,
434
+ trigger: lastReason === "threshold" || lastReason === "overflow" ? lastReason : "manual",
435
+ tokensBefore: Number(entry?.tokensBefore ?? lastStats?.tokensBefore ?? 0),
436
+ stats: ours ? lastStats : null,
437
+ details: ours ? lastDetails : null,
438
+ summary: entry?.summary,
439
+ command: wasCommand ? lastCommand : null,
440
+ }),
441
+ );
365
442
  } catch {}
366
- }, 500);
367
- if (followUpPrompt) {
443
+ }
444
+ if (followUp) {
368
445
  setTimeout(() => {
369
446
  try {
370
- void pi.sendUserMessage(followUpPrompt);
447
+ pi.sendUserMessage(followUp, { deliverAs: "followUp" });
371
448
  } catch {}
372
449
  }, 0);
373
- } else if (shouldContinueAfterAutoCompact) {
374
- scheduleAutoContinue(pi);
375
450
  }
376
451
  });
452
+
453
+ // Percentage trigger: compact at the turn boundary (no abort, loops continue).
454
+ pi.on("turn_end", async (event, ctx) => {
455
+ const cwd = ctx?.cwd ?? process.cwd();
456
+ const config = loadConfig(cwd);
457
+ if (config.trigger !== "percent") return;
458
+ if (event.outcome === "aborted") return;
459
+ if (event.entries.some((draft) => draft.type === "compaction")) return;
460
+
461
+ const decision = decideAutoCompaction({
462
+ config: autoCompactionOf(config),
463
+ usage: ctx.getContextUsage?.(),
464
+ state: autoState,
465
+ nowMs: Date.now(),
466
+ });
467
+ autoState = decision.state;
468
+ if (!decision.shouldTrigger) return;
469
+
470
+ const branch = ctx.sessionManager.getBranch() as any[];
471
+ try {
472
+ const draft = config.method === "llm"
473
+ ? await boundaryLlmDraft(pi, ctx, branch, config)
474
+ : await boundaryLosslessDraft(branch, config, cwd, decision.usage?.tokens, ctx.model?.contextWindow);
475
+ if (!draft) {
476
+ autoState = markAutoCompactionError(autoState, Date.now());
477
+ return;
478
+ }
479
+ autoState = markAutoCompactionComplete(autoState);
480
+ afterCompaction(draft.stats, config.method);
481
+ // Boundary compactions skip session_compact: the card rides in the same draft, after the compaction.
482
+ const card = config.notify
483
+ ? [{
484
+ type: "custom" as const,
485
+ customType: CARD_TYPE,
486
+ data: buildCardData({
487
+ method: config.method,
488
+ trigger: "percent",
489
+ tokensBefore: Number(draft.entry.usage?.tokensBefore ?? decision.usage?.tokens ?? draft.stats?.tokensBefore ?? 0),
490
+ stats: draft.stats,
491
+ details: (draft.entry.details as Record<string, unknown> | undefined) ?? null,
492
+ summary: draft.entry.summary,
493
+ percent: decision.usage?.percent,
494
+ threshold: decision.thresholdPercent,
495
+ }),
496
+ }]
497
+ : [];
498
+ return { entries: [draft.entry, ...card] };
499
+ } catch (err) {
500
+ autoState = markAutoCompactionError(autoState, Date.now());
501
+ if (config.notify) ctx.ui.notify(`Auto-compaction failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
502
+ return;
503
+ }
504
+ });
505
+ }
506
+
507
+ type BoundaryDraft = {
508
+ entry: { type: "compaction"; summary: string; firstKeptEntryId: string | null; details?: unknown; usage?: any };
509
+ stats: CompactionStats | null;
510
+ };
511
+
512
+ async function boundaryLosslessDraft(branch: any[], config: CompactorConfig, cwd: string, tokensBefore?: number, contextWindow?: number): Promise<BoundaryDraft | null> {
513
+ const input: LosslessPlanInput = { branchEntries: branch, tokensBefore, config, cwd, reason: "percent", contextWindow };
514
+ const plan = planLosslessCompaction(input);
515
+ if (!plan.ok) return null;
516
+ lastStats = plan.stats;
517
+ return {
518
+ entry: { type: "compaction", summary: plan.summary, firstKeptEntryId: plan.firstKeptEntryId || null, details: plan.details },
519
+ stats: plan.stats,
520
+ };
521
+ }
522
+
523
+ async function boundaryLlmDraft(pi: ExtensionAPI, ctx: ExtensionContext, branch: any[], config: CompactorConfig): Promise<BoundaryDraft | null> {
524
+ const cut = buildOwnCut(branch, 1);
525
+ const tail = cut.ok ? applyTailBudget(branch, cut) : cut;
526
+ if (!tail.ok || tail.messages.length === 0) return null;
527
+ const previous = [...branch].reverse().find((e) => e.type === "compaction")?.summary as string | undefined;
528
+ const { model, apiKey, headers, env } = await requestAuth(ctx);
529
+ const result = await generateSummaryWithUsage(
530
+ tail.messages as any,
531
+ model,
532
+ DEFAULT_COMPACTION_SETTINGS.reserveTokens,
533
+ apiKey,
534
+ headers,
535
+ undefined,
536
+ joinInstructions(config.llmInstructions),
537
+ previous,
538
+ pi.getThinkingLevel?.(),
539
+ undefined,
540
+ env,
541
+ );
542
+ lastStats = null;
543
+ return {
544
+ entry: {
545
+ type: "compaction",
546
+ summary: withActiveWork(result.text),
547
+ firstKeptEntryId: tail.firstKeptEntryId || null,
548
+ details: { compactor: COMPACTOR_ID, version: 2, method: "llm", reason: "percent" },
549
+ usage: result.usage,
550
+ },
551
+ stats: null,
552
+ };
377
553
  }
378
554
 
379
555
  export { MAX_SMART_TAIL_TOKENS };