@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,60 +1,594 @@
1
1
  /**
2
- * Main compile() orchestrator — all 6 stages
2
+ * Lossless (zero-LLM) summary — state first, hard-budgeted, rebuilt from the
3
+ * full session history every time.
4
+ *
5
+ * Section order is what a model resuming the work needs first:
6
+ * Active Work → Your Requests → Latest State → Decisions & Constraints →
7
+ * Files → Commits → Open Errors → Recent Transcript
8
+ * Every section has its own share of the budget and every item is clipped, so
9
+ * no single message (a pasted log, an injected snapshot) can blow the summary up.
3
10
  */
4
11
 
5
- import type { Message } from "@earendil-works/pi-ai";
6
- import type { CompileInput, FileOps, NormalizedBlock } from "../types.js";
7
- import { normalizeMessages } from "./normalize.js";
8
- import { filterNoise } from "./filter-noise.js";
9
- import { buildSections } from "./build-sections.js";
10
- import { formatSummary, RECALL_NOTE } from "./format.js";
11
- import { mergePrevious, mergeBriefTranscriptWithFreshBudget } from "./merge.js";
12
- import { selectRankedBriefBlocks, type BriefRankingOptions } from "./rank.js";
12
+ import { relative, isAbsolute } from "node:path";
13
+ import type { FileOps, NormalizedBlock, SummarySections } from "../types.js";
14
+ import type { SummarySource } from "./source.js";
15
+ import { extractPath } from "./extract/files.js";
16
+ import { selectRankedBriefBlocks } from "./rank.js";
17
+ import { compileBrief } from "./brief.js";
13
18
 
14
- export interface RankedCompileInput extends CompileInput {
15
- fileOps?: FileOps;
16
- ranking?: BriefRankingOptions;
17
- }
18
-
19
- interface CompileWithBriefBlocksOptions {
20
- briefBlocksFor?: (blocks: NormalizedBlock[]) => NormalizedBlock[];
21
- capFreshBrief?: boolean;
22
- preserveFreshBriefOnMerge?: boolean;
23
- }
24
-
25
- const compileWithBriefBlocks = (input: CompileInput, options: CompileWithBriefBlocksOptions = {}): string => {
26
- const blocks = filterNoise(normalizeMessages(input.messages));
27
- const briefBlocks = options.briefBlocksFor?.(blocks);
28
- const data = buildSections({ blocks, briefBlocks });
29
- const fresh = formatSummary(data, { capBriefTranscript: options.capFreshBrief ?? true });
30
- const prev = input.previousSummary
31
- ? stripRecallNote(input.previousSummary)
32
- : undefined;
33
- const merged = prev
34
- ? mergePrevious(prev, fresh, { preserveFreshBrief: options.preserveFreshBriefOnMerge })
35
- : fresh;
36
- if (!merged) return "";
37
- return merged + "\n\n---\n\n" + RECALL_NOTE;
19
+ export const RECALL_NOTE =
20
+ "Everything above is condensed from the full session history, which is kept. " +
21
+ "Use `session_recall` to search it (refs like #123 are entry indices; pass them in `expand` for full content). " +
22
+ "Do not redo work that is already done.";
23
+
24
+ export const DEFAULT_SECTIONS: SummarySections = {
25
+ activeWork: true,
26
+ requests: true,
27
+ state: true,
28
+ decisions: true,
29
+ files: true,
30
+ commits: true,
31
+ errors: true,
32
+ lessons: true,
33
+ transcript: true,
38
34
  };
39
35
 
40
- export const compile = (input: CompileInput): string =>
41
- compileWithBriefBlocks(input);
42
-
43
- export const compileRanked = (input: RankedCompileInput): string =>
44
- compileWithBriefBlocks(input, {
45
- briefBlocksFor: (blocks) =>
46
- selectRankedBriefBlocks(blocks, {
47
- ...input.ranking,
48
- fileOps: input.ranking?.fileOps ?? input.fileOps,
49
- }),
50
- capFreshBrief: false,
51
- preserveFreshBriefOnMerge: true,
52
- });
53
-
54
- const stripRecallNote = (text: string): string => {
55
- const idx = text.lastIndexOf(RECALL_NOTE);
56
- if (idx < 0) return text;
57
- return text.slice(0, idx).replace(/\s*(?:\n\n---\n\n)?\s*$/, "").trimEnd();
36
+ export interface LosslessSummaryInput {
37
+ source: SummarySource;
38
+ /** Authoritative active-work blocks from modules (goal, ralph, kanboard…). */
39
+ activeWork?: ReadonlyArray<{ id: string; text: string }>;
40
+ /** Target size in characters (tokens × chars/token). */
41
+ budgetChars: number;
42
+ cwd?: string;
43
+ fileOps?: FileOps;
44
+ sections?: SummarySections;
45
+ }
46
+
47
+ export interface LosslessSummary {
48
+ text: string;
49
+ sections: string[];
50
+ }
51
+
52
+ /** Auto budget in tokens: scales with the amount of history, 1.5k–4k. */
53
+ export function autoBudgetTokens(blockCount: number): number {
54
+ return Math.round(Math.min(4000, Math.max(1500, 1500 + blockCount * 8)));
55
+ }
56
+
57
+ // ── text helpers ─────────────────────────────────────────
58
+
59
+ const oneLine = (text: string): string => text.replace(/\s+/g, " ").trim();
60
+
61
+ /** Stable identity of a summary item (survives clipping to ≥80 chars). */
62
+ export const itemKey = (text: string): string => oneLine(text).slice(0, 80).toLowerCase();
63
+
64
+
65
+ export function clip(text: string, max: number): string {
66
+ if (max <= 1) return "";
67
+ return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
68
+ }
69
+
70
+ /** Add items while they fit in maxChars (each counted with its "- " line). */
71
+ function fitItems(items: string[], maxChars: number): string[] {
72
+ const out: string[] = [];
73
+ let used = 0;
74
+ for (const item of items) {
75
+ const cost = item.length + 3;
76
+ if (used + cost > maxChars) break;
77
+ out.push(item);
78
+ used += cost;
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function section(title: string, lines: string[]): string {
84
+ return lines.length === 0 ? "" : `[${title}]\n${lines.map((l) => `- ${l}`).join("\n")}`;
85
+ }
86
+
87
+ // ── Your Requests ────────────────────────────────────────
88
+
89
+ const TRIVIAL_REQUEST_RE =
90
+ /^(?:continue|go on|go ahead|carry on|keep going|proceed|next|go|ok(?:ay)?|yes|yep|y|sure|thanks?|thank you|ty|k|done|resume|restarted[^.]*)[\s.!~]*$/i;
91
+
92
+ export interface RequestSelection {
93
+ lines: string[];
94
+ /** Indices (into the input) of requests shown verbatim. */
95
+ shown: Set<number>;
96
+ }
97
+
98
+ export function selectRequestLines(requests: string[], maxChars: number): RequestSelection {
99
+ const all = requests
100
+ .map((r, i) => ({ text: oneLine(r), i }))
101
+ .filter((r) => r.text.length > 0 && !TRIVIAL_REQUEST_RE.test(r.text));
102
+ // The opening request and the latest one always stay.
103
+ const meaningful = all;
104
+ const shown = new Set<number>();
105
+ if (meaningful.length === 0) return { lines: [], shown };
106
+ const [first, ...rest] = meaningful;
107
+ const perItem = Math.max(160, Math.floor(maxChars / Math.min(6, meaningful.length)) - 4);
108
+ // The session's opening request: the purpose, kept short.
109
+ const lines = [clip(first.text, 240)];
110
+ shown.add(first.i);
111
+ let budget = maxChars - lines[0].length - 3;
112
+ // Newest first until the budget runs out, then restore chronological order.
113
+ const recent: string[] = [];
114
+ for (let k = rest.length - 1; k >= 0 && recent.length < 5; k--) {
115
+ const item = clip(rest[k].text, perItem);
116
+ if (item.length + 3 > budget) break;
117
+ recent.unshift(item);
118
+ shown.add(rest[k].i);
119
+ budget -= item.length + 3;
120
+ }
121
+ const omitted = rest.length - recent.length;
122
+ if (omitted > 0) lines.push(`(${omitted} more request${omitted === 1 ? "" : "s"} in between — session_recall has them)`);
123
+ return { lines: [...lines, ...recent], shown };
124
+ }
125
+
126
+ export const selectRequests = (requests: string[], maxChars: number): string[] => selectRequestLines(requests, maxChars).lines;
127
+
128
+ // ── Decisions & Constraints ──────────────────────────────
129
+
130
+ /** An instruction opens with an imperative or carries a modal; "X don't work" is a bug report. */
131
+ const IMPERATIVE_START_RE =
132
+ /^(?:(?:please|also|and|then|but|so)[, ]+)?(?:don'?t|do not|never|always|keep|avoid|make sure|ensure|use|prefer|remember|only use|stop|no need|restore|revert|undo|preserve|stick to|stay with|go back to)\b/i;
133
+ const MODAL_RE = /\b(?:should(?:n'?t)?|must|prefer|instead of|instead|rather than|required|important|no need)\b/i;
134
+ /** The user rejecting something the agent did: a correction that must stick. */
135
+ const CORRECTION_RE = /\bI (?:did not|didn't|never) (?:ask|request|want|say)\b|\bnot what I (?:asked|wanted|meant)\b|\bwhy (?:did|do) you (?:change|remove|delete)\b/i;
136
+ const DECISION_RE = { test: (piece: string) => IMPERATIVE_START_RE.test(piece) || MODAL_RE.test(piece) || CORRECTION_RE.test(piece) };
137
+
138
+ const PASTED_AI_RE = /\bI(?:'m| am) an AI\b|\bas an AI\b|\bI can help (?:with|you)\b|\bif you'd like, I\b|\bI can'?t do that\b|\*\*[^*\n]+\*\*/i;
139
+
140
+ /** Text the user quoted or pasted (code fences, > quotes) is material, not instructions. */
141
+ const stripQuoted = (text: string): string =>
142
+ text.replace(/```[\s\S]*?(?:```|$)/g, "\n").replace(/^\s*>.*$/gm, "");
143
+
144
+ export function selectDecisions(
145
+ requests: string[],
146
+ maxChars: number,
147
+ skip: ReadonlySet<number> = new Set(),
148
+ ): string[] {
149
+ const seen = new Set<string>();
150
+ const found: string[] = [];
151
+ for (const [idx, request] of requests.entries()) {
152
+ if (skip.has(idx) || TRIVIAL_REQUEST_RE.test(oneLine(request))) continue;
153
+ // An answer to an agent question is one explicit decision: keep it whole.
154
+ if (request.includes(" → ")) {
155
+ const key = request.toLowerCase().slice(0, 60);
156
+ if (!seen.has(key)) {
157
+ seen.add(key);
158
+ found.push(clip(oneLine(request), 260));
159
+ }
160
+ continue;
161
+ }
162
+ const pieces = stripQuoted(request)
163
+ .split(/\n+/)
164
+ .flatMap((line) => line.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/))
165
+ .map((p) => p.replace(/^\s*(?:[-*+>]|\d+[.)])\s+/, "").trim())
166
+ .filter((p) => p.length >= 24 && p.length <= 400);
167
+ let pushedFromThis = -1;
168
+ for (const piece of pieces) {
169
+ if (!DECISION_RE.test(piece)) continue;
170
+ // "I did not request this" belongs to the instruction right before it.
171
+ if (pushedFromThis >= 0 && CORRECTION_RE.test(piece) && !IMPERATIVE_START_RE.test(piece) && !MODAL_RE.test(piece)) {
172
+ found[pushedFromThis] = clip(`${found[pushedFromThis]} ${oneLine(piece)}`, 260);
173
+ continue;
174
+ }
175
+ if (/\?["')\]]*$/.test(piece)) continue; // a question, not a decision
176
+ if (PASTED_AI_RE.test(piece)) continue; // pasted assistant output
177
+ if (/^(?:it|this|that|these|those)\b/i.test(piece) && piece.length < 60) continue; // needs context it lacks
178
+ if (/^[`$]|[{};]\s*$/.test(piece)) continue; // code, not a decision
179
+ const key = piece.toLowerCase().slice(0, 60);
180
+ if (seen.has(key)) continue;
181
+ seen.add(key);
182
+ found.push(clip(oneLine(piece), 220));
183
+ pushedFromThis = found.length - 1;
184
+ }
185
+ }
186
+ return pickDecisions(found, maxChars);
187
+ }
188
+
189
+ /** A rule for the whole session, not one task: these outlive recency. */
190
+ const STANDING_RE =
191
+ /\b(?:always|never|every|all (?:the )?\w+|by default|default|from now on|going forward|in general|globally|whenever|each time|unless|any time)\b/i;
192
+
193
+ /**
194
+ * Half the budget goes to the most recent decisions (the current work); the
195
+ * rest to standing rules from anywhere in the session, oldest first, so an
196
+ * early "never do X" is not pushed out by a flurry of recent task details.
197
+ * Output stays chronological.
198
+ */
199
+ function pickDecisions(found: string[], maxChars: number): string[] {
200
+ const chosen = new Set<number>();
201
+ let used = 0;
202
+ const take = (i: number) => {
203
+ const cost = found[i].length + 3;
204
+ if (chosen.has(i) || used + cost > maxChars) return false;
205
+ chosen.add(i);
206
+ used += cost;
207
+ return true;
208
+ };
209
+ for (let i = found.length - 1; i >= 0 && used < maxChars / 2; i--) take(i);
210
+ for (let i = 0; i < found.length; i++) {
211
+ if (STANDING_RE.test(found[i]) || CORRECTION_RE.test(found[i])) take(i);
212
+ }
213
+ for (let i = found.length - 1; i >= 0; i--) take(i);
214
+ return [...chosen].sort((a, b) => a - b).map((i) => found[i]);
215
+ }
216
+
217
+ // ── Latest State ─────────────────────────────────────────
218
+
219
+ /** A progress report to the user is long; step narration ("Next I'll…") is short. */
220
+ const REPORT_MIN_CHARS = 400;
221
+
222
+ export function selectState(reports: string[], maxChars: number): string[] {
223
+ const flat = reports.map((r) => oneLine(r.replace(/!\[[^\]]*\]\([^)]*\)/g, ""))).filter(Boolean);
224
+ if (flat.length === 0) return [];
225
+ let reportIdx = -1;
226
+ for (let i = flat.length - 1; i >= 0; i--) {
227
+ if (flat[i].length >= REPORT_MIN_CHARS) {
228
+ reportIdx = i;
229
+ break;
230
+ }
231
+ }
232
+ const latestIdx = flat.length - 1;
233
+ if (reportIdx === latestIdx) return [`Last report: ${clip(flat[reportIdx], maxChars - 20)}`];
234
+ if (reportIdx < 0) return [`Latest step: ${clip(flat[latestIdx], Math.floor(maxChars * 0.3))}`];
235
+ const step = `Latest step: ${clip(flat[latestIdx], Math.floor(maxChars * 0.3))}`;
236
+ const age = latestIdx - reportIdx;
237
+ const label = `Last full report (${age} message${age === 1 ? "" : "s"} earlier — may be outdated)`;
238
+ return [step, `${label}: ${clip(flat[reportIdx], maxChars - step.length - label.length - 10)}`];
239
+ }
240
+
241
+ // ── Files ────────────────────────────────────────────────
242
+
243
+ const EDIT_TOOLS = /^(?:edit|multiedit|apply_patch|quick_edit|target_edit)$/i;
244
+ const WRITE_TOOLS = /^(?:write|create)$/i;
245
+ const READ_TOOLS = /^(?:read)$/i;
246
+ /** Screenshots and scratch files: looked at, not worked on. */
247
+ const NOISE_READ_RE = /(?:^\/tmp\/|\.(?:png|jpe?g|gif|webp|bmp|svg|ico)$)/i;
248
+
249
+ export function selectFiles(blocks: NormalizedBlock[], cwd: string | undefined, maxChars: number): string[] {
250
+ const modified = new Map<string, number>();
251
+ const created = new Set<string>();
252
+ const read = new Set<string>();
253
+ const rel = (p: string) => {
254
+ if (!cwd || !isAbsolute(p)) return p;
255
+ const r = relative(cwd, p);
256
+ return r && !r.startsWith("..") ? r : p;
257
+ };
258
+ for (const b of blocks) {
259
+ if (b.kind !== "tool_call") continue;
260
+ const raw = extractPath(b.args);
261
+ if (!raw) continue;
262
+ const path = rel(raw);
263
+ if (EDIT_TOOLS.test(b.name)) modified.set(path, (modified.get(path) ?? 0) + 1);
264
+ else if (WRITE_TOOLS.test(b.name)) created.add(path);
265
+ else if (READ_TOOLS.test(b.name) && !NOISE_READ_RE.test(path)) read.add(path);
266
+ }
267
+ for (const p of created) if (modified.has(p)) created.delete(p);
268
+ for (const p of [...read]) if (modified.has(p) || created.has(p)) read.delete(p);
269
+
270
+ const list = (label: string, items: string[], share: number) => {
271
+ if (items.length === 0) return "";
272
+ const fitted = fitItems(items, Math.floor(maxChars * share));
273
+ const more = items.length - fitted.length;
274
+ return `${label}: ${fitted.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
275
+ };
276
+ const byCount = [...modified.entries()].sort((a, b) => b[1] - a[1]).map(([p, n]) => (n > 1 ? `${p} ×${n}` : p));
277
+ return [
278
+ list("Modified", byCount, 0.45),
279
+ list("Created", [...created], 0.3),
280
+ list("Read", [...read].reverse(), 0.2),
281
+ ].filter(Boolean);
282
+ }
283
+
284
+ // ── Lessons ─────────────────────────────────────────────
285
+ // Failures turn into knowledge only if someone writes the lesson down. The
286
+ // agent usually does, in its own words: memory_store notes, "# Confirmed: …"
287
+ // comments in the commands it runs, and diagnosis sentences ("root cause…",
288
+ // "…silently ignores…"). Keep those verbatim instead of generating anything.
289
+
290
+ const MEMORY_TOOL_RE = /^(?:memory_store|memory_save|memory_add|save_memory|remember)$/i;
291
+ /** Marks a learned fact, not a status update. */
292
+ const LESSON_MARK_RE =
293
+ /\b(?:root cause|turns out|found it|confirmed|gotcha|caveat|silently|no-?ops?|doesn'?t (?:support|work|accept)|not supported|only works|rejects?|ignores?|the fix (?:is|was)|workaround|breaks? (?:when|if)|must (?:use|be|run)|requires?|instead of|never|always)\b/i;
294
+ /** Diagnosis sentences need a stronger mark than comments (prose is chattier). */
295
+ const DIAGNOSIS_RE =
296
+ /\b(?:root cause|turns out|gotcha|caveat|silently|no-?ops?|doesn'?t (?:support|accept)|not supported|only works|rejects?|ignores?|the (?:real )?(?:issue|problem|bug|cause) (?:is|was)|the fix (?:is|was)|workaround)\b/i;
297
+
298
+ /** Debug narration ("my DBG print never fired") is the hunt, not the finding. */
299
+ const DEBUG_RE = /\b(?:DBG|debug(?:ging)? (?:print|log|output)|print(?:ed)? (?:never|didn'?t)|never (?:fired|printed)|let me|I'?ll (?:check|look|try))\b/i;
300
+
301
+ export interface LessonCandidate {
302
+ text: string;
303
+ source: "memory" | "comment" | "diagnosis";
304
+ /** Block index (session order). */
305
+ at: number;
306
+ }
307
+
308
+ /** Agent-written lessons in session order (later duplicates win). */
309
+ export function lessonCandidates(blocks: NormalizedBlock[]): LessonCandidate[] {
310
+ const out = new Map<string, LessonCandidate>();
311
+ let at = 0;
312
+ const add = (text: string, source: LessonCandidate["source"]) => {
313
+ const t = oneLine(text);
314
+ if (t.length < 30) return;
315
+ const key = itemKey(t);
316
+ out.delete(key);
317
+ out.set(key, { text: clip(t, 320), source, at });
318
+ };
319
+ for (const [i, b] of blocks.entries()) {
320
+ at = i;
321
+ if (b.kind === "tool_call" && MEMORY_TOOL_RE.test(b.name)) {
322
+ const raw = [b.args.content, b.args.text, b.args.memory, b.args.note].find((v) => typeof v === "string") as string | undefined;
323
+ // Past a bare header ("ROOT CAUSES CONFIRMED:"): lines until there is substance.
324
+ if (raw) {
325
+ const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
326
+ let text = "";
327
+ for (const l of lines) {
328
+ text = text ? `${text} ${l}` : l;
329
+ if (text.length >= 160) break;
330
+ }
331
+ add(text, "memory");
332
+ }
333
+ } else if (b.kind === "tool_call" && typeof b.args.command === "string") {
334
+ for (const line of b.args.command.split("\n")) {
335
+ const m = line.match(/^\s*#+\s*(.+)$/);
336
+ // A comment ending in ":" introduces the next command; it is not a finding.
337
+ if (m && m[1].length <= 300 && !/:\s*$/.test(m[1]) && !DEBUG_RE.test(m[1]) && (DIAGNOSIS_RE.test(m[1]) || /\b(?:confirmed|found it)\b/i.test(m[1])) && LESSON_MARK_RE.test(m[1])) add(m[1], "comment");
338
+ }
339
+ } else if (b.kind === "assistant") {
340
+ for (const sentence of b.text.replace(/```[\s\S]*?```/g, " ").split(/(?<=[.!?])\s+|\n+/)) {
341
+ const t = sentence.trim();
342
+ if (t.length >= 50 && t.length <= 320 && !t.endsWith("?") && !DEBUG_RE.test(t) && DIAGNOSIS_RE.test(t)) add(t, "diagnosis");
343
+ }
344
+ }
345
+ }
346
+ return [...out.values()];
347
+ }
348
+
349
+ const LESSON_PRIORITY: Record<LessonCandidate["source"], number> = { memory: 0, comment: 1, diagnosis: 2 };
350
+
351
+ /** Memory notes first, then comments, then diagnoses; newest first within each; chronological output. */
352
+ export function selectLessons(blocks: NormalizedBlock[], maxChars: number): string[] {
353
+ const all = lessonCandidates(blocks).map((c, i) => ({ ...c, i }));
354
+ const ranked = [...all].sort((a, b) => LESSON_PRIORITY[a.source] - LESSON_PRIORITY[b.source] || b.i - a.i);
355
+ const chosen: typeof all = [];
356
+ let used = 0;
357
+ for (const c of ranked) {
358
+ if (used + c.text.length + 3 > maxChars) continue;
359
+ chosen.push(c);
360
+ used += c.text.length + 3;
361
+ }
362
+ return chosen.sort((a, b) => a.i - b.i).map((c) => c.text);
363
+ }
364
+
365
+ // ── Project Knowledge ───────────────────────────────────
366
+ // What the agent learned about the project that no user message states:
367
+ // the notes it wrote (where deploy steps and conventions live), the commands
368
+ // it ran repeatedly, and the hosts it worked against.
369
+
370
+ const NOTE_RE = /(?:^|\/)(?:AGENTS?|CLAUDE|DESIGN|README|SKILL|CONTRIBUTING|RUNBOOK|NOTES)\.md$|(?:^|\/)docs\/.+\.md$|\.agents\/skills\/.+\.md$/i;
371
+ const TOOLING_RE = /^(?:npm|npx|pnpm|yarn|bun|bunx|cargo|mise|make|just|go|pytest|uv|python3? -m|docker|docker-compose|kubectl|helm|rsync|scp|ssh|git push|gh|tea|systemctl|terraform|deno|flutter|xcodebuild)\b/;
372
+
373
+ /** The first meaningful command of a shell line: no `cd x &&`, pipes, redirects or env prefixes. */
374
+ function commandCore(command: string): string {
375
+ const first = command.split("\n").find((l) => l.trim() && !l.trim().startsWith("#")) ?? "";
376
+ const segments = first.split(/\s*(?:&&|;)\s*/).map((x) => x.trim()).filter(Boolean);
377
+ const main = segments.find((x) => !/^(?:cd|export|source|set|echo)\b/.test(x)) ?? "";
378
+ return main
379
+ .replace(/^(?:[A-Z_][A-Z0-9_]*=\S+\s+)+/, "")
380
+ .replace(/\s*(?:\||2>&1|>\s*\S+|>>\s*\S+).*$/, "")
381
+ .trim();
382
+ }
383
+
384
+ export function selectKnowledge(blocks: NormalizedBlock[], cwd: string | undefined, maxChars: number): string[] {
385
+ const rel = (p: string) => {
386
+ if (!cwd || !isAbsolute(p)) return p;
387
+ const r = relative(cwd, p);
388
+ return r && !r.startsWith("..") ? r : p;
389
+ };
390
+ const edited = new Set<string>();
391
+ const read = new Set<string>();
392
+ const commands = new Map<string, { text: string; n: number }>();
393
+ const hosts = new Map<string, number>();
394
+ const addHost = (h: string) => hosts.set(h, (hosts.get(h) ?? 0) + 1);
395
+ for (const b of blocks) {
396
+ if (b.kind !== "tool_call") continue;
397
+ const path = extractPath(b.args);
398
+ if (path && NOTE_RE.test(path)) (EDIT_TOOLS.test(b.name) || WRITE_TOOLS.test(b.name) ? edited : read).add(rel(path));
399
+ const command = typeof b.args.command === "string" ? b.args.command : "";
400
+ if (!command) continue;
401
+ let core = commandCore(command);
402
+ // ssh: the connection is the reusable part, not the remote script.
403
+ if (/^ssh\b/.test(core)) core = core.split(/\s['"]/)[0].trim();
404
+ if (TOOLING_RE.test(core) && core.length <= 160 && !/<<|\\$/.test(core) && (core.match(/['"]/g)?.length ?? 0) % 2 === 0) {
405
+ const key = core.split(/\s+/).slice(0, 3).join(" ");
406
+ const prev = commands.get(key);
407
+ commands.set(key, { text: core, n: (prev?.n ?? 0) + 1 });
408
+ }
409
+ for (const m of command.matchAll(/https?:\/\/([\w.-]+(?::\d+)?)/g)) addHost(m[1]);
410
+ for (const m of command.matchAll(/\b(?:ssh|scp|rsync)\b[^\n]*?\b([\w.-]+@[\w.-]+)/g)) addHost(m[1]);
411
+ }
412
+ for (const p of edited) read.delete(p);
413
+ const lines: string[] = [];
414
+ const cap = (label: string, items: string[], share: number) => {
415
+ if (items.length === 0) return;
416
+ const fitted = fitItems(items, Math.floor(maxChars * share));
417
+ const more = items.length - fitted.length;
418
+ if (fitted.length) lines.push(`${label}: ${fitted.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`);
419
+ };
420
+ cap("Notes written", [...edited], 0.2);
421
+ cap("Notes read", [...read].slice(-8), 0.15);
422
+ const repeated = [...commands.values()].filter((c) => c.n >= 2).sort((a, b) => b.n - a.n).map((c) => `\`${c.text}\` ×${c.n}`);
423
+ cap("Repeated commands", repeated.slice(0, 8), 0.45);
424
+ const topHosts = [...hosts.entries()].filter(([, n]) => n >= 2).sort((a, b) => b[1] - a[1]).map(([h]) => h);
425
+ cap("Hosts", topHosts.slice(0, 6), 0.2);
426
+ return lines;
427
+ }
428
+
429
+ // ── Commits ──────────────────────────────────────────────
430
+
431
+ const COMMIT_LINE_RE = /\[([\w./@-]+)(?: \(root-commit\))? ([0-9a-f]{7,40})\] (.+?)(?= \d+ files? changed|$)/g;
432
+ /** `git commit -m "msg"` / `-qm 'msg'` — for quiet commits that print no summary line. */
433
+ const COMMIT_CMD_RE = /\bgit\s+commit\b[^"'\n]*?\s-[a-zA-Z]*m\s*(["'])((?:(?!\1).){3,}?)\1/;
434
+
435
+ export function selectCommits(blocks: NormalizedBlock[], maxChars: number): string[] {
436
+ const commits = new Map<string, string>();
437
+ let pendingSubject: string | null = null;
438
+ for (const b of blocks) {
439
+ if (b.kind === "tool_call") {
440
+ const command = typeof b.args.command === "string" ? b.args.command : "";
441
+ pendingSubject = COMMIT_CMD_RE.exec(command)?.[2] ?? null;
442
+ continue;
443
+ }
444
+ if (b.kind !== "tool_result") continue;
445
+ let found = false;
446
+ if (!b.isError) {
447
+ for (const m of b.text.matchAll(COMMIT_LINE_RE)) {
448
+ commits.set(m[2].slice(0, 8), clip(m[3].replace(/\s+(?:Date|Author|Co-authored-by):.*$/i, "").trim(), 110));
449
+ found = true;
450
+ }
451
+ }
452
+ if (!found && pendingSubject && !b.isError) commits.set(`~${commits.size}`, clip(pendingSubject.trim(), 110));
453
+ pendingSubject = null;
454
+ }
455
+ const lines = [...commits.entries()].map(([hash, subject]) => (hash.startsWith("~") ? subject : `${hash} ${subject}`));
456
+ return fitItems(lines.slice(-10).reverse(), maxChars).reverse();
457
+ }
458
+
459
+ // ── Open Errors ──────────────────────────────────────────
460
+
461
+ const callTarget = (b: Extract<NormalizedBlock, { kind: "tool_call" }>): string => {
462
+ const path = extractPath(b.args);
463
+ if (path) return path;
464
+ const command = typeof b.args.command === "string" ? b.args.command : "";
465
+ return oneLine(command).slice(0, 60);
58
466
  };
59
467
 
60
- export { mergeBriefTranscriptWithFreshBudget };
468
+ /** Later successes of the same tool after which an error counts as moved past. */
469
+ const RESOLVED_AFTER_SUCCESSES = 3;
470
+
471
+ export function selectOpenErrors(blocks: NormalizedBlock[], maxChars: number): string[] {
472
+ const lastTarget = new Map<string, string>();
473
+ const open = new Map<string, { tool: string; text: string; ref?: number; successesAfter: number }>();
474
+ // Only recent errors can still be open; older ones are history (session_recall).
475
+ for (const b of blocks.slice(-TRANSCRIPT_WINDOW_BLOCKS)) {
476
+ if (b.kind === "tool_call") {
477
+ lastTarget.set(b.name, callTarget(b));
478
+ continue;
479
+ }
480
+ if (b.kind !== "tool_result") continue;
481
+ const key = `${b.name}\u0000${lastTarget.get(b.name) ?? ""}`;
482
+ open.delete(key);
483
+ if (b.isError) {
484
+ open.set(key, { tool: b.name, text: `[${b.name}] ${clip(oneLine(b.text), 200)}`, ref: b.sourceIndex, successesAfter: 0 });
485
+ continue;
486
+ }
487
+ // A retry with the same target clears its error (above); other successes
488
+ // of the same tool mean the work moved on.
489
+ for (const [k, err] of open) {
490
+ if (err.tool === b.name && ++err.successesAfter >= RESOLVED_AFTER_SUCCESSES) open.delete(k);
491
+ }
492
+ }
493
+ const lines = [...open.values()].map((e) => (e.ref != null ? `${e.text} (#${e.ref})` : e.text));
494
+ return fitItems(lines.slice(-5).reverse(), maxChars).reverse();
495
+ }
496
+
497
+ // ── Recent Transcript ────────────────────────────────────
498
+
499
+ /** The transcript covers recent work; older history lives in the sections above. */
500
+ export const TRANSCRIPT_WINDOW_BLOCKS = 160;
501
+
502
+ export function selectTranscript(blocks: NormalizedBlock[], maxChars: number, fileOps?: FileOps): string {
503
+ if (maxChars < 200 || blocks.length === 0) return "";
504
+ const recent = blocks.slice(-TRANSCRIPT_WINDOW_BLOCKS);
505
+ const selected = selectRankedBriefBlocks(recent, { maxBriefChars: maxChars, fileOps });
506
+ const lines = compileBrief(selected).split("\n").map((l) => clip(l, 300));
507
+ // Hard cap: keep the newest lines that fit.
508
+ const out: string[] = [];
509
+ let used = 0;
510
+ for (let i = lines.length - 1; i >= 0; i--) {
511
+ if (used + lines[i].length + 1 > maxChars) break;
512
+ out.unshift(lines[i]);
513
+ used += lines[i].length + 1;
514
+ }
515
+ while (out.length > 0 && !out[0].startsWith("[")) out.shift();
516
+ return out.join("\n").trim();
517
+ }
518
+ // ── secrets ──────────────────────────────────────────────
519
+
520
+ const SECRET_PATTERNS: ReadonlyArray<[RegExp, string]> = [
521
+ [/\b(password|passwd|passphrase|pwd|token|secret|api[_ -]?key)(\s*(?:is|=|:)\s*)(['"`]?)([^\s'"`]{4,})\3/gi, "$1$2[redacted]"],
522
+ // "use password 'x'", "token `x`": a quoted value right after the word.
523
+ [/\b(password|passwd|passphrase|pwd|token|secret|api[_ -]?key)(\s+)(['"`])([^'"`\n]{4,}?)\3/gi, "$1$2[redacted]"],
524
+ [/\bsk-[A-Za-z0-9_-]{16,}/g, "[redacted]"],
525
+ [/\b(Bearer|X-API-KEY:?|Authorization:)\s+[A-Za-z0-9._~+\/=-]{12,}/gi, "$1 [redacted]"],
526
+ [/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "[redacted]"],
527
+ [/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]"],
528
+ [/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, "[redacted]"],
529
+ [/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, "[redacted]"],
530
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[redacted private key]"],
531
+ ];
532
+
533
+ /** Summaries are re-sent every turn: keep credentials out (session_recall still has them). */
534
+ export function redactSecrets(text: string): string {
535
+ return SECRET_PATTERNS.reduce((t, [re, to]) => t.replace(re, to), text);
536
+ }
537
+
538
+ // ── assembly ─────────────────────────────────────────────
539
+
540
+ const SHARES = {
541
+ activeWork: 0.16,
542
+ requests: 0.14,
543
+ state: 0.12,
544
+ lessons: 0.1,
545
+ decisions: 0.16,
546
+ files: 0.07,
547
+ knowledge: 0.07,
548
+ commits: 0.05,
549
+ errors: 0.06,
550
+ } as const;
551
+
552
+ export function buildLosslessSummary(input: LosslessSummaryInput): LosslessSummary {
553
+ // Redact before any clipping can cut a secret's closing quote.
554
+ const source: SummarySource = {
555
+ ...input.source,
556
+ requests: input.source.requests.map(redactSecrets),
557
+ reports: input.source.reports.map(redactSecrets),
558
+ blocks: input.source.blocks.map((b) => ("text" in b && typeof b.text === "string" ? { ...b, text: redactSecrets(b.text) } : b)),
559
+ };
560
+ const on = { ...DEFAULT_SECTIONS, ...input.sections };
561
+ const budget = Math.max(2000, Math.round(input.budgetChars));
562
+ const cap = (share: number) => Math.floor(budget * share);
563
+ const parts: Array<[string, string]> = [];
564
+
565
+ if (on.activeWork && input.activeWork?.length) {
566
+ const per = Math.floor(cap(SHARES.activeWork) / input.activeWork.length);
567
+ const lines = input.activeWork.map((b) => clip(b.text.trim(), Math.max(200, per)));
568
+ parts.push(["Active Work", `[Active Work]\n${lines.join("\n\n")}`]);
569
+ }
570
+ const requestSel = on.requests ? selectRequestLines(source.requests, cap(SHARES.requests)) : { lines: [], shown: new Set<number>() };
571
+ if (on.requests) parts.push(["Your Requests", section("Your Requests", requestSel.lines)]);
572
+ if (on.state) parts.push(["Latest State", section("Latest State", selectState(source.reports, cap(SHARES.state)))]);
573
+ if (on.decisions) parts.push(["Decisions & Constraints", section("Decisions & Constraints", selectDecisions(source.requests, cap(SHARES.decisions), requestSel.shown))]);
574
+ if (on.files) parts.push(["Files", section("Files", selectFiles(source.blocks, input.cwd, cap(SHARES.files)))]);
575
+ if (on.lessons) parts.push(["Lessons", section("Lessons", selectLessons(source.blocks, cap(SHARES.lessons)))]);
576
+ if (on.files) parts.push(["Project Knowledge", section("Project Knowledge", selectKnowledge(source.blocks, input.cwd, cap(SHARES.knowledge)))]);
577
+ if (on.commits) parts.push(["Commits", section("Commits", selectCommits(source.blocks, cap(SHARES.commits)))]);
578
+ if (on.errors) parts.push(["Open Errors", section("Open Errors", selectOpenErrors(source.blocks, cap(SHARES.errors)))]);
579
+
580
+ const head = parts.filter(([, text]) => text);
581
+ const headText = head.map(([, text]) => text).join("\n\n");
582
+ const sections = head.map(([name]) => name);
583
+
584
+ let transcript = "";
585
+ if (on.transcript) {
586
+ const remaining = budget - headText.length - RECALL_NOTE.length - 40;
587
+ transcript = selectTranscript(source.blocks, Math.max(Math.floor(budget * 0.18), remaining), input.fileOps);
588
+ if (transcript) sections.push("Recent Transcript");
589
+ }
590
+
591
+ const body = [headText, transcript ? `[Recent Transcript]\n${transcript}` : ""].filter(Boolean).join("\n\n");
592
+ if (!body) return { text: "", sections: [] };
593
+ return { text: `${redactSecrets(body)}\n\n---\n\n${RECALL_NOTE}`, sections };
594
+ }