@selesai/code 0.13.0 → 0.13.2

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 (463) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/core/agent-session.d.ts +1 -1
  3. package/dist/core/agent-session.js +1 -1
  4. package/dist/core/sdk.d.ts +2 -2
  5. package/dist/core/sdk.js +1 -1
  6. package/dist/core/system-prompt.d.ts +1 -1
  7. package/dist/core/system-prompt.js +1 -1
  8. package/dist/extensions/package.json +1 -2
  9. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +371 -0
  10. package/dist/extensions/pi-hermes-memory/LICENSE +21 -0
  11. package/dist/extensions/pi-hermes-memory/README.md +643 -0
  12. package/dist/extensions/pi-hermes-memory/docs/0.1/TASKS.md +197 -0
  13. package/dist/extensions/pi-hermes-memory/docs/0.2/PLAN.md +290 -0
  14. package/dist/extensions/pi-hermes-memory/docs/0.2/TASKS.md +134 -0
  15. package/dist/extensions/pi-hermes-memory/docs/0.2/TEST-PLAN.md +216 -0
  16. package/dist/extensions/pi-hermes-memory/docs/0.3/PLAN.md +330 -0
  17. package/dist/extensions/pi-hermes-memory/docs/0.3/TASKS.md +125 -0
  18. package/dist/extensions/pi-hermes-memory/docs/0.4/PLAN.md +160 -0
  19. package/dist/extensions/pi-hermes-memory/docs/0.4/TASKS.md +113 -0
  20. package/dist/extensions/pi-hermes-memory/docs/0.6/CHANGELOG.md +57 -0
  21. package/dist/extensions/pi-hermes-memory/docs/0.6/PLAN.md +202 -0
  22. package/dist/extensions/pi-hermes-memory/docs/0.6/TASKS.md +144 -0
  23. package/dist/extensions/pi-hermes-memory/docs/0.7/PLAN.md +349 -0
  24. package/dist/extensions/pi-hermes-memory/docs/0.7/TASKS.md +110 -0
  25. package/dist/extensions/pi-hermes-memory/docs/PUBLISHING.md +149 -0
  26. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +468 -0
  27. package/dist/extensions/pi-hermes-memory/docs/images/memory-architecture.svg +1 -0
  28. package/dist/extensions/pi-hermes-memory/docs/images/pi-logo.svg +22 -0
  29. package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.png +0 -0
  30. package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.svg +19 -0
  31. package/dist/extensions/pi-hermes-memory/docs/images/security-flow.svg +1 -0
  32. package/dist/extensions/pi-hermes-memory/docs/images/session-lifecycle.svg +1 -0
  33. package/dist/extensions/pi-hermes-memory/docs/images/source-architecture.svg +1 -0
  34. package/dist/extensions/pi-hermes-memory/docs/mermaid/memory-architecture.mmd +49 -0
  35. package/dist/extensions/pi-hermes-memory/docs/mermaid/security-flow.mmd +40 -0
  36. package/dist/extensions/pi-hermes-memory/docs/mermaid/session-lifecycle.mmd +56 -0
  37. package/dist/extensions/pi-hermes-memory/docs/mermaid/source-architecture.mmd +54 -0
  38. package/dist/extensions/pi-hermes-memory/package-lock.json +3799 -0
  39. package/dist/extensions/pi-hermes-memory/package.json +66 -0
  40. package/dist/extensions/pi-hermes-memory/scripts/check-min-sdk.mjs +99 -0
  41. package/dist/extensions/pi-hermes-memory/scripts/ensure-dev.mjs +57 -0
  42. package/dist/extensions/pi-hermes-memory/src/auto-consolidation-warning.ts +6 -0
  43. package/dist/extensions/pi-hermes-memory/src/config.ts +178 -0
  44. package/dist/extensions/pi-hermes-memory/src/constants.ts +424 -0
  45. package/dist/extensions/pi-hermes-memory/src/extension-root-migration.ts +639 -0
  46. package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +401 -0
  47. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +297 -0
  48. package/dist/extensions/pi-hermes-memory/src/handlers/child-process-watchdog.mjs +90 -0
  49. package/dist/extensions/pi-hermes-memory/src/handlers/correction-detector.ts +300 -0
  50. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +86 -0
  51. package/dist/extensions/pi-hermes-memory/src/handlers/insights.ts +78 -0
  52. package/dist/extensions/pi-hermes-memory/src/handlers/interview.ts +37 -0
  53. package/dist/extensions/pi-hermes-memory/src/handlers/learn-memory.ts +186 -0
  54. package/dist/extensions/pi-hermes-memory/src/handlers/message-parts.ts +27 -0
  55. package/dist/extensions/pi-hermes-memory/src/handlers/pi-child-process.ts +485 -0
  56. package/dist/extensions/pi-hermes-memory/src/handlers/preview-context.ts +105 -0
  57. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +506 -0
  58. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +145 -0
  59. package/dist/extensions/pi-hermes-memory/src/handlers/session-flush.ts +151 -0
  60. package/dist/extensions/pi-hermes-memory/src/handlers/session-live-index.ts +94 -0
  61. package/dist/extensions/pi-hermes-memory/src/handlers/skills-command.ts +1334 -0
  62. package/dist/extensions/pi-hermes-memory/src/handlers/standing-pin.ts +111 -0
  63. package/dist/extensions/pi-hermes-memory/src/handlers/switch-project.ts +75 -0
  64. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +266 -0
  65. package/dist/extensions/pi-hermes-memory/src/index.ts +378 -0
  66. package/dist/extensions/pi-hermes-memory/src/lifecycle-timing.ts +54 -0
  67. package/dist/extensions/pi-hermes-memory/src/paths.ts +61 -0
  68. package/dist/extensions/pi-hermes-memory/src/project-context.ts +13 -0
  69. package/dist/extensions/pi-hermes-memory/src/project-memory-migration.ts +93 -0
  70. package/dist/extensions/pi-hermes-memory/src/project.ts +157 -0
  71. package/dist/extensions/pi-hermes-memory/src/prompt-context.ts +55 -0
  72. package/dist/extensions/pi-hermes-memory/src/store/atomic-lock-coordinator.ts +375 -0
  73. package/dist/extensions/pi-hermes-memory/src/store/canonical-storage-path.ts +54 -0
  74. package/dist/extensions/pi-hermes-memory/src/store/content-scanner.ts +101 -0
  75. package/dist/extensions/pi-hermes-memory/src/store/db.ts +1180 -0
  76. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +101 -0
  77. package/dist/extensions/pi-hermes-memory/src/store/markdown-mutation-lock.ts +38 -0
  78. package/dist/extensions/pi-hermes-memory/src/store/memory-lookup.ts +24 -0
  79. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +1191 -0
  80. package/dist/extensions/pi-hermes-memory/src/store/recovery-maintenance.ts +66 -0
  81. package/dist/extensions/pi-hermes-memory/src/store/schema.ts +118 -0
  82. package/dist/extensions/pi-hermes-memory/src/store/session-anchor-search.ts +472 -0
  83. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +511 -0
  84. package/dist/extensions/pi-hermes-memory/src/store/session-parser.ts +213 -0
  85. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +234 -0
  86. package/dist/extensions/pi-hermes-memory/src/store/skill-store.ts +950 -0
  87. package/dist/extensions/pi-hermes-memory/src/store/skill-utils.ts +133 -0
  88. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +1002 -0
  89. package/dist/extensions/pi-hermes-memory/src/store/sqlite-native.ts +241 -0
  90. package/dist/extensions/pi-hermes-memory/src/store/standing-instructions.ts +247 -0
  91. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +95 -0
  92. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +469 -0
  93. package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +246 -0
  94. package/dist/extensions/pi-hermes-memory/src/tools/shared-output-view.ts +170 -0
  95. package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +333 -0
  96. package/dist/extensions/pi-hermes-memory/src/tools/tool-result-views.ts +105 -0
  97. package/dist/extensions/pi-hermes-memory/src/types.ts +214 -0
  98. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +434 -0
  99. package/dist/extensions/pi-hermes-memory/tests/extension-root-migration.test.ts +368 -0
  100. package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +984 -0
  101. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +950 -0
  102. package/dist/extensions/pi-hermes-memory/tests/handlers/correction-detector.test.ts +671 -0
  103. package/dist/extensions/pi-hermes-memory/tests/handlers/insights.test.ts +157 -0
  104. package/dist/extensions/pi-hermes-memory/tests/handlers/interview.test.ts +127 -0
  105. package/dist/extensions/pi-hermes-memory/tests/handlers/pi-child-process.test.ts +905 -0
  106. package/dist/extensions/pi-hermes-memory/tests/handlers/preview-context.test.ts +152 -0
  107. package/dist/extensions/pi-hermes-memory/tests/handlers/prompt-context.test.ts +166 -0
  108. package/dist/extensions/pi-hermes-memory/tests/handlers/resources-discover.test.ts +71 -0
  109. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +614 -0
  110. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +192 -0
  111. package/dist/extensions/pi-hermes-memory/tests/handlers/session-flush.test.ts +677 -0
  112. package/dist/extensions/pi-hermes-memory/tests/handlers/session-live-index.test.ts +225 -0
  113. package/dist/extensions/pi-hermes-memory/tests/handlers/skills-command.test.ts +827 -0
  114. package/dist/extensions/pi-hermes-memory/tests/handlers/standing-pin.test.ts +134 -0
  115. package/dist/extensions/pi-hermes-memory/tests/handlers/sync-markdown-memories.test.ts +491 -0
  116. package/dist/extensions/pi-hermes-memory/tests/handlers/system-prompt.test.ts +199 -0
  117. package/dist/extensions/pi-hermes-memory/tests/handlers/thinking-max.test.ts +64 -0
  118. package/dist/extensions/pi-hermes-memory/tests/index.test.ts +30 -0
  119. package/dist/extensions/pi-hermes-memory/tests/integration/flow.test.ts +126 -0
  120. package/dist/extensions/pi-hermes-memory/tests/lifecycle-timing.test.ts +76 -0
  121. package/dist/extensions/pi-hermes-memory/tests/paths.test.ts +23 -0
  122. package/dist/extensions/pi-hermes-memory/tests/project-memory-migration.test.ts +80 -0
  123. package/dist/extensions/pi-hermes-memory/tests/project-rebinding.test.ts +115 -0
  124. package/dist/extensions/pi-hermes-memory/tests/project.test.ts +120 -0
  125. package/dist/extensions/pi-hermes-memory/tests/run-all-timeout.test.ts +107 -0
  126. package/dist/extensions/pi-hermes-memory/tests/run-all.sh +45 -0
  127. package/dist/extensions/pi-hermes-memory/tests/store/atomic-lock-coordinator.test.ts +381 -0
  128. package/dist/extensions/pi-hermes-memory/tests/store/content-scanner.test.ts +388 -0
  129. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +1046 -0
  130. package/dist/extensions/pi-hermes-memory/tests/store/markdown-mutation-lock.test.ts +34 -0
  131. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +2150 -0
  132. package/dist/extensions/pi-hermes-memory/tests/store/recovery-maintenance.test.ts +170 -0
  133. package/dist/extensions/pi-hermes-memory/tests/store/session-anchor-search.test.ts +210 -0
  134. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +643 -0
  135. package/dist/extensions/pi-hermes-memory/tests/store/session-parser.test.ts +282 -0
  136. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +287 -0
  137. package/dist/extensions/pi-hermes-memory/tests/store/skill-store.test.ts +678 -0
  138. package/dist/extensions/pi-hermes-memory/tests/store/skill-utils.test.ts +21 -0
  139. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-lazy-load.test.ts +78 -0
  140. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +543 -0
  141. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +164 -0
  142. package/dist/extensions/pi-hermes-memory/tests/store/standing-instructions.test.ts +171 -0
  143. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +84 -0
  144. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +709 -0
  145. package/dist/extensions/pi-hermes-memory/tests/tools/session-search-tool.test.ts +345 -0
  146. package/dist/extensions/pi-hermes-memory/tests/tools/shared-output-view.test.ts +362 -0
  147. package/dist/extensions/pi-hermes-memory/tests/tools/skill-tool.test.ts +478 -0
  148. package/dist/extensions/pi-hermes-memory/tests/tools/tool-result-renderer-wiring.test.ts +40 -0
  149. package/dist/extensions/pi-hermes-memory/tsconfig.json +19 -0
  150. package/dist/extensions/pi-rewind-hook/README.md +4 -1
  151. package/dist/extensions/pi-rewind-hook/index.ts +151 -21
  152. package/dist/extensions/pi-rewind-hook/package.json +5 -3
  153. package/dist/extensions/pi-subagents/.oxlintrc.json +43 -0
  154. package/dist/extensions/pi-subagents/AGENTS.md +5 -0
  155. package/dist/extensions/pi-subagents/CHANGELOG.md +157 -0
  156. package/dist/extensions/pi-subagents/README.md +12 -6
  157. package/dist/extensions/pi-subagents/VISION.md +81 -0
  158. package/dist/extensions/pi-subagents/agents/claude-code-writer.md +15 -0
  159. package/dist/extensions/pi-subagents/agents/claude-code.md +15 -0
  160. package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +15 -0
  161. package/dist/extensions/pi-subagents/agents/codex-exec.md +15 -0
  162. package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +14 -0
  163. package/dist/extensions/pi-subagents/agents/cursor-agent.md +14 -0
  164. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -3
  165. package/dist/extensions/pi-subagents/docs/agents.md +488 -0
  166. package/dist/extensions/pi-subagents/docs/configuration.md +46 -9
  167. package/dist/extensions/pi-subagents/docs/extension-api.md +42 -3
  168. package/dist/extensions/pi-subagents/docs/models.md +27 -4
  169. package/dist/extensions/pi-subagents/docs/observability.md +10 -9
  170. package/dist/extensions/pi-subagents/docs/tool-reference.md +102 -6
  171. package/dist/extensions/pi-subagents/docs/workflows.md +135 -0
  172. package/dist/extensions/pi-subagents/package-lock.json +389 -2
  173. package/dist/extensions/pi-subagents/package.json +3 -1
  174. package/dist/extensions/pi-subagents/prompts/review-loop.md +2 -2
  175. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +1 -1
  176. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +3 -1
  177. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +7 -3
  178. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  179. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  180. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -22
  181. package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +4 -4
  182. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +9 -2
  183. package/dist/extensions/pi-subagents/src/agents/agents.ts +322 -75
  184. package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +6 -0
  185. package/dist/extensions/pi-subagents/src/agents/runtime-agent-events.ts +70 -0
  186. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +27 -20
  187. package/dist/extensions/pi-subagents/src/api/agents.ts +10 -5
  188. package/dist/extensions/pi-subagents/src/api/background-work.ts +5 -1
  189. package/dist/extensions/pi-subagents/src/api/delegation.ts +0 -7
  190. package/dist/extensions/pi-subagents/src/api/preflight.ts +34 -12
  191. package/dist/extensions/pi-subagents/src/extension/config.ts +70 -0
  192. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  193. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -3
  194. package/dist/extensions/pi-subagents/src/extension/index.ts +83 -30
  195. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +44 -15
  196. package/dist/extensions/pi-subagents/src/extension/rpc.ts +55 -19
  197. package/dist/extensions/pi-subagents/src/extension/schemas.ts +41 -15
  198. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -31
  199. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -1
  200. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -10
  201. package/dist/extensions/pi-subagents/src/inspectors/herdr/session-roots-codec.ts +42 -0
  202. package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +51 -7
  203. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +2 -0
  204. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +5 -6
  205. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +77 -10
  206. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +176 -53
  207. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +19 -12
  208. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +29 -27
  209. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +20 -3
  210. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +23 -261
  211. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +73 -7
  212. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +8 -3
  213. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +74 -8
  214. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +37 -21
  215. package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +8 -8
  216. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +184 -9
  217. package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +45 -0
  218. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +28 -14
  219. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +38 -15
  220. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +1 -1
  221. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +1 -1
  222. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +50 -13
  223. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +93 -8
  224. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +10 -4
  225. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -14
  226. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +549 -377
  227. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +58 -10
  228. package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +1 -1
  229. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +26 -1
  230. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +23 -9
  231. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +9 -2
  232. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +2 -2
  233. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +204 -129
  234. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +9 -0
  235. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +23 -1
  236. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +773 -311
  237. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +140 -115
  238. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +119 -0
  239. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +463 -0
  240. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +19 -4
  241. package/dist/extensions/pi-subagents/src/runs/shared/child-launch-plan.ts +151 -0
  242. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +21 -7
  243. package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +129 -0
  244. package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +129 -0
  245. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +89 -0
  246. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +5 -4
  247. package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +114 -0
  248. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +3 -3
  249. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +167 -0
  250. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +122 -0
  251. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +348 -55
  252. package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +5 -5
  253. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +230 -0
  254. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +105 -0
  255. package/dist/extensions/pi-subagents/src/runs/shared/launch-cwd.ts +16 -0
  256. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +2 -1
  257. package/dist/extensions/pi-subagents/src/runs/shared/mcp-config-sources.ts +422 -0
  258. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  259. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  260. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +69 -7
  261. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +47 -6
  262. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +7 -2
  263. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +9 -3
  264. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +9 -5
  265. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +419 -7
  266. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +15 -1
  267. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +81 -8
  268. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +13 -0
  269. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +21 -1
  270. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +44 -4
  271. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  272. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +1 -1
  273. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  274. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +18 -0
  275. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +46 -0
  276. package/dist/extensions/pi-subagents/src/shared/extension-context.ts +24 -0
  277. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +21 -0
  278. package/dist/extensions/pi-subagents/src/shared/formatters.ts +18 -3
  279. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +5 -1
  280. package/dist/extensions/pi-subagents/src/shared/pruned-fork.ts +450 -0
  281. package/dist/extensions/pi-subagents/src/shared/session-file-trust.ts +19 -0
  282. package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +14 -3
  283. package/dist/extensions/pi-subagents/src/shared/settings.ts +19 -105
  284. package/dist/extensions/pi-subagents/src/shared/shortcuts.ts +17 -0
  285. package/dist/extensions/pi-subagents/src/shared/types.ts +359 -40
  286. package/dist/extensions/pi-subagents/src/shared/utils.ts +41 -84
  287. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +116 -0
  288. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -9
  289. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +0 -4
  290. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -2
  291. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +378 -88
  292. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +22 -11
  293. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +3 -0
  294. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +147 -79
  295. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +11 -5
  296. package/dist/extensions/pi-subagents/src/tui/fleet.ts +39 -18
  297. package/dist/extensions/pi-subagents/src/tui/render.ts +369 -46
  298. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +1 -1
  299. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +6 -3
  300. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +230 -0
  301. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +740 -58
  302. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +121 -0
  303. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +270 -0
  304. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +195 -6
  305. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +246 -0
  306. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  307. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1114 -260
  308. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +34 -0
  309. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +150 -3
  310. package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +52 -0
  311. package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +55 -0
  312. package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +53 -0
  313. package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +57 -0
  314. package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +59 -0
  315. package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +62 -0
  316. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +57 -1
  317. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  318. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +20 -0
  319. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +4 -3
  320. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +2 -2
  321. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +54 -4
  322. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +482 -4
  323. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +157 -9
  324. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +5 -5
  325. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1508 -291
  326. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +423 -1
  327. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +8 -0
  328. package/dist/extensions/pi-subagents/test/support/cursor-smoke-inputs.ts +51 -0
  329. package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
  330. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +5 -5
  331. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +6 -6
  332. package/dist/extensions/pi-subagents/test/unit/abort-recovery.test.ts +152 -0
  333. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +48 -0
  334. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +184 -6
  335. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +66 -6
  336. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +77 -0
  337. package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +18 -0
  338. package/dist/extensions/pi-subagents/test/unit/anti-slop-oxlint.test.ts +86 -0
  339. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +1 -1
  340. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +101 -1
  341. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +121 -0
  342. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +28 -0
  343. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +14 -7
  344. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +53 -0
  345. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +190 -0
  346. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +42 -0
  347. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +8 -4
  348. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +23 -0
  349. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +78 -1
  350. package/dist/extensions/pi-subagents/test/unit/child-launch-plan.test.ts +94 -0
  351. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +21 -0
  352. package/dist/extensions/pi-subagents/test/unit/child-session-name.test.ts +50 -0
  353. package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +245 -0
  354. package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +192 -0
  355. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +27 -0
  356. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +150 -0
  357. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +21 -0
  358. package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +259 -0
  359. package/dist/extensions/pi-subagents/test/unit/cursor-smoke-inputs.test.ts +77 -0
  360. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +1 -4
  361. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +1 -1
  362. package/dist/extensions/pi-subagents/test/unit/extension-context.test.ts +27 -0
  363. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +223 -0
  364. package/dist/extensions/pi-subagents/test/unit/fast-mode-extension.test.ts +27 -0
  365. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +208 -2
  366. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +29 -1
  367. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +110 -1
  368. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  369. package/dist/extensions/pi-subagents/test/unit/formatters.test.ts +14 -0
  370. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +18 -9
  371. package/dist/extensions/pi-subagents/test/unit/herdr-session-roots-codec.test.ts +63 -0
  372. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +2 -2
  373. package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +136 -6
  374. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +156 -0
  375. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +98 -0
  376. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +178 -26
  377. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +2 -1
  378. package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +21 -0
  379. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +138 -0
  380. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  381. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +91 -0
  382. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +72 -7
  383. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +21 -0
  384. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +1 -0
  385. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +20 -2
  386. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +129 -1
  387. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +2 -2
  388. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +4 -0
  389. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +358 -0
  390. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +574 -21
  391. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +109 -1
  392. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +71 -1
  393. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +7 -7
  394. package/dist/extensions/pi-subagents/test/unit/pruned-fork.test.ts +214 -0
  395. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +29 -8
  396. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +53 -0
  397. package/dist/extensions/pi-subagents/test/unit/result-delivery-ownership.test.ts +34 -0
  398. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +101 -3
  399. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +1 -0
  400. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +10 -8
  401. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +71 -1
  402. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +41 -7
  403. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +136 -2
  404. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +137 -0
  405. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +33 -12
  406. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +504 -1
  407. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +8 -0
  408. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  409. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +12 -1
  410. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +3 -5
  411. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +5 -6
  412. package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +0 -8
  413. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +5 -0
  414. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +157 -2
  415. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +1 -1
  416. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +63 -4
  417. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +27 -23
  418. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +44 -0
  419. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -0
  420. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +7 -5
  421. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +106 -0
  422. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +484 -9
  423. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +38 -4
  424. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +155 -0
  425. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +318 -0
  426. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +365 -0
  427. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +349 -0
  428. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +21 -0
  429. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +2 -7
  430. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/index.ts +13 -0
  431. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
  432. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/index.ts +41 -0
  433. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +77 -0
  434. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
  435. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +247 -0
  436. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-module-mocking.ts +91 -0
  437. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-object-parameters.ts +126 -0
  438. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +28 -0
  439. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-get.ts +28 -0
  440. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +67 -0
  441. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
  442. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +83 -0
  443. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +115 -0
  444. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +72 -0
  445. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
  446. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +366 -0
  447. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
  448. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/dictionary-types.ts +502 -0
  449. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +60 -0
  450. package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/reflect-method.ts +35 -0
  451. package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +1 -1
  452. package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +2 -2
  453. package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +1 -1
  454. package/dist/modes/interactive/interactive-mode.js +7 -7
  455. package/dist/skills/workflow/SKILL.md +65 -50
  456. package/package.json +112 -110
  457. package/dist/extensions/enable-readonly-tools.test.ts +0 -63
  458. package/dist/extensions/enable-readonly-tools.ts +0 -24
  459. package/dist/extensions/model-prompt-injector/config.json +0 -16
  460. package/dist/extensions/model-prompt-injector/index.test.ts +0 -102
  461. package/dist/extensions/model-prompt-injector/index.ts +0 -148
  462. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +0 -98
  463. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +0 -250
@@ -0,0 +1,984 @@
1
+ /**
2
+ * Unit tests for auto-consolidation — triggerConsolidation and /memory-consolidate command.
3
+ */
4
+
5
+ import { describe, it, beforeEach, before, after } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { readFileSync } from "node:fs";
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import * as os from "node:os";
11
+ import { registerConsolidateCommand, triggerConsolidation } from "../../src/handlers/auto-consolidate.js";
12
+ import { resolveWatchedChildPiInvocation } from "../../src/handlers/pi-child-process.js";
13
+ import { MemoryStore } from "../../src/store/memory-store.js";
14
+ import { AtomicLockCoordinator } from "../../src/store/atomic-lock-coordinator.js";
15
+ import { DEFAULT_CONSOLIDATION_TIMEOUT_MS, ENTRY_DELIMITER } from "../../src/constants.js";
16
+
17
+ // ─── Mock infrastructure ───
18
+
19
+ let execCalls: any[];
20
+ let directCalls: unknown[][];
21
+
22
+ const directTransportLlmConfig = { reviewTransport: "direct" as const };
23
+
24
+ function createDirectCtx(): { model: unknown; modelRegistry: unknown; _tag: string } {
25
+ return { model: {}, modelRegistry: {}, _tag: "consolidation-direct-ctx" };
26
+ }
27
+
28
+ function makeDirectDeps(
29
+ result: { ok: boolean; appliedCount: number } | "throw",
30
+ ): { runDirectMemoryCompletion: (...args: unknown[]) => Promise<{ ok: boolean; appliedCount: number }> } {
31
+ return {
32
+ runDirectMemoryCompletion: async (...args: unknown[]) => {
33
+ directCalls.push(args);
34
+ if (result === "throw") throw new Error("injected direct consolidation failure");
35
+ return result;
36
+ },
37
+ };
38
+ }
39
+ let LOCK_DIR = "";
40
+ const OLD_LOCK_DIR = process.env.PI_HERMES_CONSOLIDATION_LOCK_DIR;
41
+
42
+ function captureExecArgs(args: any[]): any[] {
43
+ const [command, childArgs, options] = args;
44
+ const capturedArgs = [...childArgs];
45
+ const promptReference = capturedArgs.at(-1);
46
+ if (typeof promptReference === "string" && promptReference.startsWith("@")) {
47
+ capturedArgs[capturedArgs.length - 1] = readFileSync(promptReference.slice(1), "utf-8");
48
+ }
49
+ return [command, capturedArgs, options];
50
+ }
51
+ before(async () => {
52
+ LOCK_DIR = await fs.mkdtemp(path.join(os.tmpdir(), "pi-consolidation-lock-"));
53
+ process.env.PI_HERMES_CONSOLIDATION_LOCK_DIR = LOCK_DIR;
54
+ });
55
+
56
+ after(async () => {
57
+ if (OLD_LOCK_DIR === undefined) {
58
+ delete process.env.PI_HERMES_CONSOLIDATION_LOCK_DIR;
59
+ } else {
60
+ process.env.PI_HERMES_CONSOLIDATION_LOCK_DIR = OLD_LOCK_DIR;
61
+ }
62
+ try { await fs.rm(LOCK_DIR, { recursive: true, force: true }); } catch { /* ignore */ }
63
+ });
64
+
65
+ function logicalChildArgs(call: any[]): string[] {
66
+ const [cmd, args] = call;
67
+ const underlying = { command: args[3], args: args.slice(4) };
68
+ const expected = resolveWatchedChildPiInvocation(underlying, Number(args[1]), args[2]);
69
+ assert.deepStrictEqual({ command: cmd, args }, expected);
70
+ return underlying.command === "selesai" ? underlying.args : underlying.args.slice(1);
71
+ }
72
+
73
+ function childPrompt(call: any[]): string {
74
+ const args = logicalChildArgs(call);
75
+ return args[args.length - 1];
76
+ }
77
+
78
+ function createMockPi(execReturn?: { code: number; stdout: string; stderr: string }) {
79
+ const ret = execReturn ?? { code: 0, stdout: "Consolidated", stderr: "" };
80
+ return {
81
+ on: () => {},
82
+ exec: async (...args: any[]) => {
83
+ execCalls.push(captureExecArgs(args));
84
+ return ret;
85
+ },
86
+ registerTool: () => {},
87
+ registerCommand: () => {},
88
+ } as any;
89
+ }
90
+
91
+ const mockStore = {
92
+ getMemoryEntries: () => ["old entry 1", "old entry 2"],
93
+ getUserEntries: () => ["user fact 1"],
94
+ getAllFailureEntries: () => ["failure lesson 1", "failure lesson 2"],
95
+ getStorageIdentity: async (target: string) => path.join("mock-store", target),
96
+ loadFromDisk: async () => {},
97
+ } as any;
98
+
99
+ async function settle(ms = 10) {
100
+ await new Promise((r) => setTimeout(r, ms));
101
+ }
102
+
103
+ /** Scope the contended-lock poll window so contention tests stay fast. */
104
+ async function withLockWait(waitMs: string, run: () => Promise<void>): Promise<void> {
105
+ const previous = process.env.PI_HERMES_CONSOLIDATION_LOCK_WAIT_MS;
106
+ process.env.PI_HERMES_CONSOLIDATION_LOCK_WAIT_MS = waitMs;
107
+ try {
108
+ await run();
109
+ } finally {
110
+ if (previous === undefined) {
111
+ delete process.env.PI_HERMES_CONSOLIDATION_LOCK_WAIT_MS;
112
+ } else {
113
+ process.env.PI_HERMES_CONSOLIDATION_LOCK_WAIT_MS = previous;
114
+ }
115
+ }
116
+ }
117
+
118
+ type ManualCommandHandler = (args: unknown, ctx: unknown) => Promise<void>;
119
+
120
+ async function runManualConsolidate(timeoutMs?: number): Promise<void> {
121
+ let handler: ManualCommandHandler | undefined;
122
+ const pi = {
123
+ on: () => {},
124
+ exec: async (...args: unknown[]) => {
125
+ execCalls.push(captureExecArgs(args as Parameters<typeof captureExecArgs>[0]));
126
+ return { code: 0, stdout: "Done", stderr: "" };
127
+ },
128
+ registerTool: () => {},
129
+ registerCommand: (_name: string, command: { handler: ManualCommandHandler }) => {
130
+ handler = command.handler;
131
+ },
132
+ } as unknown as Parameters<typeof registerConsolidateCommand>[0];
133
+
134
+ registerConsolidateCommand(pi, mockStore, timeoutMs);
135
+ assert.ok(handler, "command handler should be registered");
136
+ await handler({}, { signal: undefined, ui: { notify: () => {} } });
137
+ }
138
+
139
+ // ─── Tests ───
140
+
141
+ describe("triggerConsolidation", () => {
142
+ beforeEach(() => {
143
+ execCalls = [];
144
+ });
145
+
146
+ it("builds prompt with current entries and calls pi.exec", async () => {
147
+ const pi = createMockPi();
148
+ await triggerConsolidation(pi, mockStore, "memory");
149
+
150
+ assert.strictEqual(execCalls.length, 1, "should call pi.exec once");
151
+ const args = logicalChildArgs(execCalls[0]);
152
+ assert.ok(args[0] === "-p", "should use -p flag");
153
+ assert.ok(args.includes("--no-session"), "should include --no-session");
154
+
155
+ const prompt = args[args.length - 1];
156
+ assert.ok(prompt.includes("old entry 1"), "prompt should include current memory entries");
157
+ assert.ok(prompt.includes("memory"), "prompt should reference target");
158
+ });
159
+
160
+ it("returns { consolidated: true } on success (exit code 0)", async () => {
161
+ const pi = createMockPi({ code: 0, stdout: "Done", stderr: "" });
162
+ const result = await triggerConsolidation(pi, mockStore, "memory");
163
+
164
+ assert.strictEqual(result.consolidated, true);
165
+ assert.strictEqual(result.error, undefined);
166
+ });
167
+
168
+ it("clears a failed release before the next consolidation", async () => {
169
+ const prototype = AtomicLockCoordinator.prototype as any;
170
+ const originalDeleteOwnedLock = prototype.deleteOwnedLock;
171
+ let deleteAttempts = 0;
172
+ prototype.deleteOwnedLock = function (key: string, token: string): void {
173
+ deleteAttempts++;
174
+ if (deleteAttempts <= 3) throw new Error("injected consolidation release failure");
175
+ return originalDeleteOwnedLock.call(this, key, token);
176
+ };
177
+
178
+ try {
179
+ const pi = createMockPi();
180
+ const first = await triggerConsolidation(pi, mockStore, "memory");
181
+ const second = await triggerConsolidation(pi, mockStore, "memory");
182
+
183
+ assert.strictEqual(first.consolidated, true);
184
+ assert.strictEqual(second.consolidated, true);
185
+ assert.strictEqual(execCalls.length, 2);
186
+ assert.ok(deleteAttempts >= 4);
187
+ } finally {
188
+ prototype.deleteOwnedLock = originalDeleteOwnedLock;
189
+ }
190
+ });
191
+
192
+ it("defers a duplicate subprocess while the same target is consolidating", async () => {
193
+ const releaseExecs: Array<() => void> = [];
194
+ let markExecStarted!: () => void;
195
+ const execStarted = new Promise<void>((resolve) => { markExecStarted = resolve; });
196
+ const pi = {
197
+ on: () => {},
198
+ exec: async (...args: any[]) => {
199
+ execCalls.push(captureExecArgs(args));
200
+ markExecStarted();
201
+ await new Promise<void>((resolve) => { releaseExecs.push(resolve); });
202
+ return { code: 0, stdout: "Done", stderr: "" };
203
+ },
204
+ registerTool: () => {},
205
+ registerCommand: () => {},
206
+ } as any;
207
+
208
+ await withLockWait("0", async () => {
209
+ const first = triggerConsolidation(pi, mockStore, "memory");
210
+ await execStarted;
211
+ const second = triggerConsolidation(pi, mockStore, "memory");
212
+ const raced = await Promise.race([
213
+ second.then((result) => ({ result })),
214
+ settle(100).then(() => ({ timeout: true as const })),
215
+ ]);
216
+
217
+ releaseExecs.forEach((release) => release());
218
+ await Promise.allSettled([first, second]);
219
+
220
+ assert.ok("result" in raced, "duplicate consolidation should return without spawning another child");
221
+ assert.strictEqual(raced.result.consolidated, false);
222
+ assert.strictEqual(raced.result.deferred, true, "contention is deferral, not failure");
223
+ assert.match(raced.result.error!, /already in progress/i);
224
+ assert.strictEqual(execCalls.length, 1, "only one child Pi process should be spawned");
225
+ });
226
+ });
227
+
228
+ it("waits out transient contention instead of failing the caller", async () => {
229
+ const releaseExecs: Array<() => void> = [];
230
+ let markFirstExecStarted!: () => void;
231
+ const firstExecStarted = new Promise<void>((resolve) => { markFirstExecStarted = resolve; });
232
+ const pi = {
233
+ on: () => {},
234
+ exec: async (...args: any[]) => {
235
+ execCalls.push(captureExecArgs(args));
236
+ if (execCalls.length === 1) {
237
+ markFirstExecStarted();
238
+ await new Promise<void>((resolve) => { releaseExecs.push(resolve); });
239
+ }
240
+ return { code: 0, stdout: "Done", stderr: "" };
241
+ },
242
+ registerTool: () => {},
243
+ registerCommand: () => {},
244
+ } as any;
245
+
246
+ await withLockWait("2000", async () => {
247
+ const first = triggerConsolidation(pi, mockStore, "memory");
248
+ await firstExecStarted;
249
+ const second = triggerConsolidation(pi, mockStore, "memory");
250
+ await settle(20);
251
+ releaseExecs.forEach((release) => release());
252
+
253
+ const [firstResult, secondResult] = await Promise.all([first, second]);
254
+ assert.strictEqual(firstResult.consolidated, true);
255
+ assert.strictEqual(secondResult.consolidated, true, "the queued caller should consolidate, not hard-fail");
256
+ assert.strictEqual(secondResult.deferred, undefined);
257
+ assert.strictEqual(execCalls.length, 2);
258
+ });
259
+ });
260
+
261
+ it("skips its own child when the session it queued behind already freed space", async () => {
262
+ let entries = ["old entry 1", "old entry 2"];
263
+ const shrinkingStore = {
264
+ getMemoryEntries: () => entries,
265
+ getUserEntries: () => [],
266
+ getAllFailureEntries: () => [],
267
+ getStorageIdentity: async (target: string) => path.join("shrinking-store", target),
268
+ loadFromDisk: async () => { entries = ["merged"]; },
269
+ } as any;
270
+
271
+ const releaseExecs: Array<() => void> = [];
272
+ let markFirstExecStarted!: () => void;
273
+ const firstExecStarted = new Promise<void>((resolve) => { markFirstExecStarted = resolve; });
274
+ const pi = {
275
+ on: () => {},
276
+ exec: async (...args: any[]) => {
277
+ execCalls.push(captureExecArgs(args));
278
+ markFirstExecStarted();
279
+ await new Promise<void>((resolve) => { releaseExecs.push(resolve); });
280
+ return { code: 0, stdout: "Done", stderr: "" };
281
+ },
282
+ registerTool: () => {},
283
+ registerCommand: () => {},
284
+ } as any;
285
+
286
+ await withLockWait("2000", async () => {
287
+ const first = triggerConsolidation(pi, shrinkingStore, "memory");
288
+ await firstExecStarted;
289
+ const second = triggerConsolidation(pi, shrinkingStore, "memory");
290
+ await settle(20);
291
+ releaseExecs.forEach((release) => release());
292
+
293
+ const [, secondResult] = await Promise.all([first, second]);
294
+ assert.strictEqual(secondResult.consolidated, true);
295
+ assert.strictEqual(execCalls.length, 1, "a second LLM pass is pure cost once space is already free");
296
+ });
297
+ });
298
+
299
+ it("allows the same project target to consolidate concurrently in distinct stores", async () => {
300
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pi-consolidation-stores-"));
301
+ const stores = ["project-a", "project-b"].map((name) => new MemoryStore({
302
+ memoryDir: path.join(root, name),
303
+ memoryCharLimit: 5_000,
304
+ userCharLimit: 5_000,
305
+ } as any));
306
+ await Promise.all(stores.map((store) => store.loadFromDisk()));
307
+
308
+ let started = 0;
309
+ let markFirstStarted!: () => void;
310
+ let markBothStarted!: () => void;
311
+ const firstStarted = new Promise<void>((resolve) => { markFirstStarted = resolve; });
312
+ const bothStarted = new Promise<void>((resolve) => { markBothStarted = resolve; });
313
+ const releases: Array<() => void> = [];
314
+ const pi = {
315
+ exec: async () => {
316
+ started++;
317
+ if (started === 1) markFirstStarted();
318
+ if (started === 2) markBothStarted();
319
+ await new Promise<void>((resolve) => { releases.push(resolve); });
320
+ return { code: 0, stdout: "Done", stderr: "" };
321
+ },
322
+ } as any;
323
+
324
+ try {
325
+ const first = triggerConsolidation(pi, stores[0], "memory", undefined, 60_000, "project");
326
+ await firstStarted;
327
+ const second = triggerConsolidation(pi, stores[1], "memory", undefined, 60_000, "project");
328
+ const raced = await Promise.race([
329
+ bothStarted.then(() => "both-started" as const),
330
+ settle(100).then(() => "timeout" as const),
331
+ ]);
332
+
333
+ releases.forEach((release) => release());
334
+ await Promise.allSettled([first, second]);
335
+
336
+ assert.strictEqual(raced, "both-started");
337
+ assert.strictEqual(started, 2);
338
+ } finally {
339
+ releases.forEach((release) => release());
340
+ await fs.rm(root, { recursive: true, force: true });
341
+ }
342
+ });
343
+
344
+ it("returns { consolidated: false } on failure (non-zero exit code)", async () => {
345
+ const pi = createMockPi({ code: 1, stdout: "", stderr: "some error" });
346
+ const result = await triggerConsolidation(pi, mockStore, "memory");
347
+
348
+ assert.strictEqual(result.consolidated, false);
349
+ assert.ok(result.error, "should have error message");
350
+ assert.ok(result.error!.includes("exit"), "error should mention exit code");
351
+ });
352
+
353
+ it("surfaces timeout-style child termination clearly", async () => {
354
+ const pi = createMockPi({ code: 143, stdout: "", stderr: "", killed: true } as any);
355
+ const result = await triggerConsolidation(pi, mockStore, "memory", undefined, 60000);
356
+
357
+ assert.strictEqual(result.consolidated, false);
358
+ assert.match(result.error!, /terminated/i);
359
+ assert.match(result.error!, /60000ms/);
360
+ });
361
+
362
+ it("returns { consolidated: false } when pi.exec throws", async () => {
363
+ const crashPi = {
364
+ on: () => {},
365
+ exec: async () => { throw new Error("network failure"); },
366
+ registerTool: () => {},
367
+ registerCommand: () => {},
368
+ } as any;
369
+
370
+ const result = await triggerConsolidation(crashPi, mockStore, "memory");
371
+
372
+ assert.strictEqual(result.consolidated, false);
373
+ assert.ok(result.error!.includes("Consolidation failed"), "should mention failure");
374
+ assert.ok(result.error!.includes("network failure"), "should include original error");
375
+ });
376
+
377
+ it("defers instead of failing when pi.exec throws a stale extension ctx error", async () => {
378
+ const stalePi = {
379
+ on: () => {},
380
+ exec: async () => { throw new Error("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession()"); },
381
+ registerTool: () => {},
382
+ registerCommand: () => {},
383
+ } as any;
384
+
385
+ const result = await triggerConsolidation(stalePi, mockStore, "memory");
386
+
387
+ assert.strictEqual(result.consolidated, false);
388
+ assert.strictEqual(result.deferred, true, "stale ctx should defer, not fail");
389
+ assert.ok(!result.error!.includes("Consolidation failed"), "should not report a failure");
390
+ assert.ok(result.error!.includes("session replaced or reloaded"), "should explain the skip");
391
+ });
392
+
393
+ it("includes user profile entries when target is 'user'", async () => {
394
+ const pi = createMockPi();
395
+ await triggerConsolidation(pi, mockStore, "user");
396
+
397
+ const prompt = childPrompt(execCalls[0]);
398
+ assert.ok(prompt.includes("user fact 1"), "prompt should include user entries");
399
+ assert.ok(prompt.includes("User Profile"), "prompt should reference user profile");
400
+ });
401
+
402
+ it("includes failure entries when target is 'failure'", async () => {
403
+ const pi = createMockPi();
404
+ await triggerConsolidation(pi, mockStore, "failure");
405
+
406
+ const prompt = childPrompt(execCalls[0]);
407
+ assert.ok(prompt.includes("failure lesson 1"), "prompt should include failure entries");
408
+ assert.ok(prompt.includes("Failure Memory"), "prompt should reference failure memory");
409
+ assert.ok(prompt.includes("Target: 'failure'"), "prompt should tell the child agent to use target='failure'");
410
+ });
411
+
412
+ it("can consolidate project memory using the project tool target", async () => {
413
+ const pi = createMockPi();
414
+ await triggerConsolidation(pi, mockStore, "memory", undefined, 60000, "project");
415
+
416
+ const prompt = childPrompt(execCalls[0]);
417
+ assert.ok(prompt.includes("old entry 1"), "prompt should include project memory entries");
418
+ assert.ok(prompt.includes("Project Memory"), "prompt should label project memory");
419
+ assert.ok(prompt.includes("Target: 'project'"), "prompt should tell the child agent to use target='project'");
420
+ });
421
+
422
+ it("retries once without overrides when the override subprocess fails for model resolution reasons", async () => {
423
+ const pi = {
424
+ on: () => {},
425
+ exec: async (...args: any[]) => {
426
+ execCalls.push(captureExecArgs(args));
427
+ if (execCalls.length === 1) {
428
+ return { code: 1, stdout: "", stderr: "model not found" };
429
+ }
430
+ return { code: 0, stdout: "Consolidated", stderr: "" };
431
+ },
432
+ registerTool: () => {},
433
+ registerCommand: () => {},
434
+ } as any;
435
+
436
+ const result = await triggerConsolidation(
437
+ pi,
438
+ mockStore,
439
+ "memory",
440
+ undefined,
441
+ 60000,
442
+ "memory",
443
+ { llmModelOverride: "openrouter/deepseek/deepseek-v4-flash" },
444
+ );
445
+
446
+ assert.strictEqual(result.consolidated, true);
447
+ assert.strictEqual(execCalls.length, 2, "should retry once without overrides");
448
+ assert.deepStrictEqual(logicalChildArgs(execCalls[0]).slice(0, 6), [
449
+ "-p",
450
+ "--no-session",
451
+ "--model",
452
+ "openrouter/deepseek/deepseek-v4-flash",
453
+ "--thinking",
454
+ "off",
455
+ ]);
456
+ const retryArgs = logicalChildArgs(execCalls[1]);
457
+ assert.deepStrictEqual(retryArgs.slice(0, 2), ["-p", "--no-session"]);
458
+ assert.ok(!retryArgs.includes("--model"), "fallback retry should drop model override");
459
+ assert.ok(!retryArgs.includes("--thinking"), "fallback retry should drop thinking override");
460
+ assert.strictEqual(typeof retryArgs[retryArgs.length - 1], "string", "fallback retry should keep prompt as final arg");
461
+ });
462
+
463
+ it("does not retry generic consolidation failures that are unrelated to override resolution", async () => {
464
+ const pi = {
465
+ on: () => {},
466
+ exec: async (...args: any[]) => {
467
+ execCalls.push(captureExecArgs(args));
468
+ return { code: 1, stdout: "", stderr: "memory tool returned no changes" };
469
+ },
470
+ registerTool: () => {},
471
+ registerCommand: () => {},
472
+ } as any;
473
+
474
+ const result = await triggerConsolidation(
475
+ pi,
476
+ mockStore,
477
+ "memory",
478
+ undefined,
479
+ 60000,
480
+ "memory",
481
+ { llmModelOverride: "openrouter/deepseek/deepseek-v4-flash" },
482
+ );
483
+
484
+ assert.strictEqual(result.consolidated, false);
485
+ assert.strictEqual(execCalls.length, 1, "should not retry generic consolidation failures");
486
+ });
487
+
488
+ it("handles empty entries gracefully", async () => {
489
+ const emptyStore = {
490
+ getMemoryEntries: () => [],
491
+ getUserEntries: () => [],
492
+ getStorageIdentity: async (target: string) => path.join("empty-store", target),
493
+ loadFromDisk: async () => {},
494
+ } as any;
495
+
496
+ const pi = createMockPi();
497
+ await triggerConsolidation(pi, emptyStore, "memory");
498
+
499
+ const prompt = childPrompt(execCalls[0]);
500
+ assert.ok(prompt.includes("(empty)"), "prompt should show (empty) for empty entries");
501
+ });
502
+
503
+ describe("direct transport", () => {
504
+ beforeEach(() => {
505
+ directCalls = [];
506
+ });
507
+
508
+ it("returns consolidated true via direct transport without calling subprocess when appliedCount is positive", async () => {
509
+ const pi = createMockPi();
510
+ const directCtx = createDirectCtx();
511
+ const result = await triggerConsolidation(
512
+ pi,
513
+ mockStore,
514
+ "memory",
515
+ undefined,
516
+ 60000,
517
+ "memory",
518
+ directTransportLlmConfig,
519
+ directCtx,
520
+ null,
521
+ null,
522
+ makeDirectDeps({ ok: true, appliedCount: 3 }),
523
+ );
524
+
525
+ assert.strictEqual(result.consolidated, true);
526
+ assert.strictEqual(result.error, undefined);
527
+ assert.strictEqual(directCalls.length, 1);
528
+ assert.strictEqual(execCalls.length, 0, "subprocess must not run on successful direct consolidation");
529
+ const directOptions = directCalls[0]?.[3] as {
530
+ requireAtomicShrink?: boolean;
531
+ expectedTarget?: string;
532
+ };
533
+ assert.strictEqual(directOptions.requireAtomicShrink, true);
534
+ assert.strictEqual(directOptions.expectedTarget, "memory");
535
+ });
536
+
537
+ it("falls back to subprocess when direct transport succeeds with appliedCount 0", async () => {
538
+ const pi = createMockPi();
539
+ const directCtx = createDirectCtx();
540
+ const result = await triggerConsolidation(
541
+ pi,
542
+ mockStore,
543
+ "memory",
544
+ undefined,
545
+ 60000,
546
+ "memory",
547
+ directTransportLlmConfig,
548
+ directCtx,
549
+ null,
550
+ null,
551
+ makeDirectDeps({ ok: true, appliedCount: 0 }),
552
+ );
553
+
554
+ assert.strictEqual(result.consolidated, true);
555
+ assert.strictEqual(directCalls.length, 1);
556
+ assert.strictEqual(execCalls.length, 1, "empty direct result must fall back to subprocess");
557
+ });
558
+
559
+ it("falls back to subprocess when direct transport returns ok false", async () => {
560
+ const pi = createMockPi();
561
+ const directCtx = createDirectCtx();
562
+ const result = await triggerConsolidation(
563
+ pi,
564
+ mockStore,
565
+ "memory",
566
+ undefined,
567
+ 60000,
568
+ "memory",
569
+ directTransportLlmConfig,
570
+ directCtx,
571
+ null,
572
+ null,
573
+ makeDirectDeps({ ok: false, appliedCount: 0 }),
574
+ );
575
+
576
+ assert.strictEqual(result.consolidated, true);
577
+ assert.strictEqual(directCalls.length, 1);
578
+ assert.strictEqual(execCalls.length, 1, "failed direct result must fall back to subprocess");
579
+ });
580
+
581
+ it("falls back to subprocess when direct transport throws without propagating", async () => {
582
+ const pi = createMockPi();
583
+ const directCtx = createDirectCtx();
584
+ const result = await triggerConsolidation(
585
+ pi,
586
+ mockStore,
587
+ "memory",
588
+ undefined,
589
+ 60000,
590
+ "memory",
591
+ directTransportLlmConfig,
592
+ directCtx,
593
+ null,
594
+ null,
595
+ makeDirectDeps("throw"),
596
+ );
597
+
598
+ assert.strictEqual(result.consolidated, true);
599
+ assert.strictEqual(directCalls.length, 1);
600
+ assert.strictEqual(execCalls.length, 1, "thrown direct error must fall back to subprocess");
601
+ });
602
+
603
+ it("does not attempt direct transport when directCtx is null", async () => {
604
+ const pi = createMockPi();
605
+ const result = await triggerConsolidation(
606
+ pi,
607
+ mockStore,
608
+ "memory",
609
+ undefined,
610
+ 60000,
611
+ "memory",
612
+ directTransportLlmConfig,
613
+ null,
614
+ null,
615
+ null,
616
+ makeDirectDeps({ ok: true, appliedCount: 3 }),
617
+ );
618
+
619
+ assert.strictEqual(result.consolidated, true);
620
+ assert.strictEqual(directCalls.length, 0, "direct path must be skipped without directCtx");
621
+ assert.strictEqual(execCalls.length, 1, "subprocess-only path must still consolidate");
622
+ });
623
+ });
624
+ });
625
+
626
+ describe("registerConsolidateCommand", () => {
627
+ beforeEach(() => {
628
+ execCalls = [];
629
+ });
630
+
631
+ it("includes project memory when a project store is available", async () => {
632
+ let handler: any;
633
+ const notifications: string[] = [];
634
+ let projectReloaded = false;
635
+
636
+ const pi = {
637
+ on: () => {},
638
+ exec: async (...args: any[]) => {
639
+ execCalls.push(captureExecArgs(args));
640
+ return { code: 0, stdout: "Done", stderr: "" };
641
+ },
642
+ registerTool: () => {},
643
+ registerCommand: (_name: string, command: any) => {
644
+ handler = command.handler;
645
+ },
646
+ } as any;
647
+
648
+ const projectStore = {
649
+ getMemoryEntries: () => ["project fact"],
650
+ getUserEntries: () => [],
651
+ getStorageIdentity: async (target: string) => path.join("project-store", target),
652
+ loadFromDisk: async () => { projectReloaded = true; },
653
+ } as any;
654
+
655
+ registerConsolidateCommand(pi, mockStore, 60000, projectStore, "demo-project");
656
+ await handler({}, {
657
+ signal: undefined,
658
+ ui: { notify: (message: string) => { notifications.push(message); } },
659
+ });
660
+
661
+ assert.strictEqual(execCalls.length, 4, "should consolidate memory, user, failure, and project stores");
662
+ const failurePrompt = childPrompt(execCalls[2]);
663
+ assert.ok(failurePrompt.includes("Failure Memory"), "failure prompt should be labeled");
664
+ assert.ok(failurePrompt.includes("failure lesson 1"), "failure prompt should include failure entries");
665
+ assert.ok(failurePrompt.includes("Target: 'failure'"), "failure prompt should use target='failure'");
666
+ const projectPrompt = childPrompt(execCalls[3]);
667
+ assert.ok(projectPrompt.includes("Project Memory"), "project prompt should be labeled");
668
+ assert.ok(projectPrompt.includes("project fact"), "project prompt should include project entries");
669
+ assert.ok(projectPrompt.includes("Target: 'project'"), "project prompt should use target='project'");
670
+ assert.ok(projectReloaded, "project store should reload after consolidation");
671
+ assert.ok(notifications.some((message) => message.includes("Starting memory consolidation")), "should show an initial progress notification");
672
+ assert.ok(notifications.some((message) => message.includes("⏳ Consolidating memory")), "should show per-target progress");
673
+ const finalNotification = notifications[notifications.length - 1] ?? "";
674
+ assert.ok(finalNotification.includes("failure: ✅ consolidated"), "final notification should include failure result");
675
+ assert.ok(finalNotification.includes("project:demo-project: ✅ consolidated"), "final notification should include project result");
676
+ });
677
+
678
+ it("passes the configured timeout through to the manual consolidate child", async () => {
679
+ await runManualConsolidate(240000);
680
+
681
+ assert.ok(execCalls.length > 0, "manual consolidation should spawn children");
682
+ for (const call of execCalls) {
683
+ assert.strictEqual(call[1][1], "240000");
684
+ assert.strictEqual(call[2]?.timeout, 245000);
685
+ }
686
+ });
687
+
688
+ it("defaults the manual consolidate command to the shared consolidation timeout", async () => {
689
+ await runManualConsolidate();
690
+
691
+ assert.ok(execCalls.length > 0, "manual consolidation should spawn children");
692
+ for (const call of execCalls) {
693
+ assert.strictEqual(call[1][1], String(DEFAULT_CONSOLIDATION_TIMEOUT_MS));
694
+ }
695
+ });
696
+
697
+ it("does not throw if the command ctx becomes stale before the final summary notify", async () => {
698
+ let handler: any;
699
+
700
+ const pi = {
701
+ on: () => {},
702
+ exec: async (...args: any[]) => {
703
+ execCalls.push(captureExecArgs(args));
704
+ return { code: 0, stdout: "Done", stderr: "" };
705
+ },
706
+ registerTool: () => {},
707
+ registerCommand: (_name: string, command: any) => {
708
+ handler = command.handler;
709
+ },
710
+ } as any;
711
+
712
+ registerConsolidateCommand(pi, mockStore, 60000);
713
+
714
+ await assert.doesNotReject(async () => {
715
+ await handler({}, {
716
+ signal: undefined,
717
+ ui: {
718
+ notify: () => {
719
+ throw new Error("This extension ctx is stale after session replacement or reload.");
720
+ },
721
+ },
722
+ });
723
+ });
724
+ });
725
+
726
+ it("passes command ctx to direct consolidation and reflects success in the summary", async () => {
727
+ directCalls = [];
728
+ let handler: ((_args: unknown, ctx: unknown) => Promise<void>) | undefined;
729
+ const notifications: string[] = [];
730
+ const commandCtx = {
731
+ model: {},
732
+ modelRegistry: {},
733
+ signal: undefined,
734
+ ui: { notify: (message: string) => { notifications.push(message); } },
735
+ _tag: "manual-consolidate-ctx",
736
+ };
737
+
738
+ const pi = {
739
+ on: () => {},
740
+ exec: async (...args: unknown[]) => {
741
+ execCalls.push(captureExecArgs(args as Parameters<typeof captureExecArgs>[0]));
742
+ return { code: 0, stdout: "Done", stderr: "" };
743
+ },
744
+ registerTool: () => {},
745
+ registerCommand: (_name: string, command: { handler: typeof handler }) => {
746
+ handler = command.handler;
747
+ },
748
+ } as unknown as Parameters<typeof registerConsolidateCommand>[0];
749
+
750
+ registerConsolidateCommand(
751
+ pi,
752
+ mockStore,
753
+ 60000,
754
+ null,
755
+ null,
756
+ directTransportLlmConfig,
757
+ null,
758
+ makeDirectDeps({ ok: true, appliedCount: 2 }),
759
+ );
760
+
761
+ assert.ok(handler, "command handler should be registered");
762
+ await handler!({}, commandCtx);
763
+
764
+ assert.strictEqual(directCalls.length, 3, "memory, user, and failure targets should use direct transport");
765
+ assert.strictEqual(execCalls.length, 0, "successful direct consolidation should not spawn subprocess");
766
+ for (const call of directCalls) {
767
+ assert.strictEqual(call[0], commandCtx, "runDirectMemoryCompletion must receive the command ctx");
768
+ }
769
+
770
+ const finalNotification = notifications[notifications.length - 1] ?? "";
771
+ assert.ok(finalNotification.includes("memory: ✅ consolidated"), "summary should show memory consolidated");
772
+ assert.ok(finalNotification.includes("user: ✅ consolidated"), "summary should show user consolidated");
773
+ assert.ok(finalNotification.includes("failure: ✅ consolidated"), "summary should show failure consolidated");
774
+ });
775
+ });
776
+
777
+ describe("MemoryStore auto-consolidation integration", () => {
778
+ let MEMORY_DIR = "";
779
+
780
+ before(async () => {
781
+ MEMORY_DIR = await fs.mkdtemp(path.join(os.tmpdir(), "pi-consolidation-test-"));
782
+ });
783
+
784
+ after(async () => {
785
+ try { await fs.rm(MEMORY_DIR, { recursive: true, force: true }); } catch { /* ignore */ }
786
+ });
787
+
788
+ it("add() triggers consolidation when over limit with consolidator", async () => {
789
+ let consolidatorCalled = false;
790
+ let consolidatorTarget: string | undefined;
791
+
792
+ const { MemoryStore } = await import("../../src/store/memory-store.js");
793
+ const store = new MemoryStore({
794
+ memoryCharLimit: 120,
795
+ userCharLimit: 120,
796
+ nudgeInterval: 10,
797
+ reviewEnabled: false,
798
+ flushOnCompact: false,
799
+ flushOnShutdown: false,
800
+ flushMinTurns: 6,
801
+ autoConsolidate: true,
802
+ overflowGraceMs: 0,
803
+ correctionDetection: false,
804
+ nudgeToolCalls: 15,
805
+ memoryDir: MEMORY_DIR,
806
+ });
807
+
808
+ // Mock consolidator that actually frees space by removing all entries
809
+ store.setConsolidator(async (target, signal) => {
810
+ consolidatorCalled = true;
811
+ consolidatorTarget = target;
812
+ // Remove all entries to simulate consolidation freeing space
813
+ const entries = target === "memory" ? store.getMemoryEntries() : store.getUserEntries();
814
+ for (const entry of [...entries]) {
815
+ await store.remove(target, entry);
816
+ }
817
+ return { consolidated: true };
818
+ });
819
+
820
+ await store.loadFromDisk();
821
+
822
+ // Fill up memory to near limit (each entry gets ~44 chars of metadata)
823
+ const smallEntry = "a".repeat(60);
824
+ await store.add("memory", smallEntry);
825
+
826
+ // This add should exceed limit and trigger consolidation
827
+ const result = await store.add("memory", "b".repeat(20));
828
+
829
+ assert.ok(consolidatorCalled, "consolidator should have been called");
830
+ assert.strictEqual(consolidatorTarget, "memory");
831
+ // After consolidation removes entries, the new entry should fit
832
+ assert.ok(result.success, "add should succeed after consolidation");
833
+ });
834
+
835
+ it("add() skips consolidation when autoConsolidate is false", async () => {
836
+ let consolidatorCalled = false;
837
+ const { MemoryStore } = await import("../../src/store/memory-store.js");
838
+
839
+ const store = new MemoryStore({
840
+ memoryCharLimit: 50,
841
+ userCharLimit: 50,
842
+ nudgeInterval: 10,
843
+ reviewEnabled: false,
844
+ flushOnCompact: false,
845
+ flushOnShutdown: false,
846
+ flushMinTurns: 6,
847
+ autoConsolidate: false,
848
+ correctionDetection: false,
849
+ nudgeToolCalls: 15,
850
+ memoryDir: MEMORY_DIR,
851
+ });
852
+
853
+ store.setConsolidator(async () => {
854
+ consolidatorCalled = true;
855
+ return { consolidated: true };
856
+ });
857
+
858
+ await store.loadFromDisk();
859
+
860
+ const result = await store.add("memory", "x".repeat(60));
861
+ assert.ok(!consolidatorCalled, "consolidator should NOT be called when autoConsolidate is false");
862
+ assert.ok(!result.success, "should return error");
863
+ assert.ok(result.error!.includes("exceed"), "should mention exceeding limit");
864
+ });
865
+
866
+ it("add() skips consolidation when no consolidator set", async () => {
867
+ const { MemoryStore } = await import("../../src/store/memory-store.js");
868
+
869
+ const store = new MemoryStore({
870
+ memoryCharLimit: 50,
871
+ userCharLimit: 50,
872
+ nudgeInterval: 10,
873
+ reviewEnabled: false,
874
+ flushOnCompact: false,
875
+ flushOnShutdown: false,
876
+ flushMinTurns: 6,
877
+ autoConsolidate: true,
878
+ correctionDetection: false,
879
+ nudgeToolCalls: 15,
880
+ memoryDir: MEMORY_DIR,
881
+ });
882
+
883
+ // Intentionally NOT calling setConsolidator
884
+ await store.loadFromDisk();
885
+
886
+ const result = await store.add("memory", "x".repeat(60));
887
+ assert.ok(!result.success, "should return error");
888
+ assert.ok(result.error!.includes("exceed"), "should mention exceeding limit");
889
+ });
890
+
891
+ async function storeWithConsolidator(
892
+ dirName: string,
893
+ consolidator: () => Promise<{ consolidated: boolean; error?: string }>,
894
+ ): Promise<MemoryStore> {
895
+ const store = new MemoryStore({
896
+ memoryCharLimit: 120,
897
+ userCharLimit: 120,
898
+ nudgeInterval: 10,
899
+ reviewEnabled: false,
900
+ flushOnCompact: false,
901
+ flushOnShutdown: false,
902
+ flushMinTurns: 6,
903
+ overflowGraceMs: 0,
904
+ autoConsolidate: true,
905
+ correctionDetection: false,
906
+ nudgeToolCalls: 15,
907
+ memoryDir: path.join(MEMORY_DIR, dirName),
908
+ });
909
+ store.setConsolidator(consolidator);
910
+ await store.loadFromDisk();
911
+ await store.add("memory", "a".repeat(60));
912
+ return store;
913
+ }
914
+
915
+ it("add() surfaces the reason a failed auto-consolidation reported", async () => {
916
+ const store = await storeWithConsolidator("reason", async () => ({
917
+ consolidated: false,
918
+ error: "Consolidation subprocess was terminated (likely timeout or cancellation). Timeout: 180000ms.",
919
+ }));
920
+
921
+ const result = await store.add("memory", "b".repeat(20));
922
+
923
+ assert.ok(!result.success, "over-capacity add should still fail");
924
+ assert.ok(result.error!.startsWith("Memory at "), "original capacity error must be preserved");
925
+ assert.ok(
926
+ result.error!.includes("Auto-consolidation attempted but failed: Consolidation subprocess was terminated"),
927
+ `expected the consolidation reason to be appended, got: ${result.error}`,
928
+ );
929
+ });
930
+
931
+ it("add() reports a reasonless consolidation failure instead of staying silent", async () => {
932
+ const store = await storeWithConsolidator("reasonless", async () => ({ consolidated: false }));
933
+
934
+ const result = await store.add("memory", "b".repeat(20));
935
+
936
+ assert.ok(result.error!.includes("Auto-consolidation attempted but failed: no reason reported"), result.error);
937
+ });
938
+
939
+ it("add() asks for a retry instead of reporting failure when consolidation is deferred", async () => {
940
+ const store = await storeWithConsolidator("deferred", async () => ({
941
+ consolidated: false,
942
+ deferred: true,
943
+ error: "Consolidation already in progress for target 'memory' in another session (waited 5000ms).",
944
+ }));
945
+
946
+ const result = await store.add("memory", "b".repeat(20));
947
+
948
+ assert.ok(!result.success, "the entry genuinely was not saved");
949
+ assert.ok(result.error!.startsWith("Memory at "), "original capacity error must be preserved");
950
+ assert.ok(result.error!.includes("retry in a moment"), result.error);
951
+ assert.ok(
952
+ !result.error!.includes("Auto-consolidation attempted but failed"),
953
+ `lock contention must not read as a broken consolidation, got: ${result.error}`,
954
+ );
955
+ });
956
+
957
+ it("add() surfaces a consolidator that throws", async () => {
958
+ const store = await storeWithConsolidator("throws", async () => {
959
+ throw new Error("spawn ENOENT");
960
+ });
961
+
962
+ const result = await store.add("memory", "b".repeat(20));
963
+
964
+ assert.ok(!result.success, "a thrown consolidator must not surface as success");
965
+ assert.ok(result.error!.includes("consolidator threw"), result.error);
966
+ assert.ok(result.error!.includes("spawn ENOENT"), result.error);
967
+ });
968
+
969
+ it("add() distinguishes a consolidation that ran but freed nothing", async () => {
970
+ const store = await storeWithConsolidator("no-space", async () => ({ consolidated: true }));
971
+
972
+ const result = await store.add("memory", "b".repeat(20));
973
+
974
+ assert.ok(!result.success, "add should still fail when nothing was freed");
975
+ assert.ok(
976
+ result.error!.includes("Auto-consolidation ran but did not free enough space."),
977
+ result.error,
978
+ );
979
+ assert.ok(
980
+ !result.error!.includes("attempted but failed"),
981
+ "a successful-but-ineffective consolidation is not a consolidation failure",
982
+ );
983
+ });
984
+ });