@agentmemory/agentmemory 0.9.30 → 0.9.32

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 (334) hide show
  1. package/.env.example +57 -48
  2. package/AGENTS.md +8 -8
  3. package/README.md +256 -819
  4. package/dist/.env.example +57 -48
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +2257 -1027
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/compression-recovery-CZh63Jmz.mjs +5997 -0
  9. package/dist/compression-recovery-CZh63Jmz.mjs.map +1 -0
  10. package/dist/{connect-B4_ca6BD.mjs → connect-BX3njQ3Y.mjs} +897 -276
  11. package/dist/connect-BX3njQ3Y.mjs.map +1 -0
  12. package/dist/hooks/_capture.d.mts +5 -1
  13. package/dist/hooks/_capture.d.mts.map +1 -1
  14. package/dist/hooks/_capture.mjs +141 -26
  15. package/dist/hooks/_capture.mjs.map +1 -1
  16. package/dist/hooks/notification.mjs +20 -6
  17. package/dist/hooks/notification.mjs.map +1 -1
  18. package/dist/hooks/post-commit.mjs +83 -10
  19. package/dist/hooks/post-commit.mjs.map +1 -1
  20. package/dist/hooks/post-tool-failure.mjs +19 -6
  21. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  22. package/dist/hooks/post-tool-use.mjs +19 -6
  23. package/dist/hooks/post-tool-use.mjs.map +1 -1
  24. package/dist/hooks/pre-compact.mjs +104 -22
  25. package/dist/hooks/pre-compact.mjs.map +1 -1
  26. package/dist/hooks/pre-tool-use.mjs +83 -10
  27. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  28. package/dist/hooks/prompt-submit.mjs +12 -4
  29. package/dist/hooks/prompt-submit.mjs.map +1 -1
  30. package/dist/hooks/session-end.mjs +59 -17
  31. package/dist/hooks/session-end.mjs.map +1 -1
  32. package/dist/hooks/session-start.mjs +95 -15
  33. package/dist/hooks/session-start.mjs.map +1 -1
  34. package/dist/hooks/stop.mjs +75 -72
  35. package/dist/hooks/stop.mjs.map +1 -1
  36. package/dist/hooks/subagent-start.mjs +13 -5
  37. package/dist/hooks/subagent-start.mjs.map +1 -1
  38. package/dist/hooks/subagent-stop.mjs +32 -10
  39. package/dist/hooks/subagent-stop.mjs.map +1 -1
  40. package/dist/hooks/task-completed.mjs +21 -6
  41. package/dist/hooks/task-completed.mjs.map +1 -1
  42. package/dist/{image-refs-T_urbwHM.mjs → image-refs-Cyta4Lf9.mjs} +2 -2
  43. package/dist/{image-refs-T_urbwHM.mjs.map → image-refs-Cyta4Lf9.mjs.map} +1 -1
  44. package/dist/index.mjs +16390 -11530
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/integrations/dsh.d.mts +55 -0
  47. package/dist/integrations/dsh.d.mts.map +1 -0
  48. package/dist/integrations/dsh.mjs +1172 -0
  49. package/dist/integrations/dsh.mjs.map +1 -0
  50. package/dist/{keyed-mutex-DchXsyfP.mjs → keyed-mutex-DCUh7PG-.mjs} +58 -9
  51. package/dist/keyed-mutex-DCUh7PG-.mjs.map +1 -0
  52. package/dist/{secret-store-BnY3A50C.mjs → secret-store-iYXRcrO8.mjs} +717 -474
  53. package/dist/secret-store-iYXRcrO8.mjs.map +1 -0
  54. package/dist/session-arguments-zxsbbd8y.mjs +113 -0
  55. package/dist/session-arguments-zxsbbd8y.mjs.map +1 -0
  56. package/dist/{src-Jsq9LCeH.mjs → src-C23nMpda.mjs} +5263 -4667
  57. package/dist/src-C23nMpda.mjs.map +1 -0
  58. package/dist/{standalone-DKCdrf1d.mjs → standalone-BYo5USDb.mjs} +379 -63
  59. package/dist/standalone-BYo5USDb.mjs.map +1 -0
  60. package/dist/standalone.d.mts +1 -1
  61. package/dist/standalone.d.mts.map +1 -1
  62. package/dist/standalone.mjs +864 -81
  63. package/dist/standalone.mjs.map +1 -1
  64. package/dist/viewer/index.html +337 -168
  65. package/integrations/hermes/README.md +94 -0
  66. package/integrations/hermes/__init__.py +670 -0
  67. package/integrations/hermes/plugin.yaml +27 -0
  68. package/integrations/omp/LICENSE +190 -0
  69. package/integrations/omp/README.md +76 -0
  70. package/integrations/omp/capture.ts +1 -0
  71. package/integrations/omp/dist/index.mjs +1477 -0
  72. package/integrations/omp/index.ts +488 -0
  73. package/integrations/omp/package.json +13 -0
  74. package/integrations/omp/security.ts +67 -0
  75. package/integrations/omp/skills/agentmemory-agents/REFERENCE.md +32 -0
  76. package/integrations/omp/skills/agentmemory-agents/SKILL.md +48 -0
  77. package/integrations/omp/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  78. package/integrations/omp/skills/agentmemory-architecture/SKILL.md +37 -0
  79. package/integrations/omp/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  80. package/integrations/omp/skills/agentmemory-config/REFERENCE.md +91 -0
  81. package/integrations/omp/skills/agentmemory-config/SKILL.md +98 -0
  82. package/integrations/omp/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  83. package/integrations/omp/skills/agentmemory-hooks/REFERENCE.md +20 -0
  84. package/integrations/omp/skills/agentmemory-hooks/SKILL.md +60 -0
  85. package/integrations/omp/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  86. package/integrations/omp/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
  87. package/integrations/omp/skills/agentmemory-mcp-tools/SKILL.md +52 -0
  88. package/integrations/omp/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  89. package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +159 -0
  90. package/integrations/omp/skills/agentmemory-rest-api/SKILL.md +49 -0
  91. package/integrations/omp/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  92. package/integrations/omp/skills/commit-context/EXAMPLES.md +71 -0
  93. package/integrations/omp/skills/commit-context/SKILL.md +64 -0
  94. package/integrations/omp/skills/commit-context/TROUBLESHOOTING.md +70 -0
  95. package/integrations/omp/skills/commit-history/EXAMPLES.md +71 -0
  96. package/integrations/omp/skills/commit-history/SKILL.md +62 -0
  97. package/integrations/omp/skills/commit-history/TROUBLESHOOTING.md +70 -0
  98. package/integrations/omp/skills/forget/EXAMPLES.md +40 -0
  99. package/integrations/omp/skills/forget/SKILL.md +73 -0
  100. package/integrations/omp/skills/forget/TROUBLESHOOTING.md +70 -0
  101. package/integrations/omp/skills/handoff/EXAMPLES.md +57 -0
  102. package/integrations/omp/skills/handoff/SKILL.md +75 -0
  103. package/integrations/omp/skills/handoff/TROUBLESHOOTING.md +70 -0
  104. package/integrations/omp/skills/lesson/SKILL.md +64 -0
  105. package/integrations/omp/skills/lesson/TROUBLESHOOTING.md +70 -0
  106. package/integrations/omp/skills/memory-discipline/SKILL.md +65 -0
  107. package/integrations/omp/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  108. package/integrations/omp/skills/recall/EXAMPLES.md +74 -0
  109. package/integrations/omp/skills/recall/SKILL.md +74 -0
  110. package/integrations/omp/skills/recall/TROUBLESHOOTING.md +70 -0
  111. package/integrations/omp/skills/recap/EXAMPLES.md +62 -0
  112. package/integrations/omp/skills/recap/SKILL.md +68 -0
  113. package/integrations/omp/skills/recap/TROUBLESHOOTING.md +70 -0
  114. package/integrations/omp/skills/remember/EXAMPLES.md +67 -0
  115. package/integrations/omp/skills/remember/SKILL.md +66 -0
  116. package/integrations/omp/skills/remember/TROUBLESHOOTING.md +70 -0
  117. package/integrations/omp/skills/session-history/EXAMPLES.md +59 -0
  118. package/integrations/omp/skills/session-history/SKILL.md +67 -0
  119. package/integrations/omp/skills/session-history/TROUBLESHOOTING.md +70 -0
  120. package/integrations/omp/skills/write-agentmemory-skill/SKILL.md +41 -0
  121. package/integrations/omp/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  122. package/integrations/openclaw/LICENSE +190 -0
  123. package/integrations/openclaw/README.md +98 -0
  124. package/integrations/openclaw/openclaw.plugin.json +71 -0
  125. package/integrations/openclaw/package.json +38 -0
  126. package/integrations/openclaw/plugin.mjs +425 -0
  127. package/integrations/openclaw/plugin.yaml +26 -0
  128. package/integrations/openclaw/skills/agentmemory-agents/REFERENCE.md +32 -0
  129. package/integrations/openclaw/skills/agentmemory-agents/SKILL.md +48 -0
  130. package/integrations/openclaw/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  131. package/integrations/openclaw/skills/agentmemory-architecture/SKILL.md +37 -0
  132. package/integrations/openclaw/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  133. package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +91 -0
  134. package/integrations/openclaw/skills/agentmemory-config/SKILL.md +98 -0
  135. package/integrations/openclaw/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  136. package/integrations/openclaw/skills/agentmemory-hooks/REFERENCE.md +20 -0
  137. package/integrations/openclaw/skills/agentmemory-hooks/SKILL.md +60 -0
  138. package/integrations/openclaw/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  139. package/integrations/openclaw/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
  140. package/integrations/openclaw/skills/agentmemory-mcp-tools/SKILL.md +52 -0
  141. package/integrations/openclaw/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  142. package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +159 -0
  143. package/integrations/openclaw/skills/agentmemory-rest-api/SKILL.md +49 -0
  144. package/integrations/openclaw/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  145. package/integrations/openclaw/skills/commit-context/EXAMPLES.md +71 -0
  146. package/integrations/openclaw/skills/commit-context/SKILL.md +64 -0
  147. package/integrations/openclaw/skills/commit-context/TROUBLESHOOTING.md +70 -0
  148. package/integrations/openclaw/skills/commit-history/EXAMPLES.md +71 -0
  149. package/integrations/openclaw/skills/commit-history/SKILL.md +62 -0
  150. package/integrations/openclaw/skills/commit-history/TROUBLESHOOTING.md +70 -0
  151. package/integrations/openclaw/skills/forget/EXAMPLES.md +40 -0
  152. package/integrations/openclaw/skills/forget/SKILL.md +73 -0
  153. package/integrations/openclaw/skills/forget/TROUBLESHOOTING.md +70 -0
  154. package/integrations/openclaw/skills/handoff/EXAMPLES.md +57 -0
  155. package/integrations/openclaw/skills/handoff/SKILL.md +75 -0
  156. package/integrations/openclaw/skills/handoff/TROUBLESHOOTING.md +70 -0
  157. package/integrations/openclaw/skills/lesson/SKILL.md +64 -0
  158. package/integrations/openclaw/skills/lesson/TROUBLESHOOTING.md +70 -0
  159. package/integrations/openclaw/skills/memory-discipline/SKILL.md +65 -0
  160. package/integrations/openclaw/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  161. package/integrations/openclaw/skills/recall/EXAMPLES.md +74 -0
  162. package/integrations/openclaw/skills/recall/SKILL.md +74 -0
  163. package/integrations/openclaw/skills/recall/TROUBLESHOOTING.md +70 -0
  164. package/integrations/openclaw/skills/recap/EXAMPLES.md +62 -0
  165. package/integrations/openclaw/skills/recap/SKILL.md +68 -0
  166. package/integrations/openclaw/skills/recap/TROUBLESHOOTING.md +70 -0
  167. package/integrations/openclaw/skills/remember/EXAMPLES.md +67 -0
  168. package/integrations/openclaw/skills/remember/SKILL.md +66 -0
  169. package/integrations/openclaw/skills/remember/TROUBLESHOOTING.md +70 -0
  170. package/integrations/openclaw/skills/session-history/EXAMPLES.md +59 -0
  171. package/integrations/openclaw/skills/session-history/SKILL.md +67 -0
  172. package/integrations/openclaw/skills/session-history/TROUBLESHOOTING.md +70 -0
  173. package/integrations/openclaw/skills/write-agentmemory-skill/SKILL.md +41 -0
  174. package/integrations/openclaw/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  175. package/integrations/pi/LICENSE +190 -0
  176. package/integrations/pi/README.md +46 -51
  177. package/integrations/pi/dist/index.mjs +1460 -0
  178. package/integrations/pi/index.ts +192 -175
  179. package/integrations/pi/package.json +17 -7
  180. package/integrations/pi/security.ts +6 -26
  181. package/integrations/pi/skills/agentmemory-agents/REFERENCE.md +32 -0
  182. package/integrations/pi/skills/agentmemory-agents/SKILL.md +48 -0
  183. package/integrations/pi/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  184. package/integrations/pi/skills/agentmemory-architecture/SKILL.md +37 -0
  185. package/integrations/pi/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  186. package/integrations/pi/skills/agentmemory-config/REFERENCE.md +91 -0
  187. package/integrations/pi/skills/agentmemory-config/SKILL.md +98 -0
  188. package/integrations/pi/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  189. package/integrations/pi/skills/agentmemory-hooks/REFERENCE.md +20 -0
  190. package/integrations/pi/skills/agentmemory-hooks/SKILL.md +60 -0
  191. package/integrations/pi/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  192. package/integrations/pi/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
  193. package/integrations/pi/skills/agentmemory-mcp-tools/SKILL.md +52 -0
  194. package/integrations/pi/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  195. package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +159 -0
  196. package/integrations/pi/skills/agentmemory-rest-api/SKILL.md +49 -0
  197. package/integrations/pi/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  198. package/integrations/pi/skills/commit-context/EXAMPLES.md +71 -0
  199. package/integrations/pi/skills/commit-context/SKILL.md +64 -0
  200. package/integrations/pi/skills/commit-context/TROUBLESHOOTING.md +70 -0
  201. package/integrations/pi/skills/commit-history/EXAMPLES.md +71 -0
  202. package/integrations/pi/skills/commit-history/SKILL.md +62 -0
  203. package/integrations/pi/skills/commit-history/TROUBLESHOOTING.md +70 -0
  204. package/integrations/pi/skills/forget/EXAMPLES.md +40 -0
  205. package/integrations/pi/skills/forget/SKILL.md +73 -0
  206. package/integrations/pi/skills/forget/TROUBLESHOOTING.md +70 -0
  207. package/integrations/pi/skills/handoff/EXAMPLES.md +57 -0
  208. package/integrations/pi/skills/handoff/SKILL.md +75 -0
  209. package/integrations/pi/skills/handoff/TROUBLESHOOTING.md +70 -0
  210. package/integrations/pi/skills/lesson/SKILL.md +64 -0
  211. package/integrations/pi/skills/lesson/TROUBLESHOOTING.md +70 -0
  212. package/integrations/pi/skills/memory-discipline/SKILL.md +65 -0
  213. package/integrations/pi/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  214. package/integrations/pi/skills/recall/EXAMPLES.md +74 -0
  215. package/integrations/pi/skills/recall/SKILL.md +74 -0
  216. package/integrations/pi/skills/recall/TROUBLESHOOTING.md +70 -0
  217. package/integrations/pi/skills/recap/EXAMPLES.md +62 -0
  218. package/integrations/pi/skills/recap/SKILL.md +68 -0
  219. package/integrations/pi/skills/recap/TROUBLESHOOTING.md +70 -0
  220. package/integrations/pi/skills/remember/EXAMPLES.md +67 -0
  221. package/integrations/pi/skills/remember/SKILL.md +66 -0
  222. package/integrations/pi/skills/remember/TROUBLESHOOTING.md +70 -0
  223. package/integrations/pi/skills/session-history/EXAMPLES.md +59 -0
  224. package/integrations/pi/skills/session-history/SKILL.md +67 -0
  225. package/integrations/pi/skills/session-history/TROUBLESHOOTING.md +70 -0
  226. package/integrations/pi/skills/write-agentmemory-skill/SKILL.md +41 -0
  227. package/integrations/pi/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  228. package/package.json +33 -11
  229. package/plugin/.claude-plugin/plugin.json +16 -3
  230. package/plugin/.codex-plugin/plugin.json +4 -3
  231. package/plugin/.devin-plugin/plugin.json +6 -5
  232. package/plugin/.mcp.json +5 -1
  233. package/plugin/README.md +71 -0
  234. package/plugin/cursor/hooks.json +23 -8
  235. package/plugin/hooks/hooks.json +24 -12
  236. package/plugin/hooks.json +67 -0
  237. package/plugin/opencode/README.md +1 -1
  238. package/plugin/opencode/agentmemory-capture.ts +25 -11
  239. package/plugin/opencode/plugin.json +1 -1
  240. package/plugin/plugin.json +1 -1
  241. package/plugin/scripts/_capture.d.mts +5 -1
  242. package/plugin/scripts/_capture.d.mts.map +1 -1
  243. package/plugin/scripts/_capture.mjs +141 -26
  244. package/plugin/scripts/_capture.mjs.map +1 -1
  245. package/plugin/scripts/cursor/run-detached.mjs +786 -0
  246. package/plugin/scripts/cursor/run-detached.mjs.map +1 -0
  247. package/plugin/scripts/cursor/run-hook.d.mts +1 -0
  248. package/plugin/scripts/cursor/run-hook.mjs +704 -0
  249. package/plugin/scripts/cursor/run-hook.mjs.map +1 -0
  250. package/plugin/scripts/notification.mjs +20 -6
  251. package/plugin/scripts/notification.mjs.map +1 -1
  252. package/plugin/scripts/plugin-bridge.mjs +61 -11
  253. package/plugin/scripts/post-tool-failure.mjs +19 -6
  254. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  255. package/plugin/scripts/post-tool-use.mjs +19 -6
  256. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  257. package/plugin/scripts/pre-compact.mjs +104 -22
  258. package/plugin/scripts/pre-compact.mjs.map +1 -1
  259. package/plugin/scripts/pre-tool-use.mjs +83 -10
  260. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  261. package/plugin/scripts/prompt-submit.mjs +12 -4
  262. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  263. package/plugin/scripts/session-end.mjs +59 -17
  264. package/plugin/scripts/session-end.mjs.map +1 -1
  265. package/plugin/scripts/session-start.mjs +95 -15
  266. package/plugin/scripts/session-start.mjs.map +1 -1
  267. package/plugin/scripts/stop.mjs +75 -72
  268. package/plugin/scripts/stop.mjs.map +1 -1
  269. package/plugin/scripts/subagent-start.mjs +13 -5
  270. package/plugin/scripts/subagent-start.mjs.map +1 -1
  271. package/plugin/scripts/subagent-stop.mjs +32 -10
  272. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  273. package/plugin/scripts/task-completed.mjs +21 -6
  274. package/plugin/scripts/task-completed.mjs.map +1 -1
  275. package/plugin/skills/_shared/TROUBLESHOOTING.md +21 -7
  276. package/plugin/skills/agentmemory-agents/REFERENCE.md +8 -7
  277. package/plugin/skills/agentmemory-agents/SKILL.md +19 -5
  278. package/plugin/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  279. package/plugin/skills/agentmemory-architecture/SKILL.md +8 -4
  280. package/plugin/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  281. package/plugin/skills/agentmemory-config/REFERENCE.md +13 -15
  282. package/plugin/skills/agentmemory-config/SKILL.md +34 -10
  283. package/plugin/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  284. package/plugin/skills/agentmemory-hooks/SKILL.md +31 -10
  285. package/plugin/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  286. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +5 -5
  287. package/plugin/skills/agentmemory-mcp-tools/SKILL.md +16 -3
  288. package/plugin/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  289. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +10 -1
  290. package/plugin/skills/agentmemory-rest-api/SKILL.md +12 -6
  291. package/plugin/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  292. package/plugin/skills/commit-context/SKILL.md +1 -1
  293. package/plugin/skills/commit-context/TROUBLESHOOTING.md +70 -0
  294. package/plugin/skills/commit-history/SKILL.md +1 -1
  295. package/plugin/skills/commit-history/TROUBLESHOOTING.md +70 -0
  296. package/plugin/skills/forget/EXAMPLES.md +19 -54
  297. package/plugin/skills/forget/SKILL.md +16 -12
  298. package/plugin/skills/forget/TROUBLESHOOTING.md +70 -0
  299. package/plugin/skills/handoff/EXAMPLES.md +5 -4
  300. package/plugin/skills/handoff/SKILL.md +10 -5
  301. package/plugin/skills/handoff/TROUBLESHOOTING.md +70 -0
  302. package/plugin/skills/lesson/SKILL.md +1 -1
  303. package/plugin/skills/lesson/TROUBLESHOOTING.md +70 -0
  304. package/plugin/skills/memory-discipline/SKILL.md +1 -1
  305. package/plugin/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  306. package/plugin/skills/recall/SKILL.md +18 -7
  307. package/plugin/skills/recall/TROUBLESHOOTING.md +70 -0
  308. package/plugin/skills/recap/EXAMPLES.md +5 -3
  309. package/plugin/skills/recap/SKILL.md +11 -6
  310. package/plugin/skills/recap/TROUBLESHOOTING.md +70 -0
  311. package/plugin/skills/remember/EXAMPLES.md +3 -3
  312. package/plugin/skills/remember/SKILL.md +1 -1
  313. package/plugin/skills/remember/TROUBLESHOOTING.md +70 -0
  314. package/plugin/skills/session-history/EXAMPLES.md +2 -2
  315. package/plugin/skills/session-history/SKILL.md +10 -4
  316. package/plugin/skills/session-history/TROUBLESHOOTING.md +70 -0
  317. package/plugin/skills/write-agentmemory-skill/SKILL.md +2 -1
  318. package/plugin/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  319. package/dist/connect-B4_ca6BD.mjs.map +0 -1
  320. package/dist/docker-compose.yml +0 -73
  321. package/dist/iii-config.docker.yaml +0 -53
  322. package/dist/iii-config.yaml +0 -63
  323. package/dist/keyed-mutex-DchXsyfP.mjs.map +0 -1
  324. package/dist/secret-store-BnY3A50C.mjs.map +0 -1
  325. package/dist/src-Jsq9LCeH.mjs.map +0 -1
  326. package/dist/standalone-DKCdrf1d.mjs.map +0 -1
  327. package/dist/status-IRz7ta8-.mjs +0 -2344
  328. package/dist/status-IRz7ta8-.mjs.map +0 -1
  329. package/docker-compose.yml +0 -73
  330. package/iii-config.docker.yaml +0 -53
  331. package/iii-config.yaml +0 -63
  332. package/plugin/scripts/post-commit.mjs +0 -184
  333. package/plugin/scripts/post-commit.mjs.map +0 -1
  334. /package/plugin/scripts/{post-commit.d.mts → cursor/run-detached.d.mts} +0 -0
package/dist/cli.mjs CHANGED
@@ -1,18 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { a as runAdapter, i as resolveAdapter, r as knownAgents, t as ADAPTERS } from "./connect-B4_ca6BD.mjs";
3
- import { o as generateId } from "./keyed-mutex-DchXsyfP.mjs";
4
- import { I as hydrateProcessEnvFromFile, M as getStateSaveIntervalMs, O as getRedisUrl, _ as VERSION, at as isBundledConfig, ct as resolveEngineCwd, dt as runtimeConfigPath, g as III_PINNED_VERSION, gt as renderEngineConfig, ht as engineStateConfigPaths, i as resolveClientSecret, it as engineChildEnv, j as getStateBackend, lt as resolveLaunchRenderFailure, m as getAllTools, mt as engineFlushWaitMs, nt as dockerComposeArgs, ot as legacyDataMigrations, p as ESSENTIAL_TOOLS, pt as clearPersistedBuiltinConfig, rt as dockerProjectName, st as redactCredentialUrls, t as bearerHeaders, tt as setBootVerbose, ut as rewriteBundledConfig } from "./secret-store-BnY3A50C.mjs";
5
- import { A as SHUTDOWN_HARD_EXIT_MS, C as spoolSummary, M as runtimeMetadataPath, O as resolveDataDir, _ as drainSpool, b as retainSent, t as CURL_AUTH_HEADER, v as parseSentMark } from "./status-IRz7ta8-.mjs";
2
+ import { a as runAdapter, i as resolveAdapter, r as knownAgents, t as ADAPTERS } from "./connect-BX3njQ3Y.mjs";
3
+ import { s as generateId } from "./keyed-mutex-DCUh7PG-.mjs";
4
+ import { A as setBootVerbose, B as detectLlmProviderKind, C as getAllTools, E as VERSION, F as runtimeConfigPath, I as LLM_PROVIDER_KEYS, M as engineFlushWaitMs, N as engineStateConfigPaths, P as redactCredentialUrls, S as ESSENTIAL_TOOLS, T as III_PINNED_VERSION, V as embeddingOptInHint, Z as getRedisUrl, et as getStateBackend, i as resolveClientSecret, it as hydrateProcessEnvFromFile, mt as llmOptInHint, t as bearerHeaders, tt as getStateSaveIntervalMs, xt as parseEnvContent } from "./secret-store-iYXRcrO8.mjs";
5
+ import { $t as resolveDataDir, Gt as retainSent, Ht as drainSpool, Jt as spoolSummary, Ut as parseSentMark, an as runtimeMetadataPath, nn as registerStartupCleanup, nt as CURL_AUTH_HEADER, rn as runStartupCleanup, t as parseCompressionScope, tn as SHUTDOWN_HARD_EXIT_MS } from "./compression-recovery-CZh63Jmz.mjs";
6
6
  import { execFileSync, spawn, spawnSync } from "node:child_process";
7
- import { appendFileSync, closeSync, constants, cpSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
8
- import { delimiter, dirname, join, resolve } from "node:path";
7
+ import { appendFileSync, chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
8
+ import { basename, delimiter, dirname, isAbsolute, join, resolve, win32 } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { homedir, platform } from "node:os";
11
11
  import * as p from "@clack/prompts";
12
12
  import pc from "picocolors";
13
- import { createHash } from "node:crypto";
13
+ import { createHash, randomBytes } from "node:crypto";
14
14
  import { copyFile, mkdir } from "node:fs/promises";
15
+ import { parse, stringify } from "yaml";
16
+ import { createConnection } from "node:net";
17
+ import { registerWorker } from "iii-sdk";
15
18
  //#region src/cli/doctor-diagnostics.ts
19
+ function canAutoFix(d, status) {
20
+ return !d.manualOnly && !status?.manualFix;
21
+ }
22
+ function doctorPromptsAllowed(env, stdinIsTTY, stdoutIsTTY) {
23
+ return Boolean(stdinIsTTY) && Boolean(stdoutIsTTY) && !env["CI"];
24
+ }
16
25
  /** Common placeholder values shipped in .env.example. */
17
26
  const PLACEHOLDER_VALUES = new Set([
18
27
  "",
@@ -31,20 +40,7 @@ const PROVIDER_KEY_NAMES = [
31
40
  "OPENROUTER_API_KEY",
32
41
  "MINIMAX_API_KEY"
33
42
  ];
34
- function parseEnvFile(content) {
35
- const out = {};
36
- for (const rawLine of content.split(/\r?\n/)) {
37
- const line = rawLine.trim();
38
- if (!line || line.startsWith("#")) continue;
39
- const eq = line.indexOf("=");
40
- if (eq < 0) continue;
41
- const key = line.slice(0, eq).trim();
42
- let value = line.slice(eq + 1).trim();
43
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
44
- out[key] = value;
45
- }
46
- return out;
47
- }
43
+ const parseEnvFile = parseEnvContent;
48
44
  /** Returns the list of provider keys that look real (non-placeholder). */
49
45
  function realProviderKeys(env) {
50
46
  return PROVIDER_KEY_NAMES.filter((k) => {
@@ -71,7 +67,7 @@ function buildDiagnostics(effects) {
71
67
  id: "env-missing",
72
68
  message: "~/.agentmemory/.env is missing.",
73
69
  fixPreview: "Copy .env.example into ~/.agentmemory/.env (your keys file).",
74
- moreInfo: "agentmemory reads provider API keys (Anthropic, OpenAI, Gemini, …) from ~/.agentmemory/.env. Without this file the daemon falls back to BM25-only search and no LLM-backed enrichment runs.",
70
+ moreInfo: "agentmemory reads provider API keys (Anthropic, OpenAI, Gemini, …) from ~/.agentmemory/.env. Without this file, and without a provider key exported in the environment, the daemon falls back to BM25-only search and no LLM-backed enrichment runs.",
75
71
  check: async () => ({
76
72
  ok: effects.envFileExists(),
77
73
  detail: effects.envFileExists() ? void 0 : "no env file"
@@ -80,44 +76,97 @@ function buildDiagnostics(effects) {
80
76
  },
81
77
  {
82
78
  id: "no-llm-provider-key",
83
- message: "No LLM provider API key found in ~/.agentmemory/.env.",
84
- fixPreview: "Open ~/.agentmemory/.env in $EDITOR and paste your key, then re-check.",
85
- moreInfo: "Set at least one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY. The daemon picks the first that resolves to a real (non-placeholder) value at startup.",
79
+ message: "No LLM provider chosen for agentmemory in ~/.agentmemory/.env or the environment.",
80
+ fixPreview: "Open ~/.agentmemory/.env in $EDITOR, set AGENTMEMORY_LLM_PROVIDER and that provider's key, then re-check.",
81
+ moreInfo: "agentmemory sends session content to an LLM provider only after you choose one: set AGENTMEMORY_LLM_PROVIDER to anthropic, openai, gemini, openrouter or minimax, and set that provider's key (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY or GOOGLE_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY). A key alone never turns LLM use on. Placeholder values count as missing. AGENTMEMORY_ALLOW_AGENT_SDK=true needs no key. A value exported in the environment wins over the file, but only for a daemon started from that environment.",
86
82
  check: async () => {
87
- if (!effects.envFileExists()) return {
88
- ok: false,
89
- detail: "env file missing (run env-missing fix first)"
83
+ const file = effects.readEnvFile();
84
+ const env = { ...file };
85
+ for (const [k, v] of Object.entries(effects.processEnv())) if (v !== void 0) env[k] = v;
86
+ for (const k of placeholderProviderKeys(env)) delete env[k];
87
+ if (detectLlmProviderKind(env) !== "llm") {
88
+ const hint = llmOptInHint(env);
89
+ if (hint) return {
90
+ ok: false,
91
+ detail: hint
92
+ };
93
+ return {
94
+ ok: false,
95
+ detail: effects.envFileExists() ? "no provider chosen" : "no provider chosen (run env-missing fix first)"
96
+ };
97
+ }
98
+ const selected = (env.AGENTMEMORY_LLM_PROVIDER ?? "").trim().toLowerCase();
99
+ const selectedKeys = selected in LLM_PROVIDER_KEYS ? LLM_PROVIDER_KEYS[selected] : [];
100
+ const real = realProviderKeys(env).filter((k) => selectedKeys.includes(k));
101
+ const fromEnv = real.filter((k) => file[k] !== env[k]);
102
+ const named = real.map((k) => fromEnv.includes(k) ? `${k} (environment)` : k);
103
+ if (named.length === 0) return {
104
+ ok: true,
105
+ detail: "agent-sdk fallback, no API key"
90
106
  };
91
- const real = realProviderKeys(effects.readEnvFile());
107
+ const caveat = fromEnv.length > 0 ? "; a daemon started elsewhere will not see it" : "";
92
108
  return {
93
- ok: real.length > 0,
94
- detail: real.length > 0 ? `found: ${real.join(", ")}` : "no provider key set"
109
+ ok: true,
110
+ detail: `found: ${named.join(", ")}${caveat}`
95
111
  };
96
112
  },
97
113
  fix: (ctx) => effects.openEditor(ctx.envPath)
98
114
  },
115
+ {
116
+ id: "embedding-provider-not-chosen",
117
+ message: "Cloud embeddings are not set up: EMBEDDING_PROVIDER does not choose a present key, or names a provider whose key is missing.",
118
+ fixPreview: "Open ~/.agentmemory/.env in $EDITOR and set EMBEDDING_PROVIDER (local, openai, voyage, cohere, gemini or openrouter).",
119
+ moreInfo: "Cloud embeddings send memory text to that provider, so agentmemory uses them only when EMBEDDING_PROVIDER names the provider. A key alone never turns them on. EMBEDDING_PROVIDER=local runs on-device. Without a provider, search is keyword and graph only.",
120
+ check: async () => {
121
+ const env = { ...effects.readEnvFile() };
122
+ for (const [k, v] of Object.entries(effects.processEnv())) if (v !== void 0) env[k] = v;
123
+ const hint = embeddingOptInHint(env);
124
+ return hint ? {
125
+ ok: false,
126
+ detail: hint
127
+ } : { ok: true };
128
+ },
129
+ fix: (ctx) => effects.openEditor(ctx.envPath)
130
+ },
99
131
  {
100
132
  id: "engine-version-mismatch",
101
- message: "iii binary on PATH doesn't match the version agentmemory pins to.",
102
- fixPreview: "Re-run the iii installer for the pinned version and restart the engine.",
103
- moreInfo: "agentmemory pins the iii engine to a specific release because newer engines use a different worker model. Running a mismatched binary surfaces as EPIPE reconnect loops and empty search results.",
133
+ message: "No iii binary matches the version agentmemory pins to.",
134
+ fixPreview: "Reinstall the pinned iii-engine and restart the managed Worker Compose project.",
135
+ moreInfo: "agentmemory manages its pinned iii engine and workers through Worker Compose; a foreign process on PATH is not the instance binary. Running a mismatched binary surfaces as EPIPE reconnect loops and empty search results. Before an instance is configured, a matching private install at ~/.agentmemory/bin/iii can satisfy the pin.",
104
136
  check: async (ctx) => {
137
+ if (await effects.externalEngineRunning()) return {
138
+ ok: false,
139
+ detail: "external engine version is unknown",
140
+ manualFix: "Verify the external engine version, or stop it before starting a managed instance."
141
+ };
142
+ const privateVersion = effects.iiiBinaryVersion(effects.localBinIiiPath());
105
143
  const bin = effects.findIiiBinary();
144
+ const pathVersion = bin ? effects.iiiBinaryVersion(bin) : null;
145
+ if (privateVersion === ctx.pinnedVersion) {
146
+ const note = pathVersion && pathVersion !== ctx.pinnedVersion ? `; PATH iii is ${pathVersion}; the pinned private install is available for startup` : "";
147
+ return {
148
+ ok: true,
149
+ detail: `private install ${privateVersion} (pinned ${ctx.pinnedVersion})${note}`
150
+ };
151
+ }
106
152
  if (!bin) return {
107
153
  ok: false,
108
- detail: "iii not on PATH"
154
+ detail: "iii not on PATH and no private install"
109
155
  };
110
- const v = effects.iiiBinaryVersion(bin);
111
- if (!v) return {
156
+ if (!pathVersion) return {
112
157
  ok: false,
113
158
  detail: "iii on PATH but --version failed"
114
159
  };
115
160
  return {
116
- ok: v === ctx.pinnedVersion,
117
- detail: `${v} (pinned ${ctx.pinnedVersion})`
161
+ ok: pathVersion === ctx.pinnedVersion,
162
+ detail: `${pathVersion} (pinned ${ctx.pinnedVersion})`
118
163
  };
119
164
  },
120
165
  fix: async () => {
166
+ if (await effects.externalEngineRunning()) return {
167
+ ok: false,
168
+ message: "Verify or stop the external engine manually."
169
+ };
121
170
  const r = await effects.runIiiInstaller();
122
171
  if (!r.ok) return r;
123
172
  await effects.runStop();
@@ -142,8 +191,8 @@ function buildDiagnostics(effects) {
142
191
  {
143
192
  id: "stale-pidfile",
144
193
  message: "Stale pidfile: pid recorded but the process is gone.",
145
- fixPreview: "Clear ~/.agentmemory/iii.pid + engine-state.json, then restart.",
146
- moreInfo: "When the engine crashes hard (kill -9, OOM, host reboot) the pidfile sticks around. agentmemory refuses to start a second engine on top of a stale pid, so this state must be cleared explicitly.",
194
+ fixPreview: "Verify the Compose daemon ownership record and restart only the managed project.",
195
+ moreInfo: "When a managed Compose daemon crashes hard (kill -9, OOM, host reboot), its ownership PID can stick around. The check uses the saved Compose process, never a legacy iii.pid from a previous lifecycle.",
147
196
  check: async () => {
148
197
  if (!effects.pidfileExists()) return {
149
198
  ok: true,
@@ -184,20 +233,25 @@ function buildDiagnostics(effects) {
184
233
  },
185
234
  {
186
235
  id: "iii-on-path-not-local-bin",
187
- message: "iii is on PATH but not at agentmemory's private install path.",
236
+ message: "iii is on PATH but agentmemory's private install is absent or mismatched.",
188
237
  fixPreview: "Install the pinned version to ~/.agentmemory/bin — won't touch your PATH.",
189
238
  moreInfo: "agentmemory installs its pinned engine to ~/.agentmemory/bin/iii so a user-managed iii on PATH (homebrew, cargo, manual install) stays untouched. When agentmemory needs the pin and PATH doesn't have it, it falls back to the private install. If neither exists, run the installer.",
190
239
  manualOnly: true,
191
- check: async () => {
240
+ check: async (ctx) => {
192
241
  const bin = effects.findIiiBinary();
193
242
  if (!bin) return {
194
243
  ok: true,
195
244
  detail: "iii not on PATH (handled elsewhere)"
196
245
  };
197
246
  const localBin = effects.localBinIiiPath();
247
+ if (bin === localBin) return { ok: true };
248
+ if (effects.iiiBinaryVersion(localBin) === ctx.pinnedVersion) return {
249
+ ok: true,
250
+ detail: `private install pinned; PATH iii at ${bin} stays untouched`
251
+ };
198
252
  return {
199
- ok: bin === localBin,
200
- detail: bin === localBin ? void 0 : `iii at: ${bin}`
253
+ ok: false,
254
+ detail: `iii at: ${bin}`
201
255
  };
202
256
  },
203
257
  fix: async () => effects.runIiiInstaller().then((r) => ({
@@ -218,7 +272,7 @@ function dryRunPlan(ctx, results) {
218
272
  if (status.ok) continue;
219
273
  n++;
220
274
  lines.push(`${n}. [${diagnostic.id}] ${diagnostic.message}`);
221
- lines.push(` would fix: ${diagnostic.fixPreview}`);
275
+ lines.push(` ${canAutoFix(diagnostic, status) ? "would fix" : "manual fix"}: ${status.manualFix ?? diagnostic.fixPreview}`);
222
276
  if (status.detail) lines.push(` detail: ${status.detail}`);
223
277
  }
224
278
  if (lines.length === 0) lines.push(`All checks passing for ${ctx.baseUrl} — no fixes to run.`);
@@ -400,15 +454,22 @@ function portFlagSuffix(restPort, instance) {
400
454
  if (instance > 0 && restPort === DEFAULT_REST_PORT + instance * 100) return ` --instance ${instance}`;
401
455
  return restPort === DEFAULT_REST_PORT ? "" : ` --port ${restPort}`;
402
456
  }
457
+ //#endregion
458
+ //#region src/cli/embedding-status.ts
459
+ function embeddingStatusLabel(provider) {
460
+ if (typeof provider !== "string") return null;
461
+ const value = provider.trim();
462
+ return value && value !== "none" ? value : null;
463
+ }
403
464
  const III_INSTALL_SPAWN_TIMEOUT_MS = 345 * 1e3;
404
- const III_RELEASE_SHA256 = { "0.22.1": {
405
- "iii-aarch64-apple-darwin.tar.gz": "2b309019b909a896cae874dc947e2cdf877b4f3c51dd026b79850af858517fa4",
406
- "iii-x86_64-apple-darwin.tar.gz": "6d33940db2d3ad6a9aef9837aa5b89bf5c5e4acd0679e8178fec258af229d7e1",
407
- "iii-x86_64-unknown-linux-gnu.tar.gz": "34d4bd2ec1873e6114bb836be7db9cb3f410d0d63353d240917f0bc9c30df13b",
408
- "iii-aarch64-unknown-linux-gnu.tar.gz": "87efc60210c1580ef2e11b13c679a4dad91917a9d80f737c5ba7e3475b3ca413",
409
- "iii-armv7-unknown-linux-gnueabihf.tar.gz": "af5dfc45149678c647e5adaca55c055428eebfa73d65884b052c30c98abcfd19",
410
- "iii-x86_64-pc-windows-msvc.zip": "d862bf7cf1e864f3290c8a6fc17c308c6ecac097cb2666e4a739efc0040aae96",
411
- "iii-aarch64-pc-windows-msvc.zip": "4777a48cee3fc1a742daee929e3d943df09e4ddd3357d27a33d31c9dc9e520ac"
465
+ const III_RELEASE_SHA256 = { "0.25.0": {
466
+ "iii-aarch64-apple-darwin.tar.gz": "5f23087a41ce1cf86f9859d2f430d1809a1e048094e69b1df0cdf1ebefc2a985",
467
+ "iii-x86_64-apple-darwin.tar.gz": "e2ac4ef109ddbf459b4683171c6b5b9c8366b16dac2e1f8c4fe08b4a4e19d49a",
468
+ "iii-x86_64-unknown-linux-gnu.tar.gz": "4c4d14063eb55d79663db553e960497731420df6d78eca3aafe670e2fbaba8e2",
469
+ "iii-aarch64-unknown-linux-gnu.tar.gz": "5e52d4738a0beb609f4d32970d37be7c00d4848ee9d06300248498a00ea4bf0e",
470
+ "iii-armv7-unknown-linux-gnueabihf.tar.gz": "9ee840351ff62c889b929b618f8cbb2d66d616cecf82cde7c26d4618aa137508",
471
+ "iii-x86_64-pc-windows-msvc.zip": "b80ea5c3408879380b6ce95f7b2855ce161d68642aeec8f7b54f104794cbb322",
472
+ "iii-aarch64-pc-windows-msvc.zip": "2b1e222b90ee18ed143186e8b307d8ad2076f44ea00fa7a7d071bd3c87cde002"
412
473
  } };
413
474
  function expectedIiiSha256(version, asset, checksums = III_RELEASE_SHA256) {
414
475
  return checksums[version]?.[asset] ?? null;
@@ -500,6 +561,251 @@ function installIiiArchive(opts) {
500
561
  };
501
562
  return { ok: true };
502
563
  }
564
+ function iiiZipArchivePath(binDir) {
565
+ return join(binDir, `.iii-download-${process.pid}-${randomBytes(4).toString("hex")}.zip`);
566
+ }
567
+ function removeQuietly(path) {
568
+ try {
569
+ rmSync(path, {
570
+ force: true,
571
+ recursive: true
572
+ });
573
+ } catch {}
574
+ }
575
+ function removeStaleInstallFiles(binDir, now = Date.now()) {
576
+ const removed = [];
577
+ let entries;
578
+ try {
579
+ entries = readdirSync(binDir);
580
+ } catch {
581
+ return removed;
582
+ }
583
+ for (const name of entries) {
584
+ if (!name.startsWith(".iii-download-") && !name.startsWith(".iii-staging-")) continue;
585
+ const path = join(binDir, name);
586
+ try {
587
+ if (now - statSync(path).mtimeMs < 9e5) continue;
588
+ } catch {
589
+ continue;
590
+ }
591
+ removeQuietly(path);
592
+ if (!existsSync(path)) removed.push(name);
593
+ }
594
+ return removed;
595
+ }
596
+ const PROXY_ENV_KEYS = [
597
+ "HTTPS_PROXY",
598
+ "https_proxy",
599
+ "HTTP_PROXY",
600
+ "http_proxy",
601
+ "ALL_PROXY",
602
+ "all_proxy"
603
+ ];
604
+ function proxyConfigured(env) {
605
+ return PROXY_ENV_KEYS.some((key) => Boolean(env[key]?.trim()));
606
+ }
607
+ function windowsCurlDownloadCommand(url, dest, env = process.env) {
608
+ return {
609
+ command: join(env["SystemRoot"] || env["SYSTEMROOT"] || "C:\\Windows", "System32", "curl.exe"),
610
+ args: [
611
+ "-fsSL",
612
+ "--connect-timeout",
613
+ String(15),
614
+ "--max-time",
615
+ String(300),
616
+ "-o",
617
+ dest,
618
+ url
619
+ ]
620
+ };
621
+ }
622
+ function powershellLiteral(value) {
623
+ return `'${value.replace(/'/g, "''")}'`;
624
+ }
625
+ function windowsZipExtractCommands(archive, binDir, env = process.env) {
626
+ const system32 = join(env["SystemRoot"] || env["SYSTEMROOT"] || "C:\\Windows", "System32");
627
+ return [{
628
+ command: join(system32, "tar.exe"),
629
+ args: [
630
+ "-xf",
631
+ archive,
632
+ "-C",
633
+ binDir
634
+ ]
635
+ }, {
636
+ command: join(system32, "WindowsPowerShell", "v1.0", "powershell.exe"),
637
+ args: [
638
+ "-NoProfile",
639
+ "-NonInteractive",
640
+ "-Command",
641
+ `Expand-Archive -LiteralPath ${powershellLiteral(archive)} -DestinationPath ${powershellLiteral(binDir)} -Force`
642
+ ]
643
+ }];
644
+ }
645
+ async function downloadToFile(url, dest, env = process.env, run = runInstallCommand) {
646
+ if (proxyConfigured(env)) {
647
+ const step = windowsCurlDownloadCommand(url, dest, env);
648
+ const result = await run(step.command, step.args);
649
+ if (result.status !== 0) throw new Error(describeInstallFailure(result));
650
+ return;
651
+ }
652
+ const res = await fetch(url, {
653
+ redirect: "follow",
654
+ signal: AbortSignal.timeout(300 * 1e3)
655
+ });
656
+ if (!res.ok) throw new Error(`download failed with HTTP ${res.status} ${res.statusText}`.trim());
657
+ writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
658
+ }
659
+ function runInstallCommand(command, args) {
660
+ return new Promise((done) => {
661
+ let stdout = "";
662
+ let stderr = "";
663
+ let timedOut = false;
664
+ const child = spawn(command, args, {
665
+ stdio: [
666
+ "ignore",
667
+ "pipe",
668
+ "pipe"
669
+ ],
670
+ windowsHide: true,
671
+ shell: false
672
+ });
673
+ const killOnExit = () => {
674
+ try {
675
+ child.kill();
676
+ } catch {}
677
+ };
678
+ process.once("exit", killOnExit);
679
+ const timer = setTimeout(() => {
680
+ timedOut = true;
681
+ child.kill();
682
+ }, III_INSTALL_SPAWN_TIMEOUT_MS);
683
+ child.stdout?.on("data", (chunk) => {
684
+ stdout += chunk;
685
+ });
686
+ child.stderr?.on("data", (chunk) => {
687
+ stderr += chunk;
688
+ });
689
+ let spawnError;
690
+ child.on("error", (error) => {
691
+ spawnError = error;
692
+ if (child.pid !== void 0) return;
693
+ clearTimeout(timer);
694
+ process.off("exit", killOnExit);
695
+ done({
696
+ status: null,
697
+ stdout,
698
+ stderr,
699
+ error
700
+ });
701
+ });
702
+ child.on("close", (status, signal) => {
703
+ clearTimeout(timer);
704
+ process.off("exit", killOnExit);
705
+ if (spawnError && status === null && !timedOut) {
706
+ done({
707
+ status: null,
708
+ stdout,
709
+ stderr,
710
+ signal,
711
+ error: spawnError
712
+ });
713
+ return;
714
+ }
715
+ if (timedOut) {
716
+ done({
717
+ status: null,
718
+ stdout,
719
+ stderr,
720
+ signal,
721
+ error: Object.assign(/* @__PURE__ */ new Error("timed out"), { code: "ETIMEDOUT" })
722
+ });
723
+ return;
724
+ }
725
+ done({
726
+ status,
727
+ stdout,
728
+ stderr,
729
+ signal
730
+ });
731
+ });
732
+ });
733
+ }
734
+ async function installIiiZip(opts) {
735
+ const expected = expectedIiiSha256(opts.version, opts.asset, opts.checksums);
736
+ if (!expected) return {
737
+ ok: false,
738
+ reason: "no-checksum",
739
+ detail: `no pinned SHA-256 for ${opts.asset} in iii v${opts.version}, so the download cannot be verified`
740
+ };
741
+ const run = opts.run ?? runInstallCommand;
742
+ const download = opts.download ?? ((url, dest) => downloadToFile(url, dest, opts.env ?? process.env, run));
743
+ const archive = iiiZipArchivePath(opts.binDir);
744
+ try {
745
+ mkdirSync(opts.binDir, { recursive: true });
746
+ removeStaleInstallFiles(opts.binDir);
747
+ await download(opts.releaseUrl, archive);
748
+ } catch (err) {
749
+ removeQuietly(archive);
750
+ return {
751
+ ok: false,
752
+ reason: "download",
753
+ detail: err instanceof Error ? err.message : String(err)
754
+ };
755
+ }
756
+ let actual;
757
+ try {
758
+ actual = sha256File(archive);
759
+ } catch (err) {
760
+ removeQuietly(archive);
761
+ return {
762
+ ok: false,
763
+ reason: "download",
764
+ detail: err instanceof Error ? err.message : String(err)
765
+ };
766
+ }
767
+ if (actual !== expected) {
768
+ removeQuietly(archive);
769
+ return {
770
+ ok: false,
771
+ reason: "checksum",
772
+ detail: `SHA-256 mismatch for ${opts.asset}: expected ${expected}, got ${actual}. The download was deleted and nothing was installed`
773
+ };
774
+ }
775
+ const staging = join(opts.binDir, `.iii-staging-${process.pid}-${randomBytes(4).toString("hex")}`);
776
+ const staged = join(staging, basename(opts.binPath));
777
+ const failures = [];
778
+ try {
779
+ for (const step of windowsZipExtractCommands(archive, staging, opts.env)) {
780
+ removeQuietly(staging);
781
+ mkdirSync(staging, { recursive: true });
782
+ const result = await run(step.command, step.args);
783
+ if (result.status === 0 && existsSync(staged)) try {
784
+ renameSync(staged, opts.binPath);
785
+ return { ok: true };
786
+ } catch (err) {
787
+ failures.push(`could not move ${staged} to ${opts.binPath}: ${err instanceof Error ? err.message : String(err)}`);
788
+ break;
789
+ }
790
+ failures.push(`${step.command}: ${result.status === 0 ? `finished but ${basename(opts.binPath)} is missing from the archive` : describeInstallFailure(result)}`);
791
+ }
792
+ return {
793
+ ok: false,
794
+ reason: "extract",
795
+ detail: failures.join("; ")
796
+ };
797
+ } catch (err) {
798
+ failures.push(err instanceof Error ? err.message : String(err));
799
+ return {
800
+ ok: false,
801
+ reason: "extract",
802
+ detail: failures.join("; ")
803
+ };
804
+ } finally {
805
+ removeQuietly(staging);
806
+ removeQuietly(archive);
807
+ }
808
+ }
503
809
  //#endregion
504
810
  //#region src/cli/startup-stderr.ts
505
811
  function createStartupStderrCapture(maxBytes = 16 * 1024) {
@@ -518,66 +824,147 @@ function createStartupStderrCapture(maxBytes = 16 * 1024) {
518
824
  };
519
825
  }
520
826
  //#endregion
521
- //#region src/cli/console.ts
522
- const GLOBAL_VALUE_FLAGS = new Set([
523
- "--port",
524
- "--instance",
525
- "--data-dir",
526
- "--tools"
527
- ]);
528
- const GLOBAL_BOOLEAN_FLAGS = new Set([
529
- "--verbose",
530
- "-v",
531
- "--no-engine"
532
- ]);
533
- function defaultConsolePort(viewerPort) {
534
- return viewerPort + 1;
535
- }
536
- function readPort(raw, flag) {
537
- const port = raw === void 0 ? NaN : Number(raw);
538
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${flag} needs a port between 1 and 65535`);
539
- return port;
827
+ //#region src/cli/readiness.ts
828
+ const DEFAULT_ENGINE_TIMEOUT_MS = 12e4;
829
+ const DEFAULT_WORKER_TIMEOUT_MS = 3e5;
830
+ const MAX_STARTUP_TIMEOUT_MS = 36e5;
831
+ function startupTimeouts(env = process.env) {
832
+ const parse = (key, fallback) => {
833
+ const raw = env[key];
834
+ if (raw === void 0) return fallback;
835
+ const value = Number(raw);
836
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1 || value > 36e5) throw new Error(`${key} must be an integer between 1 and ${MAX_STARTUP_TIMEOUT_MS} milliseconds.`);
837
+ return value;
838
+ };
839
+ const ownership = env.AGENTMEMORY_ENGINE_MANAGES_WORKER;
840
+ if (ownership !== void 0 && ownership !== "true" && ownership !== "false") throw new Error("AGENTMEMORY_ENGINE_MANAGES_WORKER must be true or false.");
841
+ return {
842
+ engineMs: parse("AGENTMEMORY_ENGINE_READY_TIMEOUT_MS", DEFAULT_ENGINE_TIMEOUT_MS),
843
+ workerMs: parse("AGENTMEMORY_READY_TIMEOUT_MS", DEFAULT_WORKER_TIMEOUT_MS),
844
+ engineManagesWorker: ownership === void 0 ? void 0 : ownership === "true"
845
+ };
540
846
  }
541
- function parseConsoleArgs(tail, fallbackPort) {
542
- let consolePort = fallbackPort;
543
- const extraArgs = [];
544
- for (let i = 0; i < tail.length; i++) {
545
- const arg = tail[i];
546
- if (arg === "--console-port") {
547
- consolePort = readPort(tail[i + 1], arg);
548
- i++;
549
- continue;
847
+ function configStartsAgentmemoryWorker(raw, workerEntry, engineCwd) {
848
+ const lines = raw.split(/\r?\n/);
849
+ let workerIndent;
850
+ let execIndent;
851
+ for (const line of lines) {
852
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
853
+ const indent = line.length - line.trimStart().length;
854
+ if (workerIndent !== void 0 && indent <= workerIndent) {
855
+ workerIndent = void 0;
856
+ execIndent = void 0;
550
857
  }
551
- if (arg.startsWith("--console-port=")) {
552
- consolePort = readPort(arg.slice(15), "--console-port");
858
+ if (/^\s*-\s+name:\s*(?:iii-exec|"iii-exec"|'iii-exec')(?:\s+#.*)?\s*$/.test(line)) {
859
+ workerIndent = indent;
553
860
  continue;
554
861
  }
555
- if (GLOBAL_VALUE_FLAGS.has(arg)) {
556
- i++;
862
+ if (workerIndent === void 0) continue;
863
+ if (execIndent !== void 0 && indent <= execIndent) execIndent = void 0;
864
+ if (/^\s*exec:\s*(?:#.*)?$/.test(line)) {
865
+ execIndent = indent;
557
866
  continue;
558
867
  }
559
- if ([...GLOBAL_VALUE_FLAGS].some((flag) => arg.startsWith(`${flag}=`))) continue;
560
- if (GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
561
- extraArgs.push(arg);
868
+ if (execIndent === void 0) continue;
869
+ const scalar = line.match(/^\s*-\s+(.+)$/)?.[1]?.trim();
870
+ if (!scalar) continue;
871
+ const quoted = scalar.match(/^(["'])(.*?)\1(?:\s+#.*)?$/);
872
+ const tokens = (quoted ? quoted[2] : scalar).match(/"[^"]*"|'[^']*'|[^\s]+/g)?.map((token) => token.replace(/^("|')(.*)\1$/, "$2").replaceAll("\\", "/"));
873
+ if (!tokens) continue;
874
+ const comment = tokens.findIndex((token) => token.startsWith("#"));
875
+ if (comment !== -1) tokens.splice(comment);
876
+ const runtime = tokens[0]?.split("/").at(-1);
877
+ if (runtime !== "node" && runtime !== "node.exe" && runtime !== "tsx") continue;
878
+ if (runtime !== "tsx") while (tokens[1] && /^(?:--max[-_]old[-_]space[-_]size=\d+|--enable-source-maps|--no-warnings)$/.test(tokens[1])) tokens.splice(1, 1);
879
+ const entry = tokens.length === 2 ? tokens[1] : tokens.length === 4 && tokens[1] === "--import" && tokens[2] === "tsx" ? tokens[3] : void 0;
880
+ if (entry === workerEntry.replaceAll("\\", "/")) return true;
881
+ if (entry === "dist/index.mjs" || entry === "./dist/index.mjs" || entry === "src/index.ts" || entry === "./src/index.ts") {
882
+ if (engineCwd === void 0) return true;
883
+ try {
884
+ if (statSync(join(engineCwd, entry)).isFile()) return true;
885
+ } catch {}
886
+ }
562
887
  }
563
- return {
564
- consolePort,
565
- extraArgs
566
- };
888
+ return false;
567
889
  }
568
- function consoleArgs(launch) {
569
- return [
570
- "console",
571
- "--port",
572
- String(launch.consolePort),
573
- "--engine-port",
574
- String(launch.restPort),
575
- "--ws-port",
576
- String(launch.streamPort),
577
- "--bridge-port",
578
- String(launch.enginePort),
579
- ...launch.extraArgs
580
- ];
890
+ function startupTimeoutMessage(kind, timeoutMs) {
891
+ const name = kind === "engine" ? "iii-engine" : "agentmemory worker";
892
+ const variable = kind === "engine" ? "AGENTMEMORY_ENGINE_READY_TIMEOUT_MS" : "AGENTMEMORY_READY_TIMEOUT_MS";
893
+ return `${name} did not become ready within ${timeoutMs / 1e3}s. Set ${variable} to allow more startup time.`;
894
+ }
895
+ function startupDeadline(timeoutMs) {
896
+ const deadline = performance.now() + timeoutMs;
897
+ return { remaining: () => Math.max(0, deadline - performance.now()) };
898
+ }
899
+ /** Probe the loopback REST listener owned by this managed Compose instance.
900
+ * Client commands may honor AGENTMEMORY_URL, but startup readiness must always
901
+ * target the endpoint that this process asked Compose to create. */
902
+ function managedComposeReadinessUrl(restPort) {
903
+ if (!Number.isInteger(restPort) || restPort < 1 || restPort > 65535) throw new Error(`Invalid managed Compose REST port: ${restPort}`);
904
+ return `http://127.0.0.1:${restPort}`;
905
+ }
906
+ async function withinStartupDeadline(deadline, work, maximumMs = Infinity) {
907
+ const remaining = Math.min(deadline.remaining(), maximumMs);
908
+ if (remaining <= 0) return { completed: false };
909
+ const controller = new AbortController();
910
+ let timer;
911
+ const expired = new Promise((resolve) => {
912
+ timer = setTimeout(() => {
913
+ controller.abort();
914
+ resolve({ completed: false });
915
+ }, Math.ceil(remaining));
916
+ });
917
+ try {
918
+ const result = await Promise.race([Promise.resolve().then(() => work(controller.signal)).then((value) => ({
919
+ completed: true,
920
+ value
921
+ })), expired]);
922
+ return deadline.remaining() > 0 ? result : { completed: false };
923
+ } finally {
924
+ clearTimeout(timer);
925
+ controller.abort();
926
+ }
927
+ }
928
+ async function waitForReadiness(deadline, probe, options = {}) {
929
+ while (deadline.remaining() > 0 && !options.stopped?.()) {
930
+ const result = await withinStartupDeadline(deadline, probe, options.probeTimeoutMs ?? 1e3).catch(() => ({ completed: false }));
931
+ if (options.stopped?.()) return false;
932
+ if (result.completed && result.value) return true;
933
+ const remaining = deadline.remaining();
934
+ if (remaining <= 0) break;
935
+ const pause = Math.min(options.intervalMs ?? 250, remaining);
936
+ if (pause > 0) await new Promise((resolve) => setTimeout(resolve, pause));
937
+ }
938
+ return false;
939
+ }
940
+ async function probeEngine(base, signal) {
941
+ if (signal.aborted) return false;
942
+ (await fetch(`${base}/`, { signal })).body?.cancel().catch(() => {});
943
+ return true;
944
+ }
945
+ async function probeWorker(base, signal, onViewerPort) {
946
+ if (signal.aborted) return false;
947
+ const response = await fetch(`${base}/agentmemory/livez`, { signal });
948
+ if (!response.ok) {
949
+ response.body?.cancel().catch(() => {});
950
+ return false;
951
+ }
952
+ const data = await response.json();
953
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
954
+ const { viewerPort, viewerSkipped } = data;
955
+ if (typeof viewerPort === "number" && Number.isInteger(viewerPort) && viewerPort > 0 && viewerPort <= 65535) {
956
+ onViewerPort?.(viewerPort);
957
+ return true;
958
+ }
959
+ return viewerSkipped === true;
960
+ }
961
+ async function startWorkerAndWait(options) {
962
+ const deadline = startupDeadline(options.timeoutMs);
963
+ if (options.stopped?.()) return false;
964
+ if (!options.hasLiveWorker() && !options.engineStartsWorker()) {
965
+ if (!(await withinStartupDeadline(deadline, () => options.startWorker())).completed) return false;
966
+ }
967
+ return waitForReadiness(deadline, options.probe, { stopped: options.stopped });
581
968
  }
582
969
  //#endregion
583
970
  //#region src/cli/process-state.ts
@@ -585,6 +972,70 @@ function processStatIsRunning(stat) {
585
972
  return !stat.trimStart().startsWith("Z");
586
973
  }
587
974
  //#endregion
975
+ //#region src/cli/compose-process.ts
976
+ /**
977
+ * On macOS, ask lsof for the process's text executable and accept only a
978
+ * response explicitly associated with this PID. The `-d txt` filter limits
979
+ * names to text descriptors (the process executable).
980
+ */
981
+ function readMacProcessExecutable(pid) {
982
+ if (!Number.isSafeInteger(pid) || pid <= 0) return "";
983
+ try {
984
+ const output = execFileSync("lsof", [
985
+ "-a",
986
+ "-p",
987
+ String(pid),
988
+ "-d",
989
+ "txt",
990
+ "-Fpn"
991
+ ], {
992
+ encoding: "utf8",
993
+ stdio: [
994
+ "ignore",
995
+ "pipe",
996
+ "ignore"
997
+ ]
998
+ });
999
+ let matchedPid = false;
1000
+ for (const line of output.split(/\r?\n/)) {
1001
+ if (line.startsWith("p")) {
1002
+ if (line !== `p${pid}`) return "";
1003
+ matchedPid = true;
1004
+ continue;
1005
+ }
1006
+ if (matchedPid && line.startsWith("n")) {
1007
+ const path = line.slice(1);
1008
+ if (isAbsolute(path)) return path;
1009
+ }
1010
+ }
1011
+ } catch {}
1012
+ return "";
1013
+ }
1014
+ function sameResolvedExecutable(actual, expected, windows = false) {
1015
+ try {
1016
+ const actualPath = realpathSync(actual);
1017
+ const expectedPath = realpathSync(expected);
1018
+ return windows ? actualPath.toLowerCase() === expectedPath.toLowerCase() : actualPath === expectedPath;
1019
+ } catch {
1020
+ return false;
1021
+ }
1022
+ }
1023
+ function classifyComposeProcess(inspected, expected, windows = false) {
1024
+ if (!inspected || !inspected.command) return "unknown";
1025
+ if (!(inspected.command.includes("compose --namespace") && inspected.command.includes(expected.namespace) && inspected.command.includes(expected.file))) return "foreign";
1026
+ if (inspected.executable && sameResolvedExecutable(inspected.executable, expected.binPath, windows)) return "match";
1027
+ return "unknown";
1028
+ }
1029
+ function readUnixProcessExecutable(pid, os) {
1030
+ if (!Number.isSafeInteger(pid) || pid <= 0) return "";
1031
+ if (os === "darwin") return readMacProcessExecutable(pid);
1032
+ try {
1033
+ return readlinkSync(`/proc/${pid}/exe`);
1034
+ } catch {
1035
+ return "";
1036
+ }
1037
+ }
1038
+ //#endregion
588
1039
  //#region src/cli/splash.ts
589
1040
  const IS_COLOR_TTY = !!process.stdout.isTTY && !process.env["NO_COLOR"];
590
1041
  function accent(s) {
@@ -852,6 +1303,7 @@ async function runOnboarding() {
852
1303
  if (hint) p.log.info(hint);
853
1304
  }
854
1305
  const envPath = await seedEnvFile(provider);
1306
+ const providerWritten = setLlmProviderInEnv(envPath, provider);
855
1307
  await maybePromptContextInjection(envPath);
856
1308
  writePrefs({
857
1309
  lastAgent: agents[0] ?? null,
@@ -865,8 +1317,9 @@ async function runOnboarding() {
865
1317
  else lines.push(`! Could not write ~/.agentmemory/.env — run \`agentmemory init\` after this completes.`);
866
1318
  if (provider) {
867
1319
  const envKey = PROVIDERS.find((x) => x.value === provider)?.envKey;
868
- if (envKey) lines.push(` Uncomment ${envKey}= in that file to enable ${provider}.`);
869
- } else lines.push(" No provider chosen — agentmemory will run in BM25-only mode.");
1320
+ if (envKey) lines.push(providerWritten ? ` Set AGENTMEMORY_LLM_PROVIDER=${provider}. Add ${envKey}= to that file to enable ${provider}.` : ` Add AGENTMEMORY_LLM_PROVIDER=${provider} and ${envKey}= to that file to enable ${provider}.`);
1321
+ } else if (providerWritten) lines.push(" No LLM provider chosen. LLM summaries and compression stay off.");
1322
+ else lines.push(` ! Could not remove AGENTMEMORY_LLM_PROVIDER from ${envPath}. Delete that line to keep LLM features off.`);
870
1323
  p.note(lines.join("\n"), "ready");
871
1324
  if (agents.length > 0) await wireSelectedAgents(agents);
872
1325
  return {
@@ -874,6 +1327,19 @@ async function runOnboarding() {
874
1327
  provider
875
1328
  };
876
1329
  }
1330
+ function setLlmProviderInEnv(envPath, provider) {
1331
+ if (provider === null) delete process.env["AGENTMEMORY_LLM_PROVIDER"];
1332
+ if (!envPath || !existsSync(envPath)) return provider === null;
1333
+ try {
1334
+ const current = readFileSync(envPath, "utf-8");
1335
+ const kept = current.split("\n").filter((entry) => !/^\s*AGENTMEMORY_LLM_PROVIDER\s*=/.test(entry)).join("\n");
1336
+ const next = provider === null ? kept : `${kept}${kept.length > 0 && !kept.endsWith("\n") ? "\n" : ""}AGENTMEMORY_LLM_PROVIDER=${provider}\n`;
1337
+ if (next !== current) writeFileSync(envPath, next, { mode: 384 });
1338
+ return true;
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
877
1343
  function enableInjectContextInEnv(envPath) {
878
1344
  if (!envPath || !existsSync(envPath)) return false;
879
1345
  try {
@@ -1052,43 +1518,833 @@ async function runCaptureCommand(options) {
1052
1518
  return 0;
1053
1519
  }
1054
1520
  //#endregion
1055
- //#region src/cli.ts
1056
- const c = {
1057
- url: pc.cyan,
1058
- ok: pc.green,
1059
- warn: pc.yellow,
1060
- err: pc.red,
1061
- cmd: (s) => pc.bold(pc.cyan(s)),
1062
- label: pc.bold,
1063
- dim: pc.dim,
1064
- accent: (s) => pc.bold(pc.yellow(s))
1065
- };
1066
- const ALL_TOOLS_COUNT = getAllTools().length;
1067
- const CORE_TOOLS_COUNT = getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name)).length;
1068
- const __dirname = dirname(fileURLToPath(import.meta.url));
1069
- const args = process.argv.slice(2);
1070
- const IS_WINDOWS = platform() === "win32";
1071
- const IS_VERBOSE = args.includes("--verbose") || args.includes("-v") || process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
1072
- setBootVerbose(IS_VERBOSE);
1073
- const IS_RESET = args.includes("--reset");
1074
- hydrateProcessEnvFromFile();
1075
- if (args.includes("--version") || args.includes("-V")) {
1076
- process.stdout.write(`${VERSION}\n`);
1077
- process.exit(0);
1521
+ //#region src/cli/compression.ts
1522
+ async function runCompressionCommand(options) {
1523
+ const log = options.log ?? console.log;
1524
+ const json = options.args.includes("--json");
1525
+ try {
1526
+ const input = {};
1527
+ let retry = false;
1528
+ let dryRun = false;
1529
+ const valueFlags = {
1530
+ "--session": "sessionId",
1531
+ "--project": "project",
1532
+ "--agent-id": "agentId",
1533
+ "--limit": "limit",
1534
+ "--cursor": "cursor"
1535
+ };
1536
+ for (let i = 0; i < options.args.length; i++) {
1537
+ const flag = options.args[i];
1538
+ if (flag === "--json") continue;
1539
+ if (flag === "--retry") {
1540
+ retry = true;
1541
+ continue;
1542
+ }
1543
+ if (flag === "--dry-run") {
1544
+ dryRun = true;
1545
+ continue;
1546
+ }
1547
+ if (flag.startsWith("--port=") || flag.startsWith("--instance=")) {
1548
+ if (!flag.slice(flag.indexOf("=") + 1)) throw new Error(`Missing value for ${flag.split("=")[0]}`);
1549
+ continue;
1550
+ }
1551
+ if (flag === "--port" || flag === "--instance") {
1552
+ if (!options.args[++i] || options.args[i].startsWith("--")) throw new Error(`Missing value for ${flag}`);
1553
+ continue;
1554
+ }
1555
+ const name = valueFlags[flag];
1556
+ if (!name) throw new Error(`Unknown compression option: ${flag}`);
1557
+ if (input[name] !== void 0) throw new Error(`Duplicate option: ${flag}`);
1558
+ const value = options.args[++i];
1559
+ if (value === void 0 || value.startsWith("--")) throw new Error(`Missing value for ${flag}`);
1560
+ input[name] = value;
1561
+ }
1562
+ if (retry && dryRun) throw new Error("Use either --retry or --dry-run");
1563
+ const scope = parseCompressionScope(input);
1564
+ const headers = { "Content-Type": "application/json" };
1565
+ if (options.secret) headers.Authorization = `Bearer ${options.secret}`;
1566
+ const query = new URLSearchParams(Object.entries(scope).filter(([, value]) => value !== void 0).map(([key, value]) => [key, String(value)]));
1567
+ const response = await fetch(`${options.base}/agentmemory/compression${retry ? "/retry" : `?${query}`}`, {
1568
+ method: retry ? "POST" : "GET",
1569
+ headers,
1570
+ ...retry ? { body: JSON.stringify(scope) } : {},
1571
+ signal: AbortSignal.timeout(retry ? 12e4 : 1e4)
1572
+ });
1573
+ if (!response.ok) throw new Error(`Compression endpoint returned HTTP ${response.status}`);
1574
+ const result = await response.json();
1575
+ if (typeof result.success !== "boolean" || !Array.isArray(result.items)) throw new Error("Invalid compression response");
1576
+ if (json) log(JSON.stringify(result, null, 2));
1577
+ else {
1578
+ log(retry ? `Compression: ${result.recovered ?? 0} recovered, ${result.failed ?? 0} failed, ${result.attempted ?? 0} attempted` : `Compression: ${result.items.length} pending or failed observations (inspection only)`);
1579
+ if (result.error === "cursor_stale") log("Compression cursor changed. Restart without --cursor.");
1580
+ for (const item of result.items) log(`${item.sessionId}/${item.observationId}: ${item.outcome ?? item.status}${item.error ? ` (${item.error})` : ""}${item.error === "source_unavailable" ? " [source unavailable]" : ""}`);
1581
+ if (result.nextCursor) log(`Continue with --cursor ${result.nextCursor}`);
1582
+ if (!retry && result.items.some((item) => item.retryable)) log("Use --retry to send retained, privacy-filtered source to the configured model. Model usage may incur costs.");
1583
+ }
1584
+ return result.success ? 0 : 1;
1585
+ } catch (error) {
1586
+ const message = error instanceof Error ? error.message : "Compression request failed";
1587
+ log(json ? JSON.stringify({
1588
+ success: false,
1589
+ error: message
1590
+ }) : `Compression: ${message}`);
1591
+ return 1;
1592
+ }
1078
1593
  }
1079
- const IIPINNED_DEFAULT_VERSION = III_PINNED_VERSION;
1080
- const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || IIPINNED_DEFAULT_VERSION;
1081
- const IIIENGINE_INSTALL_CMD = `curl -fsSL https://install.iii.dev/iii/main/install.sh | VERSION=${IIPINNED_VERSION} sh`;
1082
- function iiiReleaseAsset() {
1083
- const p = platform();
1084
- const a = process.arch;
1085
- if (p === "darwin" && a === "arm64") return "iii-aarch64-apple-darwin.tar.gz";
1086
- if (p === "darwin" && a === "x64") return "iii-x86_64-apple-darwin.tar.gz";
1087
- if (p === "linux" && a === "x64") return "iii-x86_64-unknown-linux-gnu.tar.gz";
1088
- if (p === "linux" && a === "arm64") return "iii-aarch64-unknown-linux-gnu.tar.gz";
1089
- if (p === "linux" && a === "arm") return "iii-armv7-unknown-linux-gnueabihf.tar.gz";
1090
- if (p === "win32" && a === "x64") return "iii-x86_64-pc-windows-msvc.zip";
1091
- if (p === "win32" && a === "arm64") return "iii-aarch64-pc-windows-msvc.zip";
1594
+ //#endregion
1595
+ //#region src/cli/graph-index.ts
1596
+ const VALUE_FLAGS = new Set([
1597
+ "--port",
1598
+ "--instance",
1599
+ "--data-dir",
1600
+ "--tools"
1601
+ ]);
1602
+ async function runGraphIndexCommand(options) {
1603
+ const log = options.log ?? console.log;
1604
+ const json = options.args.includes("--json");
1605
+ const output = (value) => {
1606
+ if (json) {
1607
+ log(JSON.stringify(value, null, 2));
1608
+ return;
1609
+ }
1610
+ if (value.error) {
1611
+ log(String(value.error));
1612
+ return;
1613
+ }
1614
+ log(`Graph index: ${value.state ?? "unknown"}`);
1615
+ if (value.jobId) log(`Job: ${value.jobId}, processed ${value.processed ?? 0} of ${value.total ?? "unknown"}`);
1616
+ if (value.action) log(String(value.action));
1617
+ };
1618
+ let command;
1619
+ let allowLegacyScan = false;
1620
+ try {
1621
+ for (let i = 0; i < options.args.length; i++) {
1622
+ const arg = options.args[i];
1623
+ if (arg === "--json") continue;
1624
+ if (arg === "--allow-legacy-scan") {
1625
+ allowLegacyScan = true;
1626
+ continue;
1627
+ }
1628
+ if (VALUE_FLAGS.has(arg)) {
1629
+ if (!options.args[++i] || options.args[i].startsWith("--")) throw new Error(`Missing value for ${arg}`);
1630
+ continue;
1631
+ }
1632
+ if ([...VALUE_FLAGS].some((flag) => arg.startsWith(`${flag}=`) && arg.length > flag.length + 1)) continue;
1633
+ if (arg === "--verbose" || arg === "-v") continue;
1634
+ if (!command && [
1635
+ "status",
1636
+ "migrate",
1637
+ "resume"
1638
+ ].includes(arg)) {
1639
+ command = arg;
1640
+ continue;
1641
+ }
1642
+ throw new Error(`Unknown graph-index argument: ${arg}`);
1643
+ }
1644
+ command ??= "status";
1645
+ if (command === "status" && allowLegacyScan) throw new Error("status does not accept --allow-legacy-scan");
1646
+ if (command === "migrate" && !allowLegacyScan) throw new Error("Migration requires --allow-legacy-scan. Initial discovery reads each complete legacy graph scope and can use substantial memory; run it during maintenance.");
1647
+ } catch (error) {
1648
+ output({ error: error instanceof Error ? error.message : String(error) });
1649
+ return 2;
1650
+ }
1651
+ try {
1652
+ const headers = { Accept: "application/json" };
1653
+ if (options.secret) headers.Authorization = `Bearer ${options.secret}`;
1654
+ if (command !== "status") headers["Content-Type"] = "application/json";
1655
+ const response = await fetch(`${options.base}/agentmemory/graph/index${command === "status" ? "" : `/${command}`}`, {
1656
+ method: command === "status" ? "GET" : "POST",
1657
+ headers,
1658
+ ...command === "status" ? {} : { body: JSON.stringify({ allowLegacyScan }) },
1659
+ signal: AbortSignal.timeout(1e4)
1660
+ });
1661
+ const value = await response.json();
1662
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Server returned an invalid graph-index response");
1663
+ output({
1664
+ ...value,
1665
+ ...!response.ok ? { httpStatus: response.status } : {}
1666
+ });
1667
+ return response.ok ? 0 : 1;
1668
+ } catch (error) {
1669
+ output({ error: error instanceof Error ? error.message : String(error) });
1670
+ return 1;
1671
+ }
1672
+ }
1673
+ //#endregion
1674
+ //#region src/cli/compose-project.ts
1675
+ /** Registry versions are pinned here so every generated project uses one reviewed set. */
1676
+ const COMPOSE_WORKER_VERSIONS = {
1677
+ state: "0.22.20",
1678
+ http: "0.22.7"
1679
+ };
1680
+ const COMPOSE_CONTAINER_COUNT = Object.keys(COMPOSE_WORKER_VERSIONS).length + 1;
1681
+ function composeDownTimeoutMs() {
1682
+ return (20 * COMPOSE_CONTAINER_COUNT + 30) * 1e3;
1683
+ }
1684
+ function composeNamespace(restPort) {
1685
+ return `agentmemory-${restPort}`;
1686
+ }
1687
+ function composeProjectPaths(dataDir) {
1688
+ const directory = join(dataDir, "compose");
1689
+ return {
1690
+ directory,
1691
+ file: join(directory, "worker-compose.yaml"),
1692
+ stateDir: join(dataDir, "compose-state"),
1693
+ logFile: join(directory, "compose-daemon.log"),
1694
+ configDir: join(directory, "config")
1695
+ };
1696
+ }
1697
+ const AGENTMEMORY_WORKER_ENV_ALLOWLIST = new Set([
1698
+ "EMBEDDING_PROVIDER",
1699
+ "AGENTMEMORY_LLM_PROVIDER",
1700
+ "OPENAI_API_KEY",
1701
+ "OPENAI_BASE_URL",
1702
+ "OPENAI_MODEL",
1703
+ "OPENAI_EMBEDDING_API_KEY",
1704
+ "OPENAI_EMBEDDING_BASE_URL",
1705
+ "OPENAI_EMBEDDING_MODEL",
1706
+ "OPENAI_EMBEDDING_DIMENSIONS",
1707
+ "ANTHROPIC_API_KEY",
1708
+ "ANTHROPIC_BASE_URL",
1709
+ "ANTHROPIC_MODEL",
1710
+ "GEMINI_API_KEY",
1711
+ "GEMINI_MODEL",
1712
+ "GOOGLE_API_KEY",
1713
+ "VOYAGE_API_KEY",
1714
+ "VOYAGE_MODEL",
1715
+ "COHERE_API_KEY",
1716
+ "COHERE_MODEL",
1717
+ "OPENROUTER_API_KEY",
1718
+ "OPENROUTER_MODEL",
1719
+ "MINIMAX_API_KEY",
1720
+ "MINIMAX_MODEL",
1721
+ "OLLAMA_HOST",
1722
+ "LLM_PROVIDER",
1723
+ "BM25_WEIGHT",
1724
+ "VECTOR_WEIGHT",
1725
+ "MAX_TOKENS",
1726
+ "TOKEN_BUDGET",
1727
+ "MAX_OBS_PER_SESSION",
1728
+ "CONSOLIDATION_ENABLED",
1729
+ "GRAPH_EXTRACTION_ENABLED",
1730
+ "AGENTMEMORY_AUTO_COMPRESS",
1731
+ "AGENTMEMORY_INJECT_CONTEXT",
1732
+ "AGENTMEMORY_ALLOW_AGENT_SDK",
1733
+ "AGENTMEMORY_SESSION_SWEEP_ENABLED",
1734
+ "AGENTMEMORY_SESSION_SWEEP_STALE_HOURS",
1735
+ "AGENTMEMORY_FINALIZE_IDLE_MS",
1736
+ "AGENTMEMORY_GRAPH_COMPACT_ON_BOOT",
1737
+ "AGENTMEMORY_DROP_STALE_INDEX",
1738
+ "AGENTMEMORY_AUDIT_RETENTION_MONTHS",
1739
+ "AGENTMEMORY_AUDIT_MIGRATE_MAX_BYTES",
1740
+ "AGENTMEMORY_STATE_SAVE_INTERVAL_MS",
1741
+ "AGENTMEMORY_TOOLS",
1742
+ "AGENTMEMORY_VIEWER_HOST",
1743
+ "VIEWER_ALLOWED_HOSTS",
1744
+ "VIEWER_ALLOWED_ORIGINS",
1745
+ "AGENTMEMORY_SECRET",
1746
+ "AGENT_ID",
1747
+ "TEAM_ID",
1748
+ "TEAM_MODE",
1749
+ "USER_ID",
1750
+ "AGENTMEMORY_AGENT_SCOPE",
1751
+ "SNAPSHOT_DIR",
1752
+ "SNAPSHOT_ENABLED",
1753
+ "SNAPSHOT_INTERVAL",
1754
+ "STANDALONE_PERSIST_PATH",
1755
+ "CLAUDE_MEMORY_BRIDGE",
1756
+ "CLAUDE_MEMORY_LINE_BUDGET",
1757
+ "CLAUDE_PROJECT_PATH",
1758
+ "AGENTMEMORY_SUPPRESS_COST_WARNING",
1759
+ "FALLBACK_PROVIDERS",
1760
+ "OPENAI_API_VERSION",
1761
+ "OPENAI_REASONING_EFFORT",
1762
+ "OPENAI_TIMEOUT_MS",
1763
+ "MINIMAX_BASE_URL",
1764
+ "OPENROUTER_EMBEDDING_MODEL",
1765
+ "OPENROUTER_EMBEDDING_DIMENSIONS",
1766
+ "AGENTMEMORY_LLM_TIMEOUT_MS",
1767
+ "AGENTMEMORY_CAPTURE_MAX_ATTEMPTS",
1768
+ "AGENTMEMORY_CAPTURE_RETRY_INTERVAL_MS",
1769
+ "AGENTMEMORY_CAPTURE_DEDUP_HOURS",
1770
+ "AGENTMEMORY_CAPTURE_INBOX_MAX",
1771
+ "AGENTMEMORY_CAPTURE_DEAD_MAX",
1772
+ "AGENTMEMORY_CAPTURE_EVENTS_MAX",
1773
+ "AGENTMEMORY_CAPTURE_SPOOL",
1774
+ "AGENTMEMORY_CAPTURE_SPOOL_DIR",
1775
+ "AGENTMEMORY_CAPTURE_SPOOL_MAX_BYTES",
1776
+ "AGENTMEMORY_CAPTURE_SPOOL_MAX_AGE_HOURS",
1777
+ "AGENTMEMORY_IMPORT_ROOT",
1778
+ "AGENTMEMORY_EXPORT_ROOT",
1779
+ "AGENTMEMORY_GRAPH_WEIGHT",
1780
+ "SUMMARIZE_CHUNK_SIZE",
1781
+ "SUMMARIZE_CHUNK_CONCURRENCY",
1782
+ "CONSOLIDATION_DECAY_DAYS",
1783
+ "CONSOLIDATION_INTERVAL_MS",
1784
+ "AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS",
1785
+ "AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS",
1786
+ "GRAPH_EXTRACTION_BATCH_SIZE",
1787
+ "AGENTMEMORY_LLM_NOTHINK",
1788
+ "AGENTMEMORY_REFLECT",
1789
+ "AGENTMEMORY_IMAGE_EMBEDDINGS",
1790
+ "AGENTMEMORY_IMAGE_STORE_MAX_BYTES",
1791
+ "AGENTMEMORY_AUDIT_INDEX_PERSIST",
1792
+ "AGENTMEMORY_INDEX_SAVE_INTERVAL_MS",
1793
+ "AGENTMEMORY_VECTOR_BUCKET_SIZE",
1794
+ "AGENTMEMORY_VECTOR_BACKFILL_MAX",
1795
+ "AGENTMEMORY_VECTOR_BACKFILL",
1796
+ "AGENTMEMORY_SLOTS",
1797
+ "AGENTMEMORY_PROVIDER",
1798
+ "AGENTMEMORY_VERBOSE",
1799
+ "OBSIDIAN_AUTO_EXPORT",
1800
+ "AUTO_FORGET_ENABLED",
1801
+ "AUTO_FORGET_INTERVAL_MS",
1802
+ "LESSON_DECAY_ENABLED",
1803
+ "INSIGHT_DECAY_ENABLED",
1804
+ "REBUILD_EMBED_BATCH_SIZE",
1805
+ "RERANK_ENABLED"
1806
+ ]);
1807
+ const COMPOSE_RESERVED_ENV = new Set([
1808
+ "III_URL",
1809
+ "III_ENGINE_URL",
1810
+ "III_ENGINE_PORT",
1811
+ "III_REST_PORT",
1812
+ "III_VIEWER_PORT",
1813
+ "III_NAMESPACE",
1814
+ "III_COMPOSE_NAMESPACE",
1815
+ "III_COMPOSE_FILE",
1816
+ "III_COMPOSE_DIR",
1817
+ "III_CONFIG_NAME",
1818
+ "III_WORKER_NAME"
1819
+ ]);
1820
+ function stateSaveInterval(value) {
1821
+ if (value === void 0 || value.trim() === "") return getStateSaveIntervalMs();
1822
+ if (!/^\d+$/.test(value.trim())) throw new Error("AGENTMEMORY_STATE_SAVE_INTERVAL_MS must be an integer between 100 and 3600000.");
1823
+ const parsed = Number(value);
1824
+ if (!Number.isSafeInteger(parsed) || parsed < 100 || parsed > 36e5) throw new Error("AGENTMEMORY_STATE_SAVE_INTERVAL_MS must be an integer between 100 and 3600000.");
1825
+ return parsed;
1826
+ }
1827
+ function restartPolicyLines(indent, enabled) {
1828
+ if (!enabled) return [`${indent}restart: no`];
1829
+ return [
1830
+ `${indent}restart:`,
1831
+ `${indent} condition: on-failure`,
1832
+ `${indent} delay: 500ms`,
1833
+ `${indent} max_delay: 30s`,
1834
+ `${indent} max_attempts: 5`,
1835
+ `${indent} window: 60s`
1836
+ ];
1837
+ }
1838
+ function shellQuote$1(value) {
1839
+ if (/[\u0000-\u001f\u007f]/.test(value)) throw new Error("A shell command argument contains an unsupported control character.");
1840
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1841
+ }
1842
+ function shellCommand(program, args) {
1843
+ return [program, ...args].map((part) => shellQuote$1(part)).join(" ");
1844
+ }
1845
+ const WINDOWS_RUN_SAFE = /^[A-Za-z0-9._:\\/-]+$/;
1846
+ const WINDOWS_EXE_SAFE = /^[A-Za-z0-9._-]+$/;
1847
+ function windowsWorkerScript(script) {
1848
+ if (!WINDOWS_RUN_SAFE.test(script)) throw new Error(`On Windows, AGENTMEMORY_WORKER_BIN_DIR must be a path without spaces or cmd metacharacters; got ${JSON.stringify(script)}.`);
1849
+ return script;
1850
+ }
1851
+ function windowsNodeRun(nodeBin) {
1852
+ const base = win32.basename(nodeBin);
1853
+ if (!WINDOWS_EXE_SAFE.test(base)) throw new Error(`The Node executable name must not contain spaces or cmd metacharacters on Windows; got ${JSON.stringify(nodeBin)}.`);
1854
+ return `${base} dist/index.mjs`;
1855
+ }
1856
+ function windowsWorkerPath(nodeBin, inherited) {
1857
+ const entries = [win32.dirname(nodeBin), ...(inherited ?? "").split(";")].map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1858
+ const seen = /* @__PURE__ */ new Set();
1859
+ return entries.filter((entry) => {
1860
+ const key = entry.toLowerCase();
1861
+ if (seen.has(key)) return false;
1862
+ seen.add(key);
1863
+ return true;
1864
+ }).join(";");
1865
+ }
1866
+ function yamlString(value) {
1867
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) throw new Error("A Compose path or configuration value contains an unsupported control character.");
1868
+ return `'${value.replace(/'/g, "''")}'`;
1869
+ }
1870
+ function localWorkerDirectory(value, cwd, projectRoot) {
1871
+ if (isAbsolute(value)) return resolve(value);
1872
+ const fromCwd = resolve(cwd, value);
1873
+ const fromProject = resolve(projectRoot, value);
1874
+ if (fromCwd === fromProject) return fromCwd;
1875
+ if (!fromCwd.startsWith(`${resolve(projectRoot)}/`)) return fromCwd;
1876
+ return fromProject;
1877
+ }
1878
+ function localWorkerProjectDir(paths, name) {
1879
+ return join(paths.directory, "local-workers", name);
1880
+ }
1881
+ function ensureEngineTraceStorageConfig(configDir, dataDir) {
1882
+ const configFile = join(configDir, "iii-observability.yaml");
1883
+ const traceDirectory = resolve(dataDir, "observability", "traces");
1884
+ const defaultTraceDirectory = "./data/observability/traces";
1885
+ let entry;
1886
+ if (existsSync(configFile)) {
1887
+ const parsed = parse(readFileSync(configFile, "utf8"));
1888
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Invalid persisted iii-observability configuration at ${configFile}.`);
1889
+ entry = parsed;
1890
+ if (entry.id !== "iii-observability") throw new Error(`Unexpected configuration id in ${configFile}; refusing to replace it.`);
1891
+ } else entry = {
1892
+ id: "iii-observability",
1893
+ name: "Observability",
1894
+ description: "",
1895
+ schema: {},
1896
+ value: {
1897
+ sampling_ratio: .1,
1898
+ logs_console_output: false
1899
+ }
1900
+ };
1901
+ const value = entry.value && typeof entry.value === "object" && !Array.isArray(entry.value) ? entry.value : {};
1902
+ let changed = false;
1903
+ if (value.sampling_ratio === void 0) {
1904
+ value.sampling_ratio = .1;
1905
+ changed = true;
1906
+ }
1907
+ if (value.logs_console_output === void 0) {
1908
+ value.logs_console_output = false;
1909
+ changed = true;
1910
+ }
1911
+ const existingStorage = value.trace_storage;
1912
+ const storage = existingStorage && typeof existingStorage === "object" && !Array.isArray(existingStorage) ? existingStorage : {};
1913
+ const existingDirectory = storage.directory;
1914
+ if (typeof existingDirectory === "string" && existingDirectory.trim() !== "" && existingDirectory !== defaultTraceDirectory) {
1915
+ if (!changed) return;
1916
+ } else {
1917
+ storage.directory = traceDirectory;
1918
+ changed = true;
1919
+ }
1920
+ if (!changed) return;
1921
+ if (existingDirectory === void 0 || existingDirectory === defaultTraceDirectory || existingDirectory === "") value.trace_storage = storage;
1922
+ entry.value = value;
1923
+ const temporaryFile = `${configFile}.tmp`;
1924
+ writeFileSync(temporaryFile, stringify(entry), {
1925
+ encoding: "utf8",
1926
+ mode: 384
1927
+ });
1928
+ chmodSync(temporaryFile, 384);
1929
+ renameSync(temporaryFile, configFile);
1930
+ chmodSync(configFile, 384);
1931
+ }
1932
+ function renderWorkerCompose(options) {
1933
+ const { restPort, viewerPort, enginePort } = options.ports;
1934
+ const paths = composeProjectPaths(options.dataDir);
1935
+ const namespace = composeNamespace(restPort);
1936
+ const windows = options.windows ?? process.platform === "win32";
1937
+ const workerBinDir = options.workerBinDir ? options.workerBinDirIsAbsolute ? windows ? win32.resolve(options.workerBinDir) : resolve(options.workerBinDir) : localWorkerDirectory(options.workerBinDir, options.cwd ?? process.cwd(), options.projectRoot) : null;
1938
+ const startupSeconds = Math.ceil((options.startupTimeoutMs ?? 3e5) / 1e3);
1939
+ const restartEnabled = !options.disableRestartPolicy;
1940
+ if (!Number.isSafeInteger(startupSeconds) || startupSeconds < 1 || startupSeconds * 1e3 > 3600999) throw new Error("Compose worker startup timeout must be between 1 and 3600000 milliseconds.");
1941
+ const workerSource = (name) => workerBinDir ? `path://${localWorkerProjectDir(paths, name)}` : `package://api.workers.iii.dev/${name}`;
1942
+ const workerScript = (name) => workerBinDir ? windows ? win32.join(workerBinDir, name) : join(workerBinDir, name) : "";
1943
+ const filteredEnvFile = options.envFile ? join(paths.directory, ".agentmemory-filtered.env") : void 0;
1944
+ const lines = [
1945
+ `namespace: ${namespace}`,
1946
+ "startup_timeout: 60s",
1947
+ `stop_timeout: 20s`,
1948
+ "required_default: true",
1949
+ "engine:",
1950
+ ` url: ws://127.0.0.1:${enginePort}`,
1951
+ " workers:",
1952
+ " configuration:",
1953
+ " adapter:",
1954
+ " name: fs",
1955
+ " config:",
1956
+ ` directory: ${yamlString(paths.configDir)}`,
1957
+ " iii-worker-manager:",
1958
+ " host: 127.0.0.1",
1959
+ ` port: ${enginePort}`,
1960
+ "containers:"
1961
+ ];
1962
+ const stateScript = workerScript("state");
1963
+ lines.push(" state:", ` worker: ${yamlString(workerSource("state"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.state)}`] : [], " config_name: state", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(windows ? windowsWorkerScript(stateScript) : shellCommand(stateScript, []))}`] : [], " config_override:", " adapter:", " name: kv", " config:");
1964
+ if (options.stateBackend?.kind === "redis") {
1965
+ const redisUrl = options.redisUrl ?? options.stateBackend.redisUrl;
1966
+ if (!redisUrl) throw new Error("AGENTMEMORY_STATE_BACKEND=redis requires AGENTMEMORY_REDIS_URL to be set (e.g. redis://localhost:6379).");
1967
+ if (/[\u0000-\u001f\u007f]/.test(redisUrl)) throw new Error("AGENTMEMORY_REDIS_URL contains a control character; percent-encode it before using Redis.");
1968
+ lines.splice(lines.length - 2, 2, " name: redis", " config:", ` redis_url: ${yamlString(redisUrl)}`);
1969
+ } else lines.push(" store_method: file_based", ` file_path: ${yamlString(join(options.dataDir, "state_store.db"))}`, ` save_interval_ms: ${stateSaveInterval(options.workerEnvironment?.AGENTMEMORY_STATE_SAVE_INTERVAL_MS)}`);
1970
+ const httpScript = workerScript("http");
1971
+ lines.push(" http:", ` worker: ${yamlString(workerSource("http"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.http)}`] : [], " config_name: http", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(windows ? windowsWorkerScript(httpScript) : shellCommand(httpScript, []))}`] : [], " config_override:", " host: 127.0.0.1", ` port: ${restPort}`, " default_timeout: 180000", " cors:", " allowed_origins:", ` - ${yamlString(`http://localhost:${restPort}`)}`, ` - ${yamlString(`http://localhost:${viewerPort}`)}`, ` - ${yamlString(`http://127.0.0.1:${restPort}`)}`, ` - ${yamlString(`http://127.0.0.1:${viewerPort}`)}`, " allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]");
1972
+ lines.push(" agentmemory:", ` worker: ${yamlString(`path://${options.projectRoot}`)}`, " start_after: [state, http]", ` startup_timeout: ${startupSeconds}s`, " config_name: agentmemory", ...restartPolicyLines(" ", restartEnabled), " scripts:", ` run: ${yamlString(windows ? windowsNodeRun(options.nodeBin) : shellCommand(options.nodeBin, ["dist/index.mjs"]))}`);
1973
+ if (filteredEnvFile) lines.push(` env_file: [${yamlString(filteredEnvFile)}]`);
1974
+ const explicitWorkerEnvironment = Object.entries(options.workerEnvironment ?? {}).filter(([key, value]) => value !== void 0 && AGENTMEMORY_WORKER_ENV_ALLOWLIST.has(key) && !COMPOSE_RESERVED_ENV.has(key)).sort(([left], [right]) => left.localeCompare(right));
1975
+ lines.push(" environment:", ...explicitWorkerEnvironment.map(([key, value]) => ` ${key}: ${yamlString(value)}`), ...windows ? [` NoDefaultCurrentDirectoryInExePath: ${yamlString("1")}`] : [], ` HOME: ${yamlString(options.homeDir)}`, ` PATH: ${yamlString(windows ? windowsWorkerPath(options.nodeBin, options.workerEnvironment?.PATH ?? process.env.PATH) : `${dirname(options.nodeBin)}:/usr/local/bin:/usr/bin:/bin`)}`, ` AGENTMEMORY_DATA_DIR: ${yamlString(options.dataDir)}`, ` AGENTMEMORY_RUNTIME_DIR: ${yamlString(options.dataDir)}`, ...filteredEnvFile ? [` AGENTMEMORY_ENV_FILE: ${yamlString(filteredEnvFile)}`] : [], ...options.stateBackend?.kind === "redis" && options.redisUrl ? [` AGENTMEMORY_REDIS_URL: ${yamlString(options.redisUrl)}`, " AGENTMEMORY_STATE_BACKEND: 'redis'"] : [], ` III_ENGINE_PORT: ${yamlString(String(enginePort))}`, ` III_REST_PORT: ${yamlString(String(restPort))}`, ` III_VIEWER_PORT: ${yamlString(String(viewerPort))}`, " III_TELEMETRY_ENABLED: 'false'", ...options.linux ?? process.platform === "linux" ? [" MALLOC_ARENA_MAX: '2'"] : [], "");
1976
+ return lines.join("\n");
1977
+ }
1978
+ function writeWorkerCompose(options) {
1979
+ const paths = composeProjectPaths(options.dataDir);
1980
+ mkdirSync(paths.directory, {
1981
+ recursive: true,
1982
+ mode: 448
1983
+ });
1984
+ mkdirSync(paths.configDir, {
1985
+ recursive: true,
1986
+ mode: 448
1987
+ });
1988
+ mkdirSync(paths.stateDir, {
1989
+ recursive: true,
1990
+ mode: 448
1991
+ });
1992
+ ensureEngineTraceStorageConfig(paths.configDir, options.dataDir);
1993
+ mkdirSync(join(options.dataDir, "observability", "traces"), {
1994
+ recursive: true,
1995
+ mode: 448
1996
+ });
1997
+ if (options.workerBinDir) {
1998
+ const binDir = options.workerBinDirIsAbsolute ? resolve(options.workerBinDir) : localWorkerDirectory(options.workerBinDir, options.cwd ?? process.cwd(), options.projectRoot);
1999
+ for (const name of Object.keys(COMPOSE_WORKER_VERSIONS)) {
2000
+ const projectDir = localWorkerProjectDir(paths, name);
2001
+ mkdirSync(projectDir, {
2002
+ recursive: true,
2003
+ mode: 448
2004
+ });
2005
+ const manifest = [
2006
+ "iii: v1",
2007
+ `name: ${name}`,
2008
+ "language: rust",
2009
+ "scripts:",
2010
+ ` start: ${yamlString(join(binDir, name))}`,
2011
+ ""
2012
+ ].join("\n");
2013
+ writeFileSync(join(projectDir, "iii.worker.yaml"), manifest, {
2014
+ encoding: "utf8",
2015
+ mode: 384
2016
+ });
2017
+ chmodSync(join(projectDir, "iii.worker.yaml"), 384);
2018
+ }
2019
+ }
2020
+ const text = renderWorkerCompose(options);
2021
+ if (options.envFile) {
2022
+ const filteredPath = join(paths.directory, ".agentmemory-filtered.env");
2023
+ writeFileSync(filteredPath, `${readFileSync(options.envFile, "utf8").split(/\r?\n/).filter((line) => {
2024
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
2025
+ return !match || !COMPOSE_RESERVED_ENV.has(match[1]);
2026
+ }).join("\n")}\n`, {
2027
+ encoding: "utf8",
2028
+ mode: 384
2029
+ });
2030
+ chmodSync(filteredPath, 384);
2031
+ }
2032
+ writeFileSync(paths.file, text, {
2033
+ encoding: "utf8",
2034
+ mode: 384
2035
+ });
2036
+ chmodSync(paths.file, 384);
2037
+ return {
2038
+ file: paths.file,
2039
+ stateDir: paths.stateDir,
2040
+ logFile: paths.logFile,
2041
+ text
2042
+ };
2043
+ }
2044
+ //#endregion
2045
+ //#region src/cli/compose-control.ts
2046
+ function composeRegistryFailureMessage(logText, workerBinDir) {
2047
+ if (![
2048
+ /REGISTRY_UNREACHABLE|REGISTRY_ERROR|PACKAGE_DOWNLOAD_FAILED/i,
2049
+ /cannot reach the registry/i,
2050
+ /registry (?:returned|responded|response)[^\n]*HTTP 5\d\d|HTTP 5\d\d[^\n]*registry/i,
2051
+ /failed to resolve (?:a )?package|package resolution (?:failed|unavailable)|could not resolve .*registry/i,
2052
+ /could not download https:\/\/api\.workers\.iii\.dev\/download\//i
2053
+ ].some((pattern) => pattern.test(logText))) return null;
2054
+ return `Worker registry unavailable while resolving state/http; retry later or set AGENTMEMORY_WORKER_BIN_DIR.${workerBinDir ? ` Configured local workers directory: ${workerBinDir}.` : ""}`;
2055
+ }
2056
+ function composeStateRegistryFailure(dataDir, file) {
2057
+ const stateRoot = join(dataDir, "compose-state");
2058
+ try {
2059
+ const files = [];
2060
+ const walk = (dir) => {
2061
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2062
+ const path = join(dir, entry.name);
2063
+ if (entry.isDirectory()) walk(path);
2064
+ else if (entry.name === "state.json") files.push(path);
2065
+ }
2066
+ };
2067
+ walk(stateRoot);
2068
+ for (const stateFile of files) {
2069
+ const state = JSON.parse(readFileSync(stateFile, "utf8"));
2070
+ const message = Object.entries(state.containers ?? {}).filter(([, value]) => value.status === "failed").map(([name, value]) => `${name}: ${value.last_error ?? "failed"}`).join("\n");
2071
+ if (!sameComposeFile(state.compose_path, file)) continue;
2072
+ if (message && composeRegistryFailureMessage(message)) return message;
2073
+ }
2074
+ } catch {}
2075
+ return null;
2076
+ }
2077
+ function composeWorkerFailureSummary(project) {
2078
+ if (!project) return null;
2079
+ const failed = (project.containers ?? []).filter((container) => container.state === "failed");
2080
+ if (!failed.length) return null;
2081
+ return failed.map((container) => `${container.container ?? "worker"}: ${container.last_error ?? "failed after restart attempts were exhausted"}`).join("\n");
2082
+ }
2083
+ function defaultCanonicalizer(windows) {
2084
+ return (value) => {
2085
+ try {
2086
+ return windows ? realpathSync.native(value) : realpathSync(value);
2087
+ } catch {
2088
+ return value;
2089
+ }
2090
+ };
2091
+ }
2092
+ function normalizeWindowsPath(value) {
2093
+ let out = value;
2094
+ if (out.startsWith("\\\\?\\UNC\\")) out = `\\\\${out.slice(8)}`;
2095
+ else if (out.startsWith("\\\\?\\")) out = out.slice(4);
2096
+ return out.replace(/\//g, "\\").toLowerCase();
2097
+ }
2098
+ function sameComposeFile(left, right, windows = process.platform === "win32", canonicalize = defaultCanonicalizer(windows)) {
2099
+ if (!left) return false;
2100
+ if (!windows) return left === right || canonicalize(left) === canonicalize(right);
2101
+ if (normalizeWindowsPath(left) === normalizeWindowsPath(right)) return true;
2102
+ return normalizeWindowsPath(canonicalize(left)) === normalizeWindowsPath(canonicalize(right));
2103
+ }
2104
+ function ownedComposeProject(status, file, windows = process.platform === "win32", canonicalize = defaultCanonicalizer(windows)) {
2105
+ if (sameComposeFile(status.file, file, windows, canonicalize)) return status;
2106
+ return status.projects?.find((item) => sameComposeFile(item.file, file, windows, canonicalize)) ?? null;
2107
+ }
2108
+ function cleanDaemonEnv(env) {
2109
+ const out = {};
2110
+ for (const key of [
2111
+ "HOME",
2112
+ "PATH",
2113
+ "LANG",
2114
+ "LC_ALL",
2115
+ "TERM",
2116
+ "NO_COLOR",
2117
+ "III_TELEMETRY_ENABLED",
2118
+ "MALLOC_ARENA_MAX",
2119
+ "III_BUNDLE_WORKERS_DISABLED",
2120
+ "III_COMPOSE_MAX_PARALLEL_WORKERS",
2121
+ "AGENTMEMORY_TOOLS",
2122
+ "AGENTMEMORY_VIEWER_HOST",
2123
+ "VIEWER_ALLOWED_HOSTS",
2124
+ "VIEWER_ALLOWED_ORIGINS"
2125
+ ]) {
2126
+ const value = env[key];
2127
+ if (value !== void 0) out[key] = value;
2128
+ }
2129
+ return out;
2130
+ }
2131
+ async function assertComposePortsFree(ports) {
2132
+ for (const port of [...new Set(ports)]) if (await new Promise((resolve) => {
2133
+ const socket = createConnection({
2134
+ host: "127.0.0.1",
2135
+ port
2136
+ });
2137
+ const done = (value) => {
2138
+ socket.destroy();
2139
+ resolve(value);
2140
+ };
2141
+ socket.setTimeout(350);
2142
+ socket.once("connect", () => done(true));
2143
+ socket.once("timeout", () => done(false));
2144
+ socket.once("error", () => done(false));
2145
+ })) throw new Error(`Port ${port} is already in use on 127.0.0.1. Stop the process that owns it or choose another --port/--instance before starting agentmemory.`);
2146
+ }
2147
+ function composeEnvironment(env, stateDir) {
2148
+ return {
2149
+ ...cleanDaemonEnv(env),
2150
+ III_COMPOSE_STATE_DIR: stateDir,
2151
+ III_TELEMETRY_ENABLED: "false",
2152
+ ...process.platform === "linux" ? { MALLOC_ARENA_MAX: "2" } : {},
2153
+ ...env.AGENTMEMORY_DISABLE_COMPOSE_RESTART === "1" ? { AGENTMEMORY_DISABLE_COMPOSE_RESTART: "1" } : {}
2154
+ };
2155
+ }
2156
+ let quietDepth = 0;
2157
+ let savedDebug = null;
2158
+ async function withQuietSdkDebug(run) {
2159
+ if (quietDepth === 0) {
2160
+ savedDebug = console.debug;
2161
+ console.debug = () => {};
2162
+ }
2163
+ quietDepth++;
2164
+ try {
2165
+ return await run();
2166
+ } finally {
2167
+ quietDepth--;
2168
+ if (quietDepth === 0 && savedDebug) {
2169
+ console.debug = savedDebug;
2170
+ savedDebug = null;
2171
+ }
2172
+ }
2173
+ }
2174
+ async function composeCall(options, register = registerWorker) {
2175
+ return withQuietSdkDebug(() => composeCallWithClient(options, register));
2176
+ }
2177
+ async function composeCallWithClient(options, register) {
2178
+ const client = register(options.engineUrl, {
2179
+ workerName: "agentmemory-cli",
2180
+ namespace: options.namespace,
2181
+ invocationTimeoutMs: options.timeoutMs ?? 3e4,
2182
+ otel: { enabled: false },
2183
+ enableMetricsReporting: false
2184
+ });
2185
+ try {
2186
+ const connectionDeadline = Date.now() + 8e3;
2187
+ while (client.getConnectionState() !== "connected" && Date.now() < connectionDeadline) {
2188
+ const fatal = client.getFatalError();
2189
+ if (fatal) throw fatal;
2190
+ await new Promise((resolve) => setTimeout(resolve, 50));
2191
+ }
2192
+ if (client.getConnectionState() !== "connected") throw new Error("Could not connect the Compose control client to its instance engine.");
2193
+ const deadline = Date.now() + 8e3;
2194
+ let lastError;
2195
+ while (Date.now() < deadline) try {
2196
+ const result = await client.trigger({
2197
+ function_id: options.functionId,
2198
+ payload: { file: options.file },
2199
+ timeoutMs: options.timeoutMs ?? 3e4
2200
+ });
2201
+ if (result && typeof result === "object" && result.status === "failed") {
2202
+ const detail = result.error?.message ?? result.error?.code ?? "unknown Compose failure";
2203
+ throw Object.assign(/* @__PURE__ */ new Error(`${result.error?.code ? `${result.error.code}: ` : ""}${detail}`), { retryable: false });
2204
+ }
2205
+ return result;
2206
+ } catch (err) {
2207
+ lastError = err;
2208
+ if (err && typeof err === "object" && "retryable" in err && err.retryable === false) throw err;
2209
+ await new Promise((resolve) => setTimeout(resolve, 150));
2210
+ }
2211
+ throw lastError ?? /* @__PURE__ */ new Error("Compose daemon did not accept the control connection.");
2212
+ } finally {
2213
+ await client.shutdown().catch(() => {});
2214
+ }
2215
+ }
2216
+ function startComposeDaemon(runtime) {
2217
+ const project = writeWorkerCompose({
2218
+ ...runtime,
2219
+ envFile: runtime.envFile
2220
+ });
2221
+ const logFile = runtime.logFile;
2222
+ mkdirSync(dirname(logFile), {
2223
+ recursive: true,
2224
+ mode: 448
2225
+ });
2226
+ chmodSync(dirname(logFile), 448);
2227
+ const fd = openSync(logFile, "a", 384);
2228
+ const logStartOffset = fstatSync(fd).size;
2229
+ writeFileSync(fd, `\n[${(/* @__PURE__ */ new Date()).toISOString()}] Starting Worker Compose\n`);
2230
+ const environment = composeEnvironment(runtime.env, project.stateDir);
2231
+ const child = spawn(runtime.iiiBin, [
2232
+ "compose",
2233
+ "--namespace",
2234
+ composeNamespace(runtime.ports.restPort),
2235
+ "--up",
2236
+ "--file",
2237
+ project.file
2238
+ ], {
2239
+ cwd: runtime.projectRoot,
2240
+ detached: true,
2241
+ stdio: [
2242
+ "ignore",
2243
+ fd,
2244
+ fd
2245
+ ],
2246
+ windowsHide: true,
2247
+ env: environment
2248
+ });
2249
+ closeSync(fd);
2250
+ child.unref();
2251
+ return {
2252
+ child,
2253
+ file: project.file,
2254
+ namespace: composeNamespace(runtime.ports.restPort),
2255
+ stateDir: project.stateDir,
2256
+ logFile,
2257
+ logStartOffset
2258
+ };
2259
+ }
2260
+ function readComposeLogFromOffset(file, offset) {
2261
+ const fd = openSync(file, "r");
2262
+ try {
2263
+ const size = fstatSync(fd).size;
2264
+ let position = Math.min(Math.max(0, offset), size);
2265
+ const chunks = [];
2266
+ while (position < size) {
2267
+ const chunk = Buffer.allocUnsafe(Math.min(16 * 1024, size - position));
2268
+ const count = readSync(fd, chunk, 0, chunk.length, position);
2269
+ if (count === 0) break;
2270
+ chunks.push(chunk.subarray(0, count));
2271
+ position += count;
2272
+ }
2273
+ return Buffer.concat(chunks).toString("utf8");
2274
+ } finally {
2275
+ closeSync(fd);
2276
+ }
2277
+ }
2278
+ function writeComposeState(path, state) {
2279
+ mkdirSync(dirname(path), {
2280
+ recursive: true,
2281
+ mode: 448
2282
+ });
2283
+ writeFileSync(path, `${JSON.stringify(state)}\n`, {
2284
+ encoding: "utf8",
2285
+ mode: 384
2286
+ });
2287
+ chmodSync(path, 384);
2288
+ }
2289
+ //#endregion
2290
+ //#region src/cli/legacy-docker-state.ts
2291
+ function shellQuote(value) {
2292
+ return `'${value.replaceAll("'", "'\"'\"'")}'`;
2293
+ }
2294
+ function legacyDockerStopCommand(state) {
2295
+ const project = state.projectName?.trim();
2296
+ const composeFile = state.composeFile?.trim();
2297
+ if (composeFile) return `docker compose${project ? ` -p ${shellQuote(project)}` : ""} -f ${shellQuote(composeFile)} stop`;
2298
+ if (project) return `docker compose -p ${shellQuote(project)} stop`;
2299
+ if (state.containerId?.trim()) return `docker stop ${shellQuote(state.containerId.trim())}`;
2300
+ return "Cannot construct a stop command: legacy lifecycle state has no Compose file, project, or container ID.";
2301
+ }
2302
+ function legacyDockerStateMessage(state) {
2303
+ return [
2304
+ `This installation was started by an older Docker-based agentmemory CLI (REST port ${state.restPort && Number.isFinite(state.restPort) ? state.restPort : Number(state.projectName?.match(/^agentmemory-(\d+)$/)?.[1] ?? 3111)}); Docker lifecycle management is no longer part of this CLI.`,
2305
+ "Stop the old container explicitly before switching to Worker Compose:",
2306
+ ` ${legacyDockerStopCommand(state)}`,
2307
+ "The legacy lifecycle state is preserved; no container or data was changed."
2308
+ ].join("\n");
2309
+ }
2310
+ //#endregion
2311
+ //#region src/cli.ts
2312
+ const c = {
2313
+ url: pc.cyan,
2314
+ ok: pc.green,
2315
+ warn: pc.yellow,
2316
+ err: pc.red,
2317
+ cmd: (s) => pc.bold(pc.cyan(s)),
2318
+ label: pc.bold,
2319
+ dim: pc.dim,
2320
+ accent: (s) => pc.bold(pc.yellow(s))
2321
+ };
2322
+ const ALL_TOOLS_COUNT = getAllTools().length;
2323
+ const CORE_TOOLS_COUNT = getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name)).length;
2324
+ const __dirname = dirname(fileURLToPath(import.meta.url));
2325
+ const args = process.argv.slice(2);
2326
+ const IS_WINDOWS = platform() === "win32";
2327
+ const IS_VERBOSE = args.includes("--verbose") || args.includes("-v") || process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
2328
+ setBootVerbose(IS_VERBOSE);
2329
+ const IS_RESET = args.includes("--reset");
2330
+ const shellEnv = hydrateProcessEnvFromFile();
2331
+ if (args.includes("--version") || args.includes("-V")) {
2332
+ process.stdout.write(`${VERSION}\n`);
2333
+ process.exit(0);
2334
+ }
2335
+ const IIPINNED_DEFAULT_VERSION = III_PINNED_VERSION;
2336
+ const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || IIPINNED_DEFAULT_VERSION;
2337
+ const IIIENGINE_INSTALL_CMD = `curl -fsSL https://install.iii.dev/iii/main/install.sh | VERSION=${IIPINNED_VERSION} sh`;
2338
+ function iiiReleaseAsset() {
2339
+ const p = platform();
2340
+ const a = process.arch;
2341
+ if (p === "darwin" && a === "arm64") return "iii-aarch64-apple-darwin.tar.gz";
2342
+ if (p === "darwin" && a === "x64") return "iii-x86_64-apple-darwin.tar.gz";
2343
+ if (p === "linux" && a === "x64") return "iii-x86_64-unknown-linux-gnu.tar.gz";
2344
+ if (p === "linux" && a === "arm64") return "iii-aarch64-unknown-linux-gnu.tar.gz";
2345
+ if (p === "linux" && a === "arm") return "iii-armv7-unknown-linux-gnueabihf.tar.gz";
2346
+ if (p === "win32" && a === "x64") return "iii-x86_64-pc-windows-msvc.zip";
2347
+ if (p === "win32" && a === "arm64") return "iii-aarch64-pc-windows-msvc.zip";
1092
2348
  return null;
1093
2349
  }
1094
2350
  function iiiReleaseUrl() {
@@ -1119,15 +2375,14 @@ agentmemory — persistent memory for AI coding agents
1119
2375
  Usage: agentmemory [command] [options]
1120
2376
 
1121
2377
  Commands:
1122
- (default) Start agentmemory worker
2378
+ (default) Start agentmemory via a per-instance Worker Compose project
1123
2379
  init Copy bundled .env.example to ~/.agentmemory/.env if absent
1124
2380
  connect [agent] Wire agentmemory into an installed agent
1125
2381
  (${wrapList(knownAgents(), 21)}).
1126
2382
  No arg = interactive picker. --all wires every detected agent.
1127
2383
  --dry-run shows what would change. --force re-installs.
1128
2384
  status Show connection status, memory count, flags, and health
1129
- console Launch the iii web console for this engine (workers, functions,
1130
- triggers, queues, traces). --console-port N, default viewer+1
2385
+ console Show migration guidance for the retired iii console
1131
2386
  doctor Interactive diagnostic + fixer. [F]ix · [S]kip · [?]more · [Q]uit
1132
2387
  --all: apply every fix without prompting (CI)
1133
2388
  --dry-run: show what each fix would do, don't execute
@@ -1138,14 +2393,20 @@ Commands:
1138
2393
  in one command (no second terminal).
1139
2394
  upgrade Upgrade local deps + iii runtime (best effort)
1140
2395
  stop [--force] Stop the running iii-engine started by this CLI.
1141
- --force bypasses the Docker-heuristic guard and signals
1142
- whatever pidfile+lsof report on the REST port (use when
1143
- the engine was started natively but state file is missing).
2396
+ --force bypasses the native engine PID identity guard
2397
+ (use when the engine was started natively but state file is missing).
1144
2398
  mcp Start standalone MCP shim — opt-in surface for MCP-only clients
1145
2399
  (Cursor, Gemini CLI, etc). REST always available at :3111.
1146
2400
  capture [--drain] Show capture health: the local offline spool and the server
1147
2401
  inbox (pending, retrying, dead letters). --drain sends
1148
2402
  spooled observations now. --json for machine output.
2403
+ compression Inspect pending or failed observation compression.
2404
+ --session <id> or --project <name> selects the scope.
2405
+ --agent-id <id> narrows it; --limit <N> bounds the batch.
2406
+ --retry explicitly retries compression; --json for machine output.
2407
+ graph-index Inspect bounded graph index status. --json for machine output.
2408
+ migrate --allow-legacy-scan explicitly scans legacy graph data.
2409
+ resume continues a sealed migration without another full scan.
1149
2410
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
1150
2411
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
1151
2412
  out-of-range is rejected; for trees >1000 files, batch by subdirectory)
@@ -1156,30 +2417,32 @@ Options:
1156
2417
  --reset Wipe ~/.agentmemory/preferences.json and re-run onboarding
1157
2418
  --tools all|core Tool visibility (default: all = ${ALL_TOOLS_COUNT} tools; core = ${CORE_TOOLS_COUNT} essentials)
1158
2419
  --no-engine Skip auto-starting iii-engine
1159
- --port <N> Override REST port (default: 3111). Streams (N+1), viewer
1160
- (N+2), and iii engine (N+46023) auto-derive from N so a
1161
- single flag relocates the whole quartet.
2420
+ --port <N> Override REST port (default: 3111). Viewer (N+2) and
2421
+ iii engine (N+46023) derive from N for side-by-side instances.
1162
2422
  --instance <N> Shortcut for --port (3111 + N*100) to run multiple
1163
2423
  daemons side-by-side without env gymnastics.
1164
- --instance 1 -> 3211/3212/3213/49234, etc. (max N=50)
2424
+ --instance 1 -> 3211/3213/49234, etc. (max N=50)
1165
2425
  --data-dir <path> Store iii-engine state outside the current repo
1166
2426
 
1167
2427
  Environment:
1168
2428
  AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
1169
2429
  Honored by status, doctor, and MCP shim commands.
1170
2430
  AGENTMEMORY_DATA_DIR State directory fallback when --data-dir is not set.
1171
- AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
1172
- native iii-engine binary on first run.
1173
2431
  AGENTMEMORY_III_VERSION Override pinned iii-engine version (default ${IIPINNED_VERSION}).
1174
- AGENTMEMORY_III_CONFIG Engine config to start from (default: ./iii-config.yaml,
1175
- then ~/.agentmemory/iii-config.yaml, then the bundled
1176
- loopback-only config).
2432
+ AGENTMEMORY_ENGINE_READY_TIMEOUT_MS
2433
+ Engine readiness deadline in milliseconds (default 120000).
2434
+ AGENTMEMORY_READY_TIMEOUT_MS
2435
+ Worker readiness deadline in milliseconds (default 300000).
2436
+ Both deadlines accept integers from 1 through 3600000.
2437
+ AGENTMEMORY_ENGINE_MANAGES_WORKER
2438
+ true for custom engine-managed worker wrappers; false
2439
+ for CLI-managed workers. Unset detects direct worker commands.
1177
2440
  AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS
1178
2441
  Window (seconds) for the smart-search follow-up diagnostic
1179
2442
  (default 30). Long values overcount, short values undercount.
1180
2443
 
1181
2444
  Quick start:
1182
- npx @agentmemory/agentmemory # start with local iii-engine or Docker
2445
+ npx @agentmemory/agentmemory # start pinned iii Worker Compose runtime
1183
2446
  npx @agentmemory/agentmemory demo # see semantic recall in 30 seconds
1184
2447
  npx @agentmemory/agentmemory doctor # diagnose config + feature flags
1185
2448
  npx @agentmemory/agentmemory status # health + memory count + flags
@@ -1199,7 +2462,9 @@ const URL_CLIENT_COMMANDS = new Set([
1199
2462
  "status",
1200
2463
  "doctor",
1201
2464
  "mcp",
1202
- "capture"
2465
+ "capture",
2466
+ "compression",
2467
+ "graph-index"
1203
2468
  ]);
1204
2469
  let hasExplicitLocalPortOverride = false;
1205
2470
  let selectedInstance = 0;
@@ -1232,7 +2497,6 @@ if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance="))) {
1232
2497
  }
1233
2498
  const restPort = parseInt(process.env["III_REST_PORT"] || "3111", 10);
1234
2499
  if (Number.isFinite(restPort) && restPort > 0) {
1235
- process.env["III_STREAM_PORT"] ??= process.env["III_STREAMS_PORT"] ?? String(restPort + 1);
1236
2500
  process.env["III_VIEWER_PORT"] ??= String(restPort + 2);
1237
2501
  const configuredEngineUrl = process.env["III_ENGINE_URL"];
1238
2502
  if (configuredEngineUrl) try {
@@ -1293,9 +2557,6 @@ function getViewerUrl() {
1293
2557
  return `http://localhost:${getConfiguredViewerPort()}`;
1294
2558
  }
1295
2559
  }
1296
- function getStreamPort() {
1297
- return parseInt(process.env["III_STREAM_PORT"] || "", 10) || parseInt(process.env["III_STREAMS_PORT"] || "", 10) || getRestPort() + 1;
1298
- }
1299
2560
  function getEnginePort() {
1300
2561
  const url = process.env["III_ENGINE_URL"];
1301
2562
  if (url) try {
@@ -1307,43 +2568,19 @@ function getEnginePort() {
1307
2568
  return getRestPort() + 46023;
1308
2569
  }
1309
2570
  async function isEngineRunning() {
1310
- try {
1311
- await fetch(`${getBaseUrl()}/`, { signal: AbortSignal.timeout(2e3) });
1312
- return true;
1313
- } catch {
1314
- return false;
1315
- }
2571
+ const state = readEngineState();
2572
+ const endpoint = state?.kind === "compose" && state.restPort === getRestPort() && !shouldUseAgentmemoryUrl() ? `http://127.0.0.1:${state.restPort}` : getBaseUrl();
2573
+ const result = await withinStartupDeadline(startupDeadline(2e3), (signal) => probeEngine(endpoint, signal)).catch(() => null);
2574
+ return result?.completed === true && result.value;
1316
2575
  }
1317
2576
  async function isAgentmemoryReady() {
1318
- try {
1319
- const res = await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(2e3) });
1320
- if (!res.ok) return false;
1321
- try {
1322
- const data = await res.json();
1323
- if (typeof data.viewerPort === "number") {
1324
- discoveredViewerPort = data.viewerPort;
1325
- return true;
1326
- }
1327
- if (data.viewerSkipped) return true;
1328
- return false;
1329
- } catch {
1330
- return false;
1331
- }
1332
- } catch {
1333
- return false;
1334
- }
2577
+ const result = await withinStartupDeadline(startupDeadline(2e3), workerReadinessProbe).catch(() => null);
2578
+ return result?.completed === true && result.value;
1335
2579
  }
1336
- function findIiiConfig() {
1337
- const envPath = process.env["AGENTMEMORY_III_CONFIG"];
1338
- const candidates = [
1339
- ...envPath ? [envPath] : [],
1340
- join(process.cwd(), "iii-config.yaml"),
1341
- join(homedir(), ".agentmemory", "iii-config.yaml"),
1342
- join(__dirname, "iii-config.yaml"),
1343
- join(__dirname, "..", "iii-config.yaml")
1344
- ];
1345
- for (const c of candidates) if (existsSync(c)) return resolve(c);
1346
- return "";
2580
+ function workerReadinessProbe(signal) {
2581
+ return probeWorker(getBaseUrl(), signal, (port) => {
2582
+ discoveredViewerPort = port;
2583
+ });
1347
2584
  }
1348
2585
  function warnIfRelocatedDataDir() {
1349
2586
  if (!dataDirResolution.relocatedFrom) return;
@@ -1396,8 +2633,20 @@ function fallbackIiiPaths() {
1396
2633
  paths.push("/usr/local/bin/iii");
1397
2634
  return paths;
1398
2635
  }
2636
+ const III_VERSION_PROBE_TIMEOUTS_MS = [3e3, 3e4];
2637
+ function isProbeTimeout(err) {
2638
+ const e = err;
2639
+ return e?.code === "ETIMEDOUT" || e?.signal === "SIGTERM";
2640
+ }
2641
+ let iiiProbeScan = null;
1399
2642
  function iiiBinVersion(binPath) {
1400
- try {
2643
+ if (iiiProbeScan?.has(binPath)) return iiiProbeScan.get(binPath);
2644
+ const version = probeIiiVersion(binPath);
2645
+ iiiProbeScan?.set(binPath, version);
2646
+ return version;
2647
+ }
2648
+ function probeIiiVersion(binPath) {
2649
+ for (const timeout of III_VERSION_PROBE_TIMEOUTS_MS) try {
1401
2650
  const match = execFileSync(binPath, ["--version"], {
1402
2651
  encoding: "utf-8",
1403
2652
  stdio: [
@@ -1405,12 +2654,14 @@ function iiiBinVersion(binPath) {
1405
2654
  "pipe",
1406
2655
  "ignore"
1407
2656
  ],
1408
- timeout: 3e3
2657
+ timeout
1409
2658
  }).match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/);
1410
2659
  return match ? match[1] : null;
1411
- } catch {
1412
- return null;
2660
+ } catch (err) {
2661
+ if (!isProbeTimeout(err)) return null;
2662
+ vlog(`iii --version at ${binPath} timed out after ${timeout}ms`);
1413
2663
  }
2664
+ return null;
1414
2665
  }
1415
2666
  function resolveCompatibleIii(iiiBinPath) {
1416
2667
  if (!iiiBinPath) return null;
@@ -1431,15 +2682,6 @@ function enginePidfilePath() {
1431
2682
  function engineStatePath() {
1432
2683
  return runtimeMetadataPath("engine-state.json");
1433
2684
  }
1434
- function writeEnginePidfile(pid) {
1435
- try {
1436
- const pidPath = enginePidfilePath();
1437
- mkdirSync(dirname(pidPath), { recursive: true });
1438
- writeFileSync(pidPath, `${pid}\n`, { encoding: "utf-8" });
1439
- } catch (err) {
1440
- vlog(`writeEnginePidfile: ${err instanceof Error ? err.message : String(err)}`);
1441
- }
1442
- }
1443
2685
  function readEnginePidfile() {
1444
2686
  try {
1445
2687
  const pidStr = readFileSync(enginePidfilePath(), "utf-8").trim();
@@ -1471,28 +2713,11 @@ function clearWorkerPidfile() {
1471
2713
  unlinkSync(workerPidfilePath());
1472
2714
  } catch {}
1473
2715
  }
1474
- function writeEngineState(state) {
1475
- try {
1476
- const statePath = engineStatePath();
1477
- mkdirSync(dirname(statePath), { recursive: true });
1478
- const normalized = state.kind === "docker" && state.schemaVersion === 2 ? {
1479
- ...state,
1480
- restPort: state.restPort ?? getRestPort(),
1481
- dataDir: state.dataDir ?? dataDirResolution.dataDir
1482
- } : {
1483
- ...state,
1484
- restPort: state.restPort ?? getRestPort()
1485
- };
1486
- writeFileSync(statePath, `${JSON.stringify(normalized)}\n`, { encoding: "utf-8" });
1487
- } catch (err) {
1488
- vlog(`writeEngineState: ${err instanceof Error ? err.message : String(err)}`);
1489
- }
1490
- }
1491
2716
  function readEngineState() {
1492
2717
  try {
1493
2718
  const raw = readFileSync(engineStatePath(), "utf-8");
1494
2719
  const parsed = JSON.parse(raw);
1495
- if (parsed && (parsed.kind === "native" || parsed.kind === "docker")) return parsed;
2720
+ if (parsed && (parsed.kind === "native" || parsed.kind === "docker" || parsed.kind === "compose")) return parsed;
1496
2721
  return null;
1497
2722
  } catch {
1498
2723
  return null;
@@ -1505,271 +2730,138 @@ function clearEngineState() {
1505
2730
  }
1506
2731
  function engineStateRestPort(state) {
1507
2732
  if (state.restPort && Number.isFinite(state.restPort)) return state.restPort;
1508
- if (state.kind === "docker") {
1509
- const projectPort = state.projectName?.match(/^agentmemory-(\d+)$/)?.[1];
1510
- if (projectPort) return parseInt(projectPort, 10);
1511
- return 3111;
1512
- }
2733
+ if (state.kind === "compose") return 3111;
1513
2734
  try {
1514
2735
  const configuredPort = (readFileSync(state.configPath, "utf-8").match(/- name:\s*(?:iii-)?http([\s\S]*?)(?=\n\s*- name:|$)/)?.[1])?.match(/\n\s*port:\s*(\d+)/)?.[1];
1515
2736
  if (configuredPort) return parseInt(configuredPort, 10);
1516
2737
  } catch {}
1517
2738
  return 3111;
1518
2739
  }
1519
- async function startWorkerForEngineState() {
1520
- const workerPid = readWorkerPidfile();
1521
- if (workerPid && pidAlive(workerPid)) return;
1522
- if (configuredEngineMayStartWorker() && await waitForConfiguredWorker(5e3)) return;
1523
- await import("./src-Jsq9LCeH.mjs");
2740
+ async function startWorkerForEngineState(background = false) {
2741
+ if (readEngineState()?.kind === "compose") {
2742
+ const timeoutMs = startupTimeouts().workerMs;
2743
+ if (!await waitForReadiness(startupDeadline(timeoutMs), workerReadinessProbe, { stopped: () => {
2744
+ const state = readEngineState();
2745
+ if (state?.kind === "compose") try {
2746
+ const failure = composeRegistryFailureMessage(readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0), state.workerBinDir);
2747
+ if (failure) startupFailure = {
2748
+ kind: "engine-crashed",
2749
+ stderr: failure
2750
+ };
2751
+ } catch {}
2752
+ return startupInterrupted || engineStartupFailed() || Boolean(startupFailure?.stderr?.startsWith("Worker registry unavailable"));
2753
+ } })) {
2754
+ printComposeStartupLogs();
2755
+ const state = readEngineState();
2756
+ let registryFailure = state?.kind === "compose" ? composeStateRegistryFailure(dataDirResolution.dataDir, state.file) : null;
2757
+ if (state?.kind === "compose" && !registryFailure) registryFailure = (() => {
2758
+ try {
2759
+ return composeRegistryFailureMessage(readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0), state.workerBinDir);
2760
+ } catch {
2761
+ return null;
2762
+ }
2763
+ })();
2764
+ if (state?.kind === "compose" && await isEngineRunning()) try {
2765
+ const failures = (ownedComposeProject(await composeCall({
2766
+ engineUrl: state.engineUrl,
2767
+ namespace: state.namespace,
2768
+ functionId: "compose::status",
2769
+ file: state.file
2770
+ }), state.file)?.containers ?? []).filter((container) => container.state === "failed" || container.last_error).map((container) => {
2771
+ let detail = container.last_error ?? container.state ?? "failed";
2772
+ if (container.log_path) try {
2773
+ const log = readFileSync(container.log_path, "utf8").trim();
2774
+ if (log) detail += `\n${log.split(/\r?\n/).slice(-12).join("\n")}`;
2775
+ } catch {}
2776
+ return `${container.container ?? "worker"}: ${detail}`;
2777
+ });
2778
+ if (failures.length) {
2779
+ state.workerFailure = failures.join("\n");
2780
+ writeComposeState(engineStatePath(), {
2781
+ ...state,
2782
+ workerVersions: state.workerVersions ?? (state.workerBinDir ? { local: true } : COMPOSE_WORKER_VERSIONS)
2783
+ });
2784
+ const aggregate = composeRegistryFailureMessage(state.workerFailure, state.workerBinDir);
2785
+ if (aggregate) registryFailure = `${aggregate} Compose reported: ${state.workerFailure}`;
2786
+ else {
2787
+ const logTail = readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0).trim().split(/\r?\n/).slice(-12).join("\n");
2788
+ registryFailure = `Worker Compose failed: ${state.workerFailure}${logTail ? `\nRecent Compose output:\n${logTail}` : ""}`;
2789
+ }
2790
+ }
2791
+ } catch {}
2792
+ throw new Error(registryFailure || startupFailure?.stderr || startupTimeoutMessage("worker", timeoutMs));
2793
+ }
2794
+ composeStartupCompleted = true;
2795
+ finishStartup();
2796
+ return;
2797
+ }
2798
+ const timeoutMs = startupTimeouts().workerMs;
2799
+ startupWorkerFailure = null;
2800
+ if (!await startWorkerAndWait({
2801
+ timeoutMs,
2802
+ hasLiveWorker: () => {
2803
+ const workerPid = readWorkerPidfile();
2804
+ return Boolean(workerPid && pidAlive(workerPid));
2805
+ },
2806
+ engineStartsWorker: configuredEngineMayStartWorker,
2807
+ startWorker: async () => {
2808
+ if (background) {
2809
+ const builtEntry = join(__dirname, "index.mjs");
2810
+ const workerArgs = existsSync(builtEntry) ? [builtEntry] : [
2811
+ "--import",
2812
+ "tsx",
2813
+ join(__dirname, "index.ts")
2814
+ ];
2815
+ const child = spawn(process.execPath, workerArgs, {
2816
+ detached: true,
2817
+ stdio: "ignore",
2818
+ windowsHide: true,
2819
+ env: { ...process.env }
2820
+ });
2821
+ spawnedStartupWorker = child;
2822
+ child.on("error", (error) => {
2823
+ if (spawnedStartupWorker === child) startupWorkerFailure = error;
2824
+ });
2825
+ child.on("exit", (code, signal) => {
2826
+ if (spawnedStartupWorker === child) startupWorkerFailure = /* @__PURE__ */ new Error(`agentmemory worker exited during startup (${signal ?? code}).`);
2827
+ });
2828
+ child.unref();
2829
+ } else {
2830
+ importedStartupWorker = true;
2831
+ await import("./src-C23nMpda.mjs");
2832
+ }
2833
+ },
2834
+ probe: workerReadinessProbe,
2835
+ stopped: () => startupInterrupted || engineStartupFailed() || startupWorkerFailure !== null
2836
+ })) {
2837
+ if (startupWorkerFailure) throw startupWorkerFailure;
2838
+ if (engineStartupFailed()) throw new Error("iii-engine exited before the agentmemory worker became ready.");
2839
+ throw new Error(startupTimeoutMessage("worker", timeoutMs));
2840
+ }
2841
+ finishStartup();
1524
2842
  }
1525
2843
  function configuredEngineMayStartWorker() {
2844
+ const explicit = startupTimeouts().engineManagesWorker;
2845
+ if (explicit !== void 0) return explicit;
1526
2846
  const state = readEngineState();
1527
- if (state?.kind !== "native") return false;
2847
+ if (state?.kind !== "native" || state.attached) return false;
1528
2848
  try {
1529
- return readFileSync(state.configPath, "utf-8").includes("- name: iii-exec");
2849
+ return configStartsAgentmemoryWorker(readFileSync(state.configPath, "utf-8"), join(__dirname, "index.mjs"), state.cwd);
1530
2850
  } catch {
1531
2851
  return false;
1532
2852
  }
1533
2853
  }
1534
- async function waitForConfiguredWorker(timeoutMs) {
1535
- const deadline = Date.now() + timeoutMs;
1536
- while (Date.now() < deadline) {
1537
- const workerPid = readWorkerPidfile();
1538
- if (workerPid && pidAlive(workerPid)) return true;
1539
- if (await isAgentmemoryReady()) return true;
1540
- await new Promise((resolve) => setTimeout(resolve, 100));
1541
- }
1542
- return false;
1543
- }
1544
- function discoverComposeFile() {
1545
- return [
1546
- join(__dirname, "..", "docker-compose.yml"),
1547
- join(__dirname, "docker-compose.yml"),
1548
- join(process.cwd(), "docker-compose.yml")
1549
- ].find((c) => existsSync(c)) ?? null;
1550
- }
1551
- function sameDockerMountSource(type, left, right) {
1552
- return type === "bind" ? resolve(left) === resolve(right) : left === right;
1553
- }
1554
- function inspectDockerContainer(dockerBin, containerId, state, ownerPort) {
1555
- const result = spawnSync(dockerBin, ["inspect", containerId], {
1556
- encoding: "utf-8",
1557
- stdio: [
1558
- "ignore",
1559
- "pipe",
1560
- "ignore"
1561
- ],
1562
- maxBuffer: 1024 * 1024
1563
- });
1564
- if (result.status !== 0) return {
1565
- status: "unavailable",
1566
- reason: `docker inspect failed for ${containerId}`
1567
- };
1568
- let record;
1569
- try {
1570
- record = JSON.parse(result.stdout)[0] ?? {};
1571
- } catch {
1572
- return {
1573
- status: "unavailable",
1574
- reason: `docker inspect returned invalid JSON for ${containerId}`
1575
- };
1576
- }
1577
- const image = record.Config?.Image ?? "";
1578
- const expectedImage = `iiidev/iii:${state.engineVersion ?? IIPINNED_VERSION}`;
1579
- if (image !== expectedImage) return {
1580
- status: "unavailable",
1581
- reason: `saved container uses ${image || "an unknown image"}, expected ${expectedImage}`
1582
- };
1583
- const labels = record.Config?.Labels ?? {};
1584
- const projectName = labels["com.docker.compose.project"] ?? "";
1585
- if (labels["com.docker.compose.service"] !== "iii-engine" || !projectName) return {
1586
- status: "unavailable",
1587
- reason: "saved container is not the iii-engine service of a Compose project"
1588
- };
1589
- if (state.projectName && state.projectName !== projectName) return {
1590
- status: "unavailable",
1591
- reason: `saved Docker project ${state.projectName} does not match container project ${projectName}`
1592
- };
1593
- if (state.schemaVersion === 2 && projectName !== dockerProjectName(ownerPort)) return {
1594
- status: "unavailable",
1595
- reason: `Docker project ${projectName} does not match REST port ${ownerPort}`
1596
- };
1597
- if (!(record.HostConfig?.PortBindings?.[`${ownerPort}/tcp`] ?? []).some((binding) => binding.HostPort === String(ownerPort))) return {
1598
- status: "unavailable",
1599
- reason: `saved container does not publish REST port ${ownerPort}`
1600
- };
1601
- const dataMount = record.Mounts?.find((mount) => mount.Destination === "/data");
1602
- const dataMountType = dataMount?.Type;
1603
- const dataMountSource = dataMountType === "volume" ? dataMount?.Name ?? dataMount?.Source ?? "" : dataMount?.Source ?? "";
1604
- if (dataMountType !== "bind" && dataMountType !== "volume" || !dataMountSource) return {
1605
- status: "unavailable",
1606
- reason: "saved container has no verifiable /data bind or volume mount"
1607
- };
1608
- if (state.dataMountType && state.dataMountType !== dataMountType) return {
1609
- status: "unavailable",
1610
- reason: `saved /data mount type ${state.dataMountType} changed to ${dataMountType}`
1611
- };
1612
- if (state.dataMountSource && !sameDockerMountSource(dataMountType, state.dataMountSource, dataMountSource)) return {
1613
- status: "unavailable",
1614
- reason: "saved container /data mount source changed"
1615
- };
1616
- if (!state.dataMountSource) {
1617
- if (dataMountType === "bind" && resolve(dataMountSource) !== resolve(dataDirResolution.dataDir)) return {
1618
- status: "unavailable",
1619
- reason: `container /data bind ${dataMountSource} does not match ${dataDirResolution.dataDir}`
1620
- };
1621
- if (dataMountType === "volume" && dataDirResolution.source !== "default") return {
1622
- status: "unavailable",
1623
- reason: "legacy container uses a named volume but this invocation selects an explicit data directory"
1624
- };
1625
- }
1626
- if (state.dataDir && resolve(state.dataDir) !== resolve(dataDirResolution.dataDir)) return {
1627
- status: "unavailable",
1628
- reason: `saved data directory ${state.dataDir} does not match ${dataDirResolution.dataDir}`
1629
- };
1630
- return {
1631
- status: record.State?.Running ? "running" : "stopped",
1632
- composeFile: existsSync(state.composeFile) ? state.composeFile : discoverComposeFile() ?? state.composeFile,
1633
- projectName,
1634
- containerId: record.Id ?? containerId,
1635
- dataMountType,
1636
- dataMountSource,
1637
- preserveContainer: state.preserveContainer === true || state.schemaVersion !== 2 && dataMountType === "volume"
1638
- };
1639
- }
1640
- function inspectOwnedDockerEngine(state) {
1641
- if (state.kind !== "docker") return {
1642
- status: "unavailable",
1643
- reason: "engine state is not Docker"
1644
- };
1645
- const ownerPort = engineStateRestPort(state);
1646
- if (state.schemaVersion === 2 && state.projectName !== dockerProjectName(ownerPort)) return {
1647
- status: "unavailable",
1648
- reason: `saved Docker project ${state.projectName} does not match REST port ${ownerPort}`
1649
- };
1650
- const dockerBin = whichBinary("docker");
1651
- if (!dockerBin) return {
1652
- status: "unavailable",
1653
- reason: "docker is not on PATH"
1654
- };
1655
- const discoveryFailures = [];
1656
- if (state.containerId) {
1657
- const direct = inspectDockerContainer(dockerBin, state.containerId, state, ownerPort);
1658
- if (direct.status !== "unavailable") return direct;
1659
- if (state.preserveContainer) return direct;
1660
- discoveryFailures.push(direct.reason);
1661
- }
1662
- const composeFile = existsSync(state.composeFile) ? state.composeFile : discoverComposeFile() ?? state.composeFile;
1663
- const candidateIds = /* @__PURE__ */ new Set();
1664
- try {
1665
- if (existsSync(composeFile)) {
1666
- const composeText = readFileSync(composeFile, "utf-8");
1667
- if (!/^\s+iii-engine:/m.test(composeText)) return {
1668
- status: "unavailable",
1669
- reason: `${composeFile} does not define iii-engine`
1670
- };
1671
- const psResult = spawnSync(dockerBin, dockerComposeArgs(composeFile, state.projectName, [
1672
- "ps",
1673
- "-q",
1674
- "--all",
1675
- "iii-engine"
1676
- ]), {
1677
- encoding: "utf-8",
1678
- stdio: [
1679
- "ignore",
1680
- "pipe",
1681
- "ignore"
1682
- ]
1683
- });
1684
- if (psResult.status === 0) {
1685
- for (const id of (psResult.stdout ?? "").trim().split(/\s+/)) if (id) candidateIds.add(id);
1686
- } else discoveryFailures.push(`docker compose ps failed for project ${state.projectName ?? "<default>"}`);
1687
- }
1688
- const scanResult = spawnSync(dockerBin, [
1689
- "ps",
1690
- "-aq",
1691
- "--filter",
1692
- "label=com.docker.compose.service=iii-engine",
1693
- "--filter",
1694
- `ancestor=iiidev/iii:${state.engineVersion ?? IIPINNED_VERSION}`,
1695
- ...state.projectName ? ["--filter", `label=com.docker.compose.project=${state.projectName}`] : []
1696
- ], {
1697
- encoding: "utf-8",
1698
- stdio: [
1699
- "ignore",
1700
- "pipe",
1701
- "ignore"
1702
- ]
1703
- });
1704
- if (scanResult.status === 0) {
1705
- for (const id of (scanResult.stdout ?? "").trim().split(/\s+/)) if (id) candidateIds.add(id);
1706
- } else discoveryFailures.push("docker container scan failed");
1707
- const matches = [];
1708
- for (const containerId of candidateIds) {
1709
- const inspected = inspectDockerContainer(dockerBin, containerId, state, ownerPort);
1710
- if (inspected.status === "running" || inspected.status === "stopped") {
1711
- if (!matches.some((match) => (match.status === "running" || match.status === "stopped") && match.containerId === inspected.containerId)) matches.push(inspected);
1712
- } else if (inspected.status === "unavailable") discoveryFailures.push(inspected.reason);
1713
- }
1714
- if (matches.length === 1) return matches[0];
1715
- if (matches.length > 1) return {
1716
- status: "unavailable",
1717
- reason: `multiple Docker iii-engine containers match REST port ${ownerPort}`
1718
- };
1719
- if (discoveryFailures.length > 0) return {
1720
- status: "unavailable",
1721
- reason: [...new Set(discoveryFailures)].join("; ")
1722
- };
1723
- if (state.schemaVersion !== 2 || state.preserveContainer) return {
1724
- status: "unavailable",
1725
- reason: `legacy Docker ownership exists for REST port ${ownerPort}, but no unique container can be verified`
1726
- };
1727
- return {
1728
- status: "missing",
1729
- composeFile,
1730
- projectName: state.projectName
1731
- };
1732
- } catch (err) {
1733
- return {
1734
- status: "unavailable",
1735
- reason: err instanceof Error ? err.message : String(err)
1736
- };
1737
- }
1738
- }
1739
- function persistDockerInspection(state, inspection) {
1740
- if (inspection.status !== "running" && inspection.status !== "stopped") return state;
1741
- const resolved = {
1742
- ...state,
1743
- composeFile: inspection.composeFile,
1744
- projectName: inspection.projectName,
1745
- containerId: inspection.containerId,
1746
- dataMountType: inspection.dataMountType,
1747
- dataMountSource: inspection.dataMountSource,
1748
- preserveContainer: inspection.preserveContainer
1749
- };
1750
- writeEngineState(resolved);
1751
- return resolved;
1752
- }
1753
2854
  async function assertRuntimePortOwnership() {
1754
2855
  const state = readEngineState();
1755
2856
  if (!state) return;
1756
- const ownerPort = engineStateRestPort(state);
1757
- const requestedPort = getRestPort();
1758
- if (ownerPort === requestedPort) return;
1759
2857
  if (state.kind === "docker") {
1760
- const inspection = inspectOwnedDockerEngine(state);
1761
- if (inspection.status === "unavailable") {
1762
- p.log.error(`The data directory contains Docker ownership for REST port ${ownerPort}, but it cannot be verified: ${inspection.reason}. Refusing to overwrite its lifecycle state.`);
1763
- process.exit(1);
1764
- }
1765
- if (inspection.status === "running" || inspection.status === "stopped") {
1766
- persistDockerInspection(state, inspection);
1767
- p.log.error(`The canonical lifecycle state is owned by a ${inspection.status} Docker engine on REST port ${ownerPort}. Refusing to reuse it for port ${requestedPort}. Use the original port, or use --instance <N> for an isolated daemon.`);
1768
- process.exit(1);
1769
- }
1770
- p.log.error(`The canonical lifecycle state still records a Docker engine for REST port ${ownerPort}, but its container is missing. Refusing to overwrite that ownership from port ${requestedPort}. Re-run \`agentmemory stop\` with the original port to reconcile it.`);
2858
+ p.log.error(legacyDockerStateMessage(state));
1771
2859
  process.exit(1);
1772
2860
  }
2861
+ const ownerPort = engineStateRestPort(state);
2862
+ if (state.kind === "compose" && ownerPort !== getRestPort()) return;
2863
+ const requestedPort = getRestPort();
2864
+ if (ownerPort === requestedPort) return;
1773
2865
  const enginePid = readEnginePidfile();
1774
2866
  const workerPid = readWorkerPidfile();
1775
2867
  let active = Boolean(enginePid && pidAlive(enginePid) && !isForeignPortHolder(pidCommand(enginePid)) || workerPid && pidAlive(workerPid));
@@ -1785,14 +2877,6 @@ async function assertRuntimePortOwnership() {
1785
2877
  clearWorkerPidfile();
1786
2878
  clearEngineState();
1787
2879
  }
1788
- function configureDockerHostUser() {
1789
- if (typeof process.getuid !== "function" || typeof process.getgid !== "function") return;
1790
- const hostUid = String(process.getuid());
1791
- const hostGid = String(process.getgid());
1792
- process.env["AGENTMEMORY_DOCKER_UID"] ??= hostUid;
1793
- process.env["AGENTMEMORY_DOCKER_GID"] ??= hostGid;
1794
- if (process.env["AGENTMEMORY_DOCKER_UID"] === hostUid && process.env["AGENTMEMORY_DOCKER_GID"] === hostGid) process.env["AGENTMEMORY_DOCKER_SKIP_CHOWN"] ??= "1";
1795
- }
1796
2880
  function isInvokedViaNpx() {
1797
2881
  if (process.env["npm_lifecycle_event"] === "npx") return true;
1798
2882
  if ((process.argv[1] ?? "").includes("_npx")) return true;
@@ -1824,49 +2908,46 @@ async function maybeOfferGlobalInstall() {
1824
2908
  if (runCommand(npmBin, [
1825
2909
  "install",
1826
2910
  "-g",
1827
- `@agentmemory/agentmemory@0.9.30`
1828
- ], { label: `Installing @agentmemory/agentmemory@0.9.30 globally` })) {
2911
+ `@agentmemory/agentmemory@0.9.32`
2912
+ ], { label: `Installing @agentmemory/agentmemory@0.9.32 globally` })) {
1829
2913
  p.log.success("Installed globally. `agentmemory stop` etc. will now work in new shells.");
1830
2914
  writePrefs({ skipGlobalInstall: true });
1831
2915
  } else p.log.warn("Global install failed. Try manually: npm install -g @agentmemory/agentmemory");
1832
2916
  }
1833
- function adoptRunningEngine() {
1834
- try {
1835
- const existingState = readEngineState();
1836
- const existingPid = readEnginePidfile();
1837
- if (existingState && existingPid) return;
1838
- const enginePid = findEnginePidsByPort(getRestPort())[0];
1839
- if (enginePid) {
1840
- const comm = pidCommand(enginePid);
1841
- if (isForeignPortHolder(comm)) {
1842
- vlog(`adoptRunningEngine: refusing to adopt pid ${enginePid} (${comm}) — not the iii engine binary`);
1843
- return;
1844
- }
1845
- }
1846
- if (enginePid && !existingPid) writeEnginePidfile(enginePid);
1847
- if (!existingState) writeEngineState({
1848
- kind: "native",
1849
- configPath: findIiiConfig() || "",
1850
- attached: true
1851
- });
1852
- if (enginePid && !existingPid) p.log.info(c.ok(`Attached to existing iii-engine (pid ${enginePid})`));
1853
- } catch (err) {
1854
- vlog(`adoptRunningEngine: ${err instanceof Error ? err.message : String(err)}`);
1855
- }
1856
- }
1857
2917
  async function runIiiInstaller() {
1858
2918
  const releaseUrl = iiiReleaseUrl();
1859
2919
  const asset = iiiReleaseAsset();
1860
2920
  const isZipAsset = asset?.endsWith(".zip") === true;
1861
2921
  if (!releaseUrl) {
1862
- p.log.warn(`iii-engine binary not available for ${platform()}/${process.arch}. Use Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
2922
+ p.log.warn(`iii-engine binary not available for ${platform()}/${process.arch}. Download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
1863
2923
  return {
1864
2924
  ok: false,
1865
2925
  binPath: null
1866
2926
  };
1867
2927
  }
1868
2928
  if (IS_WINDOWS || isZipAsset) {
1869
- p.log.info(`Auto-install unavailable on ${platform()} — ${asset} isn't tar-compatible. Install manually:\n 1. Download ${releaseUrl}\n Its SHA-256 must be ${expectedIiiSha256(IIPINNED_VERSION, asset ?? "") ?? `the value in ${releaseUrl.replace(/\.zip$/, ".sha256")}`}\n 2. Extract iii.exe and place it on PATH (e.g. %USERPROFILE%\\.local\\bin)\nOr use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`);
2929
+ const binDir = agentmemoryBinDir();
2930
+ const binPath = privateIiiPath();
2931
+ const label = `Installing iii-engine v${IIPINNED_VERSION} (pinned)`;
2932
+ const spinner = p.spinner();
2933
+ spinner.start(label);
2934
+ const outcome = await installIiiZip({
2935
+ releaseUrl,
2936
+ version: IIPINNED_VERSION,
2937
+ asset: asset ?? "",
2938
+ binDir,
2939
+ binPath
2940
+ });
2941
+ if (outcome.ok) {
2942
+ spinner.stop(`${label} ${pc.green("✓")}`);
2943
+ iiiProbeScan?.delete(binPath);
2944
+ return {
2945
+ ok: true,
2946
+ binPath
2947
+ };
2948
+ }
2949
+ spinner.stop(`${label} ${pc.red("✗")}`);
2950
+ p.log.warn(`iii-engine auto-install failed: ${outcome.detail.slice(0, 300)}\nInstall it manually, then re-run agentmemory:\n 1. Download ${releaseUrl}\n Its SHA-256 must be ${expectedIiiSha256(IIPINNED_VERSION, asset ?? "") ?? `the value in ${releaseUrl.replace(/\.zip$/, ".sha256")}`}\n 2. Extract iii.exe into ${binDir}`);
1870
2951
  return {
1871
2952
  ok: false,
1872
2953
  binPath: null
@@ -1905,13 +2986,14 @@ async function runIiiInstaller() {
1905
2986
  const sha256 = expectedIiiSha256(IIPINNED_VERSION, asset ?? "");
1906
2987
  const heading = outcome.reason === "checksum" ? "iii-engine download failed verification" : outcome.reason === "no-checksum" ? "iii-engine auto-install skipped" : "iii-engine download failed";
1907
2988
  const checksumHint = sha256 ? "" : `Compare the archive against ${releaseUrl.replace(/\.(tar\.gz|zip)$/, ".sha256")} before extracting.\n`;
1908
- p.log.warn(`${heading}: ${outcome.detail.slice(0, 300)}\nInstall it manually, then re-run agentmemory:\n ${iiiManualInstallCommand(releaseUrl, binDir, binPath, sha256)}\n` + checksumHint + `Or use Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`);
2989
+ p.log.warn(`${heading}: ${outcome.detail.slice(0, 300)}\nInstall it manually, then re-run agentmemory:\n ${iiiManualInstallCommand(releaseUrl, binDir, binPath, sha256)}\n` + checksumHint);
1909
2990
  return {
1910
2991
  ok: false,
1911
2992
  binPath: null
1912
2993
  };
1913
2994
  }
1914
2995
  spinner.stop(`${label} ${pc.green("✓")}`);
2996
+ iiiProbeScan?.delete(binPath);
1915
2997
  return {
1916
2998
  ok: true,
1917
2999
  binPath
@@ -1919,159 +3001,312 @@ async function runIiiInstaller() {
1919
3001
  }
1920
3002
  let startupFailure = null;
1921
3003
  let activeStartupStderr = createStartupStderrCapture();
3004
+ let spawnedNativeEngine = null;
3005
+ let spawnedComposeDaemon = null;
3006
+ let composeStartupCompleted = false;
3007
+ let importedStartupWorker = false;
3008
+ let spawnedStartupWorker = null;
3009
+ let startupWorkerFailure = null;
3010
+ let engineReadinessDeadline = null;
3011
+ let startupInterrupted = false;
3012
+ let finishStartup = () => {};
3013
+ function beginStartup() {
3014
+ finishStartup();
3015
+ startupInterrupted = false;
3016
+ const unregister = registerStartupCleanup(cleanupFailedStartup);
3017
+ const interrupt = async (exitCode) => {
3018
+ startupInterrupted = true;
3019
+ process.exit(await runStartupCleanup(exitCode).catch(() => exitCode));
3020
+ };
3021
+ const onInterrupt = () => {
3022
+ interrupt(130);
3023
+ };
3024
+ const onTerminate = () => {
3025
+ interrupt(143);
3026
+ };
3027
+ process.on("SIGINT", onInterrupt);
3028
+ process.on("SIGTERM", onTerminate);
3029
+ finishStartup = () => {
3030
+ process.off("SIGINT", onInterrupt);
3031
+ process.off("SIGTERM", onTerminate);
3032
+ unregister();
3033
+ finishStartup = () => {};
3034
+ };
3035
+ }
3036
+ function engineStartupFailed() {
3037
+ return startupFailure?.kind === "engine-crashed";
3038
+ }
3039
+ async function cleanupFailedStartup() {
3040
+ engineReadinessDeadline = null;
3041
+ if (importedStartupWorker && readWorkerPidfile() === process.pid) clearWorkerPidfile();
3042
+ const worker = spawnedStartupWorker;
3043
+ spawnedStartupWorker = null;
3044
+ if (worker && typeof worker.pid === "number") {
3045
+ if (worker.exitCode === null && worker.signalCode === null && !await signalAndWait(worker.pid, "SIGTERM", 3e3)) {
3046
+ p.log.warn(`Started worker pid ${worker.pid} survived shutdown; lifecycle state was preserved.`);
3047
+ return;
3048
+ }
3049
+ if (readWorkerPidfile() === worker.pid) clearWorkerPidfile();
3050
+ }
3051
+ const composeChild = spawnedComposeDaemon;
3052
+ spawnedComposeDaemon = null;
3053
+ if (composeChild && typeof composeChild.pid === "number") if (!await signalAndWait(composeChild.pid, "SIGTERM", 2e4)) p.log.warn(`Compose daemon pid ${composeChild.pid} survived shutdown; lifecycle state was preserved.`);
3054
+ else clearEngineState();
3055
+ const child = spawnedNativeEngine;
3056
+ spawnedNativeEngine = null;
3057
+ if (!child || typeof child.pid !== "number") return;
3058
+ if (!IS_WINDOWS) try {
3059
+ process.kill(-child.pid, "SIGTERM");
3060
+ } catch {}
3061
+ if (child.exitCode === null && child.signalCode === null) {
3062
+ if (!await signalAndWait(child.pid, "SIGTERM", 3e3)) {
3063
+ p.log.warn(`Started engine pid ${child.pid} survived shutdown; lifecycle state was preserved.`);
3064
+ return;
3065
+ }
3066
+ }
3067
+ if (readEnginePidfile() === child.pid) {
3068
+ clearEnginePidfile();
3069
+ clearEngineState();
3070
+ }
3071
+ }
1922
3072
  function printCapturedStartupStderr() {
1923
3073
  const stderr = redactCredentialUrls(activeStartupStderr.text().trim());
1924
3074
  if (IS_VERBOSE && stderr) p.note(stderr, "engine stderr");
1925
3075
  }
1926
- function printDockerStartupLogs() {
3076
+ function readComposeStartupLogs() {
1927
3077
  const state = readEngineState();
1928
- if (state?.kind !== "docker") return;
1929
- const inspection = inspectOwnedDockerEngine(state);
1930
- if (inspection.status !== "running" && inspection.status !== "stopped") return;
1931
- const dockerBin = whichBinary("docker");
1932
- if (!dockerBin) return;
1933
- const result = spawnSync(dockerBin, [
1934
- "logs",
1935
- "--tail",
1936
- "80",
1937
- inspection.containerId
1938
- ], {
1939
- encoding: "utf-8",
1940
- stdio: [
1941
- "ignore",
1942
- "pipe",
1943
- "pipe"
1944
- ],
1945
- timeout: 5e3,
1946
- maxBuffer: 64 * 1024
1947
- });
1948
- const output = redactCredentialUrls(`${result.stdout ?? ""}${result.stderr ?? ""}`.trim());
1949
- if (output) p.note(output.slice(-16 * 1024), "docker engine logs");
3078
+ if (state?.kind !== "compose") return null;
3079
+ try {
3080
+ const text = readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0).trim();
3081
+ return {
3082
+ path: state.logFile,
3083
+ text: redactCredentialUrls(text.slice(-16 * 1024))
3084
+ };
3085
+ } catch {
3086
+ return {
3087
+ path: state.logFile,
3088
+ text: "(Compose log file could not be read.)"
3089
+ };
3090
+ }
1950
3091
  }
1951
- function spawnEngineBackground(bin, spawnArgs, label, cwd) {
1952
- vlog(`spawn: ${bin} ${spawnArgs.join(" ")}${cwd ? ` (cwd: ${cwd})` : ""}`);
1953
- activeStartupStderr = createStartupStderrCapture();
1954
- const child = spawn(bin, spawnArgs, {
1955
- detached: true,
1956
- stdio: [
1957
- "ignore",
1958
- "ignore",
1959
- "pipe"
1960
- ],
1961
- windowsHide: true,
1962
- env: engineChildEnv(process.env),
1963
- ...cwd ? { cwd } : {}
1964
- });
1965
- const isDocker = label.includes("Docker");
1966
- if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
1967
- child.stderr?.on("data", (chunk) => {
1968
- activeStartupStderr.append(chunk);
1969
- });
1970
- child.on("exit", (code, signal) => {
1971
- if (code !== null && code !== 0 || code === null && signal !== null) {
1972
- const stderr = redactCredentialUrls(activeStartupStderr.text());
1973
- startupFailure = {
1974
- kind: isDocker ? "docker-crashed" : "engine-crashed",
1975
- stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
1976
- binary: bin
1977
- };
1978
- vlog(`engine exited early: code=${code} signal=${signal}`);
1979
- if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
1980
- if (!isDocker) {
1981
- clearEnginePidfile();
1982
- clearEngineState();
1983
- }
1984
- }
1985
- });
1986
- child.unref();
1987
- return child;
3092
+ function printComposeStartupLogs() {
3093
+ const logs = readComposeStartupLogs();
3094
+ if (!logs) return;
3095
+ if (logs.text) p.note(`Log file: ${logs.path}\n\n${logs.text}`, "Recent Worker Compose output");
1988
3096
  }
1989
- function prepareEngineLaunch(configPath) {
1990
- const home = homedir();
1991
- const bundledConfig = isBundledConfig(configPath, __dirname);
1992
- const cwd = resolveEngineCwd(configPath, process.cwd(), home, bundledConfig);
1993
- const stateBackendKind = getStateBackend();
3097
+ function composePidMatches(pid, state) {
3098
+ return composePidIdentity(pid, state) === "match";
3099
+ }
3100
+ function composePidIdentity(pid, state) {
3101
+ return classifyComposeProcess(inspectComposeProcess(pid), state, IS_WINDOWS);
3102
+ }
3103
+ function inspectComposeProcess(pid) {
1994
3104
  try {
1995
- mkdirSync(cwd, { recursive: true });
1996
- } catch (err) {
1997
- const failure = resolveLaunchRenderFailure(stateBackendKind, err);
1998
- if (failure.fatal) {
1999
- p.log.error(failure.message);
2000
- process.exit(1);
3105
+ let command = "";
3106
+ let executable = "";
3107
+ if (IS_WINDOWS) {
3108
+ const result = execFileSync("powershell.exe", [
3109
+ "-NoProfile",
3110
+ "-NonInteractive",
3111
+ "-Command",
3112
+ `$process = Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'; if ($process) { @{ command = $process.CommandLine; executable = $process.ExecutablePath } | ConvertTo-Json -Compress }`
3113
+ ], {
3114
+ encoding: "utf8",
3115
+ stdio: [
3116
+ "ignore",
3117
+ "pipe",
3118
+ "ignore"
3119
+ ]
3120
+ }).trim();
3121
+ const processInfo = JSON.parse(result);
3122
+ command = processInfo.command ?? "";
3123
+ executable = processInfo.executable ?? "";
3124
+ } else if (platform() === "darwin") {
3125
+ command = execFileSync("ps", [
3126
+ "-p",
3127
+ String(pid),
3128
+ "-o",
3129
+ "command="
3130
+ ], {
3131
+ encoding: "utf8",
3132
+ stdio: [
3133
+ "ignore",
3134
+ "pipe",
3135
+ "ignore"
3136
+ ]
3137
+ }).trim();
3138
+ executable = readUnixProcessExecutable(pid, "darwin");
3139
+ } else {
3140
+ command = execFileSync("ps", [
3141
+ "-p",
3142
+ String(pid),
3143
+ "-o",
3144
+ "command="
3145
+ ], {
3146
+ encoding: "utf8",
3147
+ stdio: [
3148
+ "ignore",
3149
+ "pipe",
3150
+ "ignore"
3151
+ ]
3152
+ }).trim();
3153
+ executable = readUnixProcessExecutable(pid, "linux");
2001
3154
  }
2002
3155
  return {
2003
- configPath,
2004
- cwd: process.cwd()
3156
+ command,
3157
+ executable
2005
3158
  };
3159
+ } catch {
3160
+ return null;
2006
3161
  }
3162
+ }
3163
+ function composeChildProcesses(state) {
3164
+ if (IS_WINDOWS || !state.composePid) return [];
2007
3165
  try {
2008
- const rawConfig = readFileSync(configPath, "utf-8");
2009
- const options = {
2010
- dataDir: dataDirResolution.dataDir,
2011
- ports: {
2012
- restPort: getRestPort(),
2013
- streamPort: getStreamPort(),
2014
- viewerPort: getConfiguredViewerPort(),
2015
- enginePort: getEnginePort()
2016
- },
2017
- stateBackend: {
2018
- kind: stateBackendKind,
2019
- redisUrl: getRedisUrl()
2020
- },
2021
- saveIntervalMs: getStateSaveIntervalMs()
2022
- };
2023
- const rewritten = bundledConfig ? rewriteBundledConfig(rawConfig, home, process.execPath, join(__dirname, "index.mjs"), options) : renderEngineConfig(rawConfig, options);
2024
- const runtimePath = runtimeConfigPath(dataDirResolution.dataDir);
2025
- mkdirSync(dirname(runtimePath), { recursive: true });
2026
- writeFileSync(runtimePath, rewritten, "utf-8");
3166
+ const rows = execFileSync("ps", ["-eo", "pid=,ppid=,pgid=,command="], {
3167
+ encoding: "utf8",
3168
+ stdio: [
3169
+ "ignore",
3170
+ "pipe",
3171
+ "ignore"
3172
+ ]
3173
+ }).split(/\r?\n/).map((row) => {
3174
+ const match = row.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
3175
+ return match ? {
3176
+ pid: Number(match[1]),
3177
+ ppid: Number(match[2]),
3178
+ pgid: Number(match[3]),
3179
+ args: match[4]
3180
+ } : null;
3181
+ }).filter((row) => row !== null);
3182
+ const descendants = new Set([state.composePid]);
3183
+ let changed = true;
3184
+ while (changed) {
3185
+ changed = false;
3186
+ for (const row of rows) if (!descendants.has(row.pid) && descendants.has(row.ppid)) {
3187
+ descendants.add(row.pid);
3188
+ changed = true;
3189
+ }
3190
+ }
3191
+ return rows.filter((row) => row.pid !== state.composePid && descendants.has(row.pid)).map(({ pid, pgid, args }) => ({
3192
+ pid,
3193
+ pgid,
3194
+ args
3195
+ }));
3196
+ } catch {
3197
+ return [];
3198
+ }
3199
+ }
3200
+ function composeChildPids(state) {
3201
+ return [...new Set(composeChildProcesses(state).map((child) => child.pid))];
3202
+ }
3203
+ function composeEnginePid(state) {
3204
+ return composeChildProcesses(state).find(({ args }) => args.includes(" --config ") && args.includes(state.namespace))?.pid;
3205
+ }
3206
+ async function terminateComposeChildren(state) {
3207
+ if (IS_WINDOWS) return true;
3208
+ const groups = [...new Set(composeChildProcesses(state).map((child) => child.pgid).filter((pgid) => pgid !== state.composePid))];
3209
+ let stopped = true;
3210
+ for (const pgid of groups) {
2027
3211
  try {
2028
- const cleared = clearPersistedBuiltinConfig(cwd, runtimePath, rewritten);
2029
- if (cleared.length > 0) vlog(`cleared ${cleared.length} persisted builtin config entries so the rendered runtime config seeds the engine again`);
3212
+ process.kill(-pgid, "SIGTERM");
2030
3213
  } catch (err) {
2031
- p.log.error(`${String(err instanceof Error ? err.message : err)}. The engine would ignore the rendered runtime config and keep its previously persisted ports and timeouts. Fix the permissions on that file and run agentmemory start again.`);
2032
- process.exit(1);
3214
+ if (err.code !== "ESRCH") stopped = false;
3215
+ continue;
2033
3216
  }
2034
- if (selectedInstance === 0 && dataDirResolution.source === "default") {
2035
- for (const m of legacyDataMigrations(process.cwd(), home, dataDirResolution.dataDir)) if (existsSync(m.from) && !existsSync(m.to)) try {
2036
- mkdirSync(dirname(m.to), { recursive: true });
2037
- cpSync(m.from, m.to, { recursive: true });
2038
- p.log.info(`Copied existing data: ${m.from} -> ${m.to}`);
2039
- } catch (err) {
2040
- vlog(`data copy failed for ${m.from}: ${String(err)}`);
3217
+ const deadline = Date.now() + 5e3;
3218
+ while (Date.now() < deadline) {
3219
+ try {
3220
+ process.kill(-pgid, 0);
3221
+ } catch {
3222
+ break;
2041
3223
  }
3224
+ await new Promise((resolve) => setTimeout(resolve, 100));
2042
3225
  }
2043
- return {
2044
- configPath: runtimePath,
2045
- cwd
2046
- };
2047
- } catch (err) {
2048
- const failure = resolveLaunchRenderFailure(stateBackendKind, err);
2049
- if (failure.fatal) {
2050
- p.log.error(failure.message);
2051
- process.exit(1);
2052
- }
2053
- vlog(`runtime config generation failed, using bundled config verbatim: ${String(err)}`);
2054
- return {
2055
- configPath,
2056
- cwd
2057
- };
3226
+ try {
3227
+ process.kill(-pgid, 0);
3228
+ process.kill(-pgid, "SIGKILL");
3229
+ } catch {}
2058
3230
  }
3231
+ for (const pid of composeChildPids(state)) if (pidAlive(pid) && !await signalAndWait(pid, "SIGTERM", 1e3)) stopped = false;
3232
+ return stopped;
2059
3233
  }
2060
- function startIiiBin(iiiBin, configPath) {
2061
- const s = p.spinner();
2062
- s.start(`Starting iii-engine: ${iiiBin}`);
2063
- const launch = prepareEngineLaunch(configPath);
2064
- writeEngineState({
2065
- kind: "native",
2066
- configPath: launch.configPath,
2067
- binPath: iiiBin
3234
+ async function startIiiBin(iiiBin) {
3235
+ const workerBinDir = process.env["AGENTMEMORY_WORKER_BIN_DIR"]?.trim() || void 0;
3236
+ if (workerBinDir && (!existsSync(resolve(process.cwd(), workerBinDir)) || !["state", "http"].every((name) => existsSync(join(resolve(process.cwd(), workerBinDir), name))))) throw new Error(`AGENTMEMORY_WORKER_BIN_DIR must contain executable state and http binaries: ${workerBinDir}`);
3237
+ const envPath = process.env["AGENTMEMORY_ENV_FILE"] || join(homedir(), ".agentmemory", ".env");
3238
+ const namespace = composeNamespace(getRestPort());
3239
+ const projectRoot = resolve(__dirname, "..");
3240
+ const ports = {
3241
+ restPort: getRestPort(),
3242
+ viewerPort: getConfiguredViewerPort(),
3243
+ enginePort: getEnginePort()
3244
+ };
3245
+ await assertComposePortsFree([
3246
+ ports.restPort,
3247
+ ports.viewerPort,
3248
+ ports.enginePort
3249
+ ]);
3250
+ const compose = startComposeDaemon({
3251
+ ports,
3252
+ dataDir: dataDirResolution.dataDir,
3253
+ projectRoot,
3254
+ nodeBin: process.execPath,
3255
+ homeDir: homedir(),
3256
+ ...existsSync(envPath) ? { envFile: envPath } : {},
3257
+ ...workerBinDir ? { workerBinDir } : {},
3258
+ cwd: process.cwd(),
3259
+ stateBackend: { kind: getStateBackend() },
3260
+ ...getStateBackend() === "redis" && getRedisUrl() ? { redisUrl: getRedisUrl() } : {},
3261
+ workerEnvironment: process.env,
3262
+ disableRestartPolicy: process.env["AGENTMEMORY_DISABLE_COMPOSE_RESTART"] === "1",
3263
+ startupTimeoutMs: startupTimeouts().workerMs,
3264
+ linux: process.platform === "linux",
3265
+ stateDir: join(dataDirResolution.dataDir, "compose-state"),
3266
+ logFile: join(homedir(), ".agentmemory", `compose-${getRestPort()}.log`),
3267
+ iiiBin,
3268
+ env: process.env
3269
+ });
3270
+ spawnedComposeDaemon = compose.child;
3271
+ composeStartupCompleted = false;
3272
+ if (typeof compose.child.pid !== "number") throw new Error("Failed to spawn the iii Compose daemon.");
3273
+ const state = {
3274
+ kind: "compose",
3275
+ restPort: getRestPort(),
3276
+ viewerPort: getConfiguredViewerPort(),
3277
+ enginePort: getEnginePort(),
3278
+ engineUrl: `ws://127.0.0.1:${getEnginePort()}`,
3279
+ namespace,
3280
+ file: compose.file,
3281
+ stateDir: compose.stateDir,
3282
+ logFile: compose.logFile,
3283
+ logStartOffset: compose.logStartOffset,
3284
+ binPath: iiiBin,
3285
+ composePid: compose.child.pid,
3286
+ ...workerBinDir ? { workerBinDir: resolve(process.cwd(), workerBinDir) } : {}
3287
+ };
3288
+ writeComposeState(engineStatePath(), {
3289
+ ...state,
3290
+ workerVersions: workerBinDir ? { local: true } : COMPOSE_WORKER_VERSIONS
3291
+ });
3292
+ engineReadinessDeadline = startupDeadline(startupTimeouts().engineMs);
3293
+ compose.child.on("error", (error) => {
3294
+ if (spawnedComposeDaemon === compose.child) startupFailure = {
3295
+ kind: "engine-crashed",
3296
+ stderr: redactCredentialUrls(error.message),
3297
+ binary: iiiBin
3298
+ };
3299
+ });
3300
+ compose.child.on("exit", (code, signal) => {
3301
+ if (spawnedComposeDaemon === compose.child && !composeStartupCompleted && (code !== 0 || signal !== null)) startupFailure = {
3302
+ kind: "engine-crashed",
3303
+ stderr: signal ? `Compose exited from ${signal}` : `Compose exited with code ${code}`,
3304
+ binary: iiiBin
3305
+ };
2068
3306
  });
2069
- spawnEngineBackground(iiiBin, [
2070
- "--config",
2071
- launch.configPath,
2072
- "--no-update-check"
2073
- ], "iii-engine", launch.cwd);
2074
- s.stop(c.ok("iii-engine process started"));
3307
+ const s = p.spinner();
3308
+ s.start(`Starting iii Worker Compose: ${iiiBin}`);
3309
+ s.stop(c.ok(`Compose daemon started (pid ${compose.child.pid})`));
2075
3310
  return true;
2076
3311
  }
2077
3312
  function pickCompatibleIii(candidates) {
@@ -2082,11 +3317,17 @@ function pickCompatibleIii(candidates) {
2082
3317
  }
2083
3318
  return null;
2084
3319
  }
2085
- function warnIfDockerIgnoresStateBackend(stateBackendKind) {
2086
- if (stateBackendKind !== "redis") return;
2087
- p.log.warn("AGENTMEMORY_STATE_BACKEND=redis is not applied on the Docker path: docker-compose.yml mounts iii-config.docker.yaml read-only, so this start will not render it. Edit iii-config.docker.yaml by hand to point iii-state/iii-stream at redis (see README \"Storage backend\"), or it keeps using the file store. docker-compose.yml passes AGENTMEMORY_REDIS_URL into the engine container, so redis_url: '${AGENTMEMORY_REDIS_URL}' works in that file.");
2088
- }
2089
3320
  async function startEngine() {
3321
+ iiiProbeScan = /* @__PURE__ */ new Map();
3322
+ try {
3323
+ return await scanAndStartEngine();
3324
+ } finally {
3325
+ iiiProbeScan = null;
3326
+ }
3327
+ }
3328
+ async function scanAndStartEngine() {
3329
+ startupFailure = null;
3330
+ engineReadinessDeadline = null;
2090
3331
  let stateBackendKind;
2091
3332
  try {
2092
3333
  stateBackendKind = getStateBackend();
@@ -2098,58 +3339,49 @@ async function startEngine() {
2098
3339
  p.log.error("AGENTMEMORY_STATE_BACKEND=redis requires AGENTMEMORY_REDIS_URL to be set (e.g. redis://localhost:6379). Set both in ~/.agentmemory/.env, or unset AGENTMEMORY_STATE_BACKEND to keep the default file store.");
2099
3340
  process.exit(1);
2100
3341
  }
2101
- await assertRuntimePortOwnership();
2102
3342
  const persistedState = readEngineState();
2103
3343
  if (persistedState?.kind === "docker") {
2104
- warnIfDockerIgnoresStateBackend(stateBackendKind);
2105
- const inspection = inspectOwnedDockerEngine(persistedState);
2106
- if (inspection.status === "unavailable" || inspection.status === "missing") {
2107
- const detail = inspection.status === "unavailable" ? inspection.reason : "container is missing";
2108
- p.log.error(`A saved Docker engine owns this lifecycle state, but it cannot be resumed safely (${detail}). Run \`agentmemory stop\` to reconcile that project before starting another engine.`);
2109
- return false;
3344
+ p.log.error(legacyDockerStateMessage(persistedState));
3345
+ return false;
3346
+ }
3347
+ await assertRuntimePortOwnership();
3348
+ if (persistedState?.kind === "compose") {
3349
+ if (await isEngineRunning()) {
3350
+ p.log.error("The managed Worker Compose engine is already running; it should have been reconciled before attempting a fresh start.");
3351
+ process.exit(1);
3352
+ }
3353
+ if (persistedState.restPort === getRestPort()) {
3354
+ if (persistedState.composePid && pidAlive(persistedState.composePid)) {
3355
+ p.log.error(`Worker Compose daemon ${persistedState.composePid} is still alive. Stop it before retrying.`);
3356
+ process.exit(1);
3357
+ }
3358
+ clearEngineState();
2110
3359
  }
2111
- persistDockerInspection(persistedState, inspection);
2112
- if (inspection.status === "running") return true;
2113
- const dockerBin = whichBinary("docker");
2114
- return Boolean(dockerBin && runCommand(dockerBin, ["start", inspection.containerId], { label: `Restarting owned Docker container ${inspection.containerId}` }));
2115
3360
  }
2116
- const configPath = findIiiConfig();
2117
3361
  warnIfRelocatedDataDir();
2118
3362
  const pathIii = whichBinary("iii");
2119
- vlog(`iii binary: ${pathIii ?? "(not on PATH)"}, config: ${configPath || "(not found)"}`);
2120
- const dockerBin = whichBinary("docker");
2121
- const composeFile = [
2122
- join(__dirname, "..", "docker-compose.yml"),
2123
- join(__dirname, "docker-compose.yml"),
2124
- join(process.cwd(), "docker-compose.yml")
2125
- ].find((c) => existsSync(c));
2126
- const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
3363
+ vlog(`iii binary: ${pathIii ?? "(not on PATH)"}`);
2127
3364
  const fallbacks = fallbackIiiPaths().filter((p) => existsSync(p));
2128
- for (const f of fallbacks) vlog(`fallback iii at ${f} reports version: ${iiiBinVersion(f) ?? "unknown"}`);
3365
+ vlog(`fallback iii candidates: ${fallbacks.length > 0 ? fallbacks.join(", ") : "(none)"}`);
2129
3366
  let iiiBin = pickCompatibleIii([pathIii, ...fallbacks]);
2130
- if (iiiBin && configPath && !(dockerOptIn && dockerBin && composeFile)) {
3367
+ if (iiiBin) {
2131
3368
  if (iiiBin !== pathIii) {
2132
3369
  p.log.info(`Using iii at: ${c.dim(iiiBin)} (v${c.accent(IIPINNED_VERSION)})`);
2133
3370
  process.env["PATH"] = `${dirname(iiiBin)}${delimiter}${process.env["PATH"] ?? ""}`;
2134
3371
  }
2135
- return startIiiBin(iiiBin, configPath);
3372
+ return startIiiBin(iiiBin);
2136
3373
  }
2137
3374
  if (pathIii && !iiiBin) vlog(`iii on PATH is v${iiiBinVersion(pathIii) ?? "unknown"}, pin is v${IIPINNED_VERSION}. Will install pinned engine to ${privateIiiPath()}.`);
2138
- if (!configPath) {
2139
- startupFailure = { kind: "no-engine" };
2140
- return false;
2141
- }
2142
- vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
2143
3375
  mkdirSync(dataDirResolution.dataDir, { recursive: true });
2144
- vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
2145
3376
  const interactive = !!process.stdin.isTTY && !process.env["CI"];
2146
3377
  let choice;
2147
- const pathIiiMismatch = pathIii !== null && resolveCompatibleIii(pathIii) === null;
2148
- if (dockerOptIn && dockerBin && composeFile) choice = "docker";
2149
- else if (pathIiiMismatch) {
3378
+ if (pathIii !== null && resolveCompatibleIii(pathIii) === null) {
2150
3379
  choice = "install";
2151
3380
  const detected = iiiBinVersion(pathIii);
2152
3381
  p.log.info(`iii on PATH is v${detected ?? "unknown"} but agentmemory pins v${IIPINNED_VERSION}. Installing pinned engine to ~/.agentmemory/bin (leaves your existing iii untouched).`);
3382
+ } else if (IS_WINDOWS) {
3383
+ choice = "install";
3384
+ p.log.warn("iii-engine binary not found locally.");
2153
3385
  } else if (!interactive) {
2154
3386
  choice = "install";
2155
3387
  p.log.info("Non-interactive environment detected — auto-installing iii-engine.");
@@ -2160,11 +3392,6 @@ async function startEngine() {
2160
3392
  label: `Install iii v${IIPINNED_VERSION} to ~/.agentmemory/bin (~6MB, ~5s)`,
2161
3393
  hint: "recommended"
2162
3394
  }];
2163
- if (dockerBin && composeFile) options.push({
2164
- value: "docker",
2165
- label: "Use Docker compose",
2166
- hint: "advanced"
2167
- });
2168
3395
  options.push({
2169
3396
  value: "manual",
2170
3397
  label: "Show manual install steps and exit"
@@ -2189,87 +3416,29 @@ async function startEngine() {
2189
3416
  if (result.ok && result.binPath) {
2190
3417
  process.env["PATH"] = `${dirname(result.binPath)}${delimiter}${process.env["PATH"] ?? ""}`;
2191
3418
  iiiBin = result.binPath;
2192
- return startIiiBin(iiiBin, configPath);
2193
- }
2194
- if (dockerBin && composeFile && interactive) {
2195
- const fallback = await p.confirm({
2196
- message: "Auto-install failed. Try Docker compose instead?",
2197
- initialValue: true
2198
- });
2199
- if (p.isCancel(fallback) || fallback !== true) {
2200
- startupFailure = { kind: "no-engine" };
2201
- return false;
2202
- }
2203
- choice = "docker";
2204
- } else {
2205
- startupFailure = { kind: "no-engine" };
2206
- return false;
3419
+ return startIiiBin(iiiBin);
2207
3420
  }
3421
+ startupFailure = { kind: "no-engine" };
3422
+ return false;
2208
3423
  }
2209
- if (choice === "docker" && dockerBin && composeFile) {
2210
- warnIfDockerIgnoresStateBackend(stateBackendKind);
2211
- const s = p.spinner();
2212
- s.start("Starting iii-engine via Docker...");
2213
- configureDockerHostUser();
2214
- const projectName = dockerProjectName(getRestPort());
2215
- writeEngineState({
2216
- kind: "docker",
2217
- schemaVersion: 2,
2218
- composeFile,
2219
- projectName,
2220
- engineVersion: IIPINNED_VERSION,
2221
- dataMountType: "bind",
2222
- dataMountSource: dataDirResolution.dataDir,
2223
- preserveContainer: false
2224
- });
2225
- spawnEngineBackground(dockerBin, dockerComposeArgs(composeFile, projectName, ["up", "-d"]), "iii-engine via Docker");
2226
- s.stop("Docker compose started");
2227
- return true;
2228
- }
2229
- if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
2230
- else startupFailure = { kind: "no-engine" };
3424
+ startupFailure = { kind: "no-engine" };
2231
3425
  return false;
2232
3426
  }
2233
- async function waitForEngine(timeoutMs) {
2234
- const start = Date.now();
2235
- while (Date.now() - start < timeoutMs) {
2236
- if (await isEngineRunning()) return true;
2237
- await new Promise((r) => setTimeout(r, 500));
2238
- }
2239
- return false;
3427
+ async function waitForEngine(timeoutMs = startupTimeouts().engineMs) {
3428
+ const state = readEngineState();
3429
+ const endpoint = state?.kind === "compose" ? managedComposeReadinessUrl(state.restPort) : getBaseUrl();
3430
+ const probe = state?.kind === "compose" ? (signal) => probeEngine(endpoint, signal) : (signal) => probeEngine(endpoint, signal);
3431
+ return waitForReadiness(engineReadinessDeadline ?? startupDeadline(timeoutMs), probe, {
3432
+ intervalMs: 250,
3433
+ probeTimeoutMs: 750,
3434
+ stopped: () => startupInterrupted || engineStartupFailed()
3435
+ });
2240
3436
  }
2241
- async function reconcilePersistedDockerEngine() {
3437
+ async function waitForPersistedNativeEngine() {
2242
3438
  const state = readEngineState();
2243
- if (state?.kind !== "docker") return false;
2244
- const inspection = inspectOwnedDockerEngine(state);
2245
- if (inspection.status === "unavailable") {
2246
- p.log.error(`The saved Docker engine cannot be verified: ${inspection.reason}. Refusing to overwrite its lifecycle state. Run \`agentmemory stop\` after restoring Docker/compose access.`);
2247
- process.exit(1);
2248
- }
2249
- if (inspection.status === "missing") {
2250
- p.log.error("The saved Docker project no longer has an iii-engine container. Run `agentmemory stop` to remove its stale project state, then start agentmemory again.");
2251
- process.exit(1);
2252
- }
2253
- persistDockerInspection(state, inspection);
2254
- if (inspection.status === "stopped") {
2255
- const dockerBin = whichBinary("docker");
2256
- if (!dockerBin || !runCommand(dockerBin, ["start", inspection.containerId], { label: `Restarting owned Docker container ${inspection.containerId}` })) {
2257
- p.log.error("The owned Docker container could not be restarted; lifecycle state was preserved.");
2258
- process.exit(1);
2259
- }
2260
- }
2261
- if (!await waitForEngine(5e3)) {
2262
- printDockerStartupLogs();
2263
- p.log.error(`The owned Docker container is running, but its REST API on port ${getRestPort()} is unhealthy. The saved project was preserved; inspect the Docker logs above or run \`agentmemory stop\`.`);
2264
- process.exit(1);
2265
- }
2266
- await startWorkerForEngineState();
2267
- if (!await waitForAgentmemoryReady(15e3)) {
2268
- p.log.error("agentmemory worker did not become ready within 15s.");
2269
- process.exit(1);
2270
- }
2271
- await maybeOfferGlobalInstall();
2272
- printReadyHint();
3439
+ const pid = readEnginePidfile();
3440
+ if (state?.kind !== "native" || !pid || !pidAlive(pid) || isForeignPortHolder(pidCommand(pid))) return false;
3441
+ if (!await waitForEngine()) throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
2273
3442
  return true;
2274
3443
  }
2275
3444
  function installInstructions() {
@@ -2285,12 +3454,9 @@ function installInstructions() {
2285
3454
  ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
2286
3455
  ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
2287
3456
  verify,
2288
- " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
3457
+ " 3. Extract iii.exe to %USERPROFILE%\\.agentmemory\\bin\\iii.exe (or add to PATH)",
2289
3458
  " 4. Re-run: npx @agentmemory/agentmemory",
2290
3459
  "",
2291
- ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
2292
- " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
2293
- "",
2294
3460
  "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
2295
3461
  "",
2296
3462
  "Docs: https://iii.dev/docs"
@@ -2303,9 +3469,6 @@ function installInstructions() {
2303
3469
  linuxInstall,
2304
3470
  " Then re-run: npx @agentmemory/agentmemory",
2305
3471
  "",
2306
- ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
2307
- " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
2308
- "",
2309
3472
  "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
2310
3473
  "",
2311
3474
  "Docs: https://iii.dev/docs"
@@ -2314,85 +3477,101 @@ function installInstructions() {
2314
3477
  function portInUseDiagnostic(port) {
2315
3478
  return IS_WINDOWS ? ` netstat -ano | findstr :${port}` : ` lsof -i :${port} # or: ss -tlnp | grep :${port}`;
2316
3479
  }
2317
- async function waitForAgentmemoryReady(timeoutMs) {
2318
- const start = Date.now();
2319
- while (Date.now() - start < timeoutMs) {
2320
- if (await isAgentmemoryReady()) return true;
2321
- await new Promise((r) => setTimeout(r, 250));
2322
- }
2323
- return false;
2324
- }
2325
- function getEngineHost() {
2326
- const envKeys = shouldUseAgentmemoryUrl() ? ["III_ENGINE_URL", "AGENTMEMORY_URL"] : ["III_ENGINE_URL"];
2327
- for (const envKey of envKeys) {
2328
- const raw = process.env[envKey];
2329
- if (!raw) continue;
2330
- try {
2331
- const parsed = new URL(raw);
2332
- if (parsed.hostname) return parsed.hostname;
2333
- } catch {}
2334
- }
2335
- return "localhost";
2336
- }
2337
3480
  function printReadyHint() {
2338
3481
  const restUrl = getBaseUrl();
2339
3482
  const viewerUrl = getViewerUrl();
2340
- const engineHost = getEngineHost();
2341
- const streamUrl = `ws://${engineHost}:${getStreamPort()}`;
2342
- const engineUrl = `ws://${engineHost}:${getEnginePort()}`;
3483
+ const engineUrl = process.env["III_ENGINE_URL"] || `ws://localhost:${getEnginePort()}`;
2343
3484
  const invocation = isInvokedViaNpx() ? "npx @agentmemory/agentmemory" : "agentmemory";
2344
- const consolePort = defaultConsolePort(getConfiguredViewerPort());
2345
3485
  const portFlags = portFlagSuffix(getRestPort(), selectedInstance);
2346
- const consoleLine = `${c.label("iii console")} ${c.cmd(`${invocation} console${portFlags}`)} ${c.dim(`(serves on :${consolePort}; first run downloads the console next to the pinned iii)`)}`;
2347
3486
  const lines = [
2348
3487
  `${c.label("REST API")} ${c.url(restUrl)}`,
2349
3488
  `${c.label("Viewer")} ${c.url(viewerUrl)}`,
2350
- `${c.label("Streams")} ${c.url(streamUrl)}`,
2351
- `${c.label("Engine")} ${c.url(engineUrl)}`,
2352
- consoleLine
3489
+ `${c.label("Engine")} ${c.url(engineUrl)}`
2353
3490
  ];
2354
3491
  p.note(lines.join("\n"), `agentmemory v${c.accent(VERSION)}`);
2355
3492
  const demoCommand = `${invocation} demo${portFlags}`;
2356
3493
  process.stdout.write(`\n${c.dim("Try:")} ${c.cmd(demoCommand)}\n`);
2357
3494
  }
3495
+ async function reconcilePersistedComposeEngine() {
3496
+ const state = readEngineState();
3497
+ if (state?.kind !== "compose") return false;
3498
+ if (!await isEngineRunning()) {
3499
+ if (state.composePid && pidAlive(state.composePid) && composePidIdentity(state.composePid, state) !== "foreign") {
3500
+ p.log.error(`Worker Compose daemon ${state.composePid} is alive but REST ${state.restPort} is not ready. Inspect ${state.logFile}; use agentmemory stop to reconcile.`);
3501
+ process.exit(1);
3502
+ }
3503
+ if (await isEngineRunning()) {
3504
+ p.log.error(`An engine is responding on :${state.restPort}, but the recorded Compose daemon is gone. Refusing to adopt or clear this endpoint automatically.`);
3505
+ process.exit(1);
3506
+ }
3507
+ clearEngineState();
3508
+ p.log.info("Found stale Worker Compose lifecycle state; starting a fresh managed instance.");
3509
+ return false;
3510
+ }
3511
+ const version = iiiBinVersion(state.binPath);
3512
+ if (state.composePid && !composePidMatches(state.composePid, state)) {
3513
+ p.log.error(`Compose metadata points to PID ${state.composePid}, but its process identity does not match the saved engine/file/namespace.`);
3514
+ process.exit(1);
3515
+ }
3516
+ if (version !== IIPINNED_VERSION) {
3517
+ p.log.error(`Managed Compose engine is ${version ?? "unverified"}; expected ${IIPINNED_VERSION}. Run agentmemory stop before changing the runtime.`);
3518
+ process.exit(1);
3519
+ }
3520
+ try {
3521
+ if (!ownedComposeProject(await composeCall({
3522
+ engineUrl: state.engineUrl,
3523
+ namespace: state.namespace,
3524
+ functionId: "compose::status",
3525
+ file: state.file
3526
+ }), state.file)) throw new Error("Compose daemon does not report the owned worker-compose.yaml project.");
3527
+ } catch (err) {
3528
+ p.log.error(`Could not verify the managed Compose project: ${err instanceof Error ? err.message : String(err)}. See ${state.logFile}.`);
3529
+ process.exit(1);
3530
+ }
3531
+ return true;
3532
+ }
2358
3533
  async function main() {
3534
+ startupTimeouts();
2359
3535
  await assertRuntimePortOwnership();
2360
3536
  try {
2361
- if ((await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(1500) })).ok) {
3537
+ if ((await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(1500) })).ok && readEngineState()?.kind !== "compose") {
2362
3538
  p.log.error(`agentmemory is already running on port ${getRestPort()}. Starting a second instance here would corrupt the running daemon's REST routing. Use the REST API (or the MCP tools) against the running instance, run a different --instance, or stop it first with \`agentmemory stop\`.`);
2363
3539
  process.exit(1);
2364
3540
  }
2365
3541
  } catch {}
3542
+ const legacyState = readEngineState();
3543
+ if (legacyState?.kind === "docker") {
3544
+ p.intro("agentmemory");
3545
+ p.log.error(legacyDockerStateMessage(legacyState));
3546
+ process.exit(1);
3547
+ }
2366
3548
  if (IS_RESET) resetPrefs();
2367
3549
  const firstRun = isFirstRun();
2368
3550
  const prefs = readPrefs();
2369
3551
  if (firstRun || IS_RESET || IS_VERBOSE || !prefs.skipSplash) renderSplash(VERSION);
2370
3552
  if (firstRun || IS_RESET) await runOnboarding();
3553
+ beginStartup();
2371
3554
  if (skipEngine) {
2372
3555
  if (IS_VERBOSE) p.log.info("Skipping engine check (--no-engine)");
2373
- await import("./src-Jsq9LCeH.mjs");
2374
- if (await waitForAgentmemoryReady(15e3)) {
2375
- await maybeOfferGlobalInstall();
2376
- printReadyHint();
2377
- }
3556
+ await startWorkerForEngineState();
3557
+ await maybeOfferGlobalInstall();
3558
+ printReadyHint();
3559
+ return;
3560
+ }
3561
+ if (await reconcilePersistedComposeEngine()) {
3562
+ await startWorkerForEngineState();
3563
+ await maybeOfferGlobalInstall();
3564
+ printReadyHint();
2378
3565
  return;
2379
3566
  }
2380
- if (await reconcilePersistedDockerEngine()) return;
2381
- if (await isEngineRunning()) {
3567
+ if (await isEngineRunning() || await waitForPersistedNativeEngine()) {
2382
3568
  if (IS_VERBOSE) p.log.success("iii-engine is running");
2383
3569
  const persisted = readEngineState();
2384
3570
  const attachedBin = (persisted?.kind === "native" && persisted.binPath && existsSync(persisted.binPath) ? persisted.binPath : null) ?? whichBinary("iii") ?? fallbackIiiPaths().find((p) => existsSync(p)) ?? null;
2385
- const detected = attachedBin ? iiiBinVersion(attachedBin) : null;
3571
+ const detected = persisted?.kind === "compose" ? iiiBinVersion(persisted.binPath) : attachedBin ? iiiBinVersion(attachedBin) : null;
2386
3572
  if (detected === IIPINNED_VERSION) {
2387
- adoptRunningEngine();
2388
- await startWorkerForEngineState();
2389
- if (!await waitForAgentmemoryReady(15e3)) {
2390
- p.log.error("agentmemory worker did not become ready within 15s.");
2391
- process.exit(1);
2392
- }
2393
- await maybeOfferGlobalInstall();
2394
- printReadyHint();
2395
- return;
3573
+ p.log.error(`An unmanaged iii engine is already listening on ${getEnginePort()}. Agentmemory will not attach to it; stop that engine explicitly or select another --instance/--port.`);
3574
+ process.exit(1);
2396
3575
  }
2397
3576
  const detectedLabel = detected ? `v${detected}` : "an unverified version";
2398
3577
  const base = isInvokedViaNpx() ? "npx @agentmemory/agentmemory" : "agentmemory";
@@ -2419,17 +3598,42 @@ async function main() {
2419
3598
  if (!await startEngine()) {
2420
3599
  p.log.error("Could not start iii-engine.");
2421
3600
  const lines = installInstructions();
2422
- if (startupFailure?.kind === "no-docker-compose") lines.unshift("Docker is installed but docker-compose.yml is missing from this", "install. Re-install with: npm install -g @agentmemory/agentmemory", "");
2423
3601
  p.note(lines.join("\n"), "Setup required");
2424
- process.exit(1);
3602
+ throw new Error("iii-engine could not be started.");
2425
3603
  }
2426
3604
  const s = p.spinner();
2427
3605
  s.start("Waiting for iii-engine to be ready...");
2428
- if (!await waitForEngine(15e3)) {
3606
+ const ready = await waitForEngine();
3607
+ if (ready) {
3608
+ const composeState = readEngineState();
3609
+ if (composeState?.kind === "compose") {
3610
+ composeState.enginePid = composeEnginePid(composeState);
3611
+ writeComposeState(engineStatePath(), {
3612
+ ...composeState,
3613
+ workerVersions: composeState.workerVersions ?? COMPOSE_WORKER_VERSIONS
3614
+ });
3615
+ }
3616
+ }
3617
+ if (!ready) {
2429
3618
  const port = getRestPort();
2430
- s.stop("iii-engine did not become ready within 15s");
2431
- printDockerStartupLogs();
2432
- if (startupFailure?.kind === "engine-crashed" || startupFailure?.kind === "docker-crashed") {
3619
+ let composeState = readEngineState();
3620
+ let composeLog = composeState?.kind === "compose" ? readComposeStartupLogs() : null;
3621
+ if (composeState?.kind === "compose" && spawnedComposeDaemon?.exitCode !== null) {
3622
+ await new Promise((resolve) => setTimeout(resolve, 150));
3623
+ composeLog = readComposeStartupLogs() ?? composeLog;
3624
+ }
3625
+ let registryFailure = null;
3626
+ if (composeState?.kind === "compose") {
3627
+ const stateFailure = composeState.workerFailure ?? composeStateRegistryFailure(dataDirResolution.dataDir, composeState.file);
3628
+ const logFailure = composeLog ? composeRegistryFailureMessage(composeLog.text, composeState.workerBinDir) : null;
3629
+ if (stateFailure) registryFailure = `Worker registry unavailable while resolving state/http; retry later or set AGENTMEMORY_WORKER_BIN_DIR. Compose reported: ${stateFailure}`;
3630
+ else registryFailure = logFailure;
3631
+ }
3632
+ const composeLogTail = composeLog?.text.trim().split(/\r?\n/).slice(-12).join("\n");
3633
+ const shortFailure = registryFailure || startupFailure?.stderr || (composeState?.kind === "compose" && composeLogTail ? `Worker Compose startup failed. Recent Compose output:\n${composeLogTail}` : startupTimeoutMessage("engine", startupTimeouts().engineMs));
3634
+ s.stop(shortFailure);
3635
+ printComposeStartupLogs();
3636
+ if (startupFailure?.kind === "engine-crashed") {
2433
3637
  p.log.error("The iii-engine process crashed on startup.");
2434
3638
  if (startupFailure.binary) p.log.info(`Binary: ${startupFailure.binary}`);
2435
3639
  if (startupFailure.stderr) p.note(startupFailure.stderr, "engine stderr");
@@ -2438,7 +3642,6 @@ async function main() {
2438
3642
  "Common causes:",
2439
3643
  ` - iii-engine version mismatch — reinstall the pinned v${IIPINNED_VERSION} binary`,
2440
3644
  " (sh script on macOS/Linux, GitHub release zip on Windows)",
2441
- " - Docker Desktop not running (if you're using the Docker path)",
2442
3645
  " - Port already in use (see below)",
2443
3646
  "",
2444
3647
  "See https://iii.dev/docs for current install instructions."
@@ -2456,17 +3659,26 @@ async function main() {
2456
3659
  "Re-run with --verbose to see engine stderr."
2457
3660
  ].join("\n"), "Troubleshooting");
2458
3661
  }
2459
- process.exit(1);
3662
+ if (composeLog?.text) p.log.error(`Compose log: ${composeLog.path}`);
3663
+ throw new Error(shortFailure);
2460
3664
  }
2461
3665
  s.stop(c.ok("iii-engine is ready"));
2462
3666
  await startWorkerForEngineState();
2463
- if (!await waitForAgentmemoryReady(15e3)) {
2464
- p.log.error("agentmemory worker did not become ready within 15s.");
2465
- process.exit(1);
3667
+ const managedCompose = readEngineState();
3668
+ if (managedCompose?.kind === "compose" && !managedCompose.enginePid) {
3669
+ managedCompose.enginePid = composeChildPids(managedCompose)[0];
3670
+ writeComposeState(engineStatePath(), {
3671
+ ...managedCompose,
3672
+ workerVersions: managedCompose.workerVersions ?? COMPOSE_WORKER_VERSIONS
3673
+ });
2466
3674
  }
2467
3675
  await maybeOfferGlobalInstall();
2468
3676
  printReadyHint();
2469
3677
  writePrefs({ skipSplash: true });
3678
+ if (managedCompose?.kind === "compose") {
3679
+ await new Promise((resolve) => process.stdout.write("", () => resolve()));
3680
+ process.exit(0);
3681
+ }
2470
3682
  }
2471
3683
  async function apiFetch(base, path, timeoutMs = 5e3) {
2472
3684
  try {
@@ -2480,10 +3692,29 @@ async function apiFetch(base, path, timeoutMs = 5e3) {
2480
3692
  }
2481
3693
  }
2482
3694
  async function runStatus() {
2483
- getRestPort();
2484
3695
  const base = getBaseUrl();
2485
3696
  p.intro("agentmemory status");
3697
+ const state = readEngineState();
3698
+ const savedState = !(shouldUseAgentmemoryUrl() && process.env["AGENTMEMORY_URL"]) && (state?.kind !== "compose" || state.restPort === getRestPort()) ? state : null;
3699
+ if (savedState?.kind === "docker") p.log.warn(legacyDockerStateMessage(savedState));
2486
3700
  if (!await isEngineRunning()) {
3701
+ if (savedState?.kind === "compose" && savedState.composePid && pidAlive(savedState.composePid)) try {
3702
+ const failure = composeWorkerFailureSummary(ownedComposeProject(await composeCall({
3703
+ engineUrl: savedState.engineUrl,
3704
+ namespace: savedState.namespace,
3705
+ functionId: "compose::status",
3706
+ file: savedState.file
3707
+ }), savedState.file));
3708
+ if (failure) {
3709
+ p.log.error(`Worker Compose failed after exhausting restart attempts; REST is unavailable at ${base}.`);
3710
+ p.log.error(failure);
3711
+ p.log.info(`Inspect ${savedState.logFile}, then run agentmemory stop before retrying.`);
3712
+ process.exit(1);
3713
+ }
3714
+ } catch (error) {
3715
+ const message = error instanceof Error ? error.message : String(error);
3716
+ p.log.warn(`Worker Compose status unavailable: ${message}`);
3717
+ }
2487
3718
  p.log.error(`Not running — no response at ${base}`);
2488
3719
  p.log.info("Start with: npx @agentmemory/agentmemory");
2489
3720
  process.exit(1);
@@ -2503,8 +3734,8 @@ async function runStatus() {
2503
3734
  const version = healthRes?.version || "?";
2504
3735
  const sessionList = Array.isArray(sessionsRes?.sessions) ? sessionsRes.sessions : [];
2505
3736
  const sessions = sessionList.length;
2506
- const nodes = Number(graphRes?.totalNodes ?? graphRes?.nodes ?? graphRes?.nodeCount ?? 0);
2507
- const edges = Number(graphRes?.totalEdges ?? graphRes?.edges ?? graphRes?.edgeCount ?? 0);
3737
+ const nodes = graphRes?.available === false ? "unknown" : graphRes?.totalNodes ?? graphRes?.nodes ?? graphRes?.nodeCount ?? "unknown";
3738
+ const edges = graphRes?.available === false ? "unknown" : graphRes?.totalEdges ?? graphRes?.edges ?? graphRes?.edgeCount ?? "unknown";
2508
3739
  const cb = healthRes?.circuitBreaker?.state || "closed";
2509
3740
  const heapMB = h?.memory ? Math.round(h.memory.heapUsed / 1048576) : 0;
2510
3741
  const uptime = h?.uptimeSeconds ? Math.round(h.uptimeSeconds) : 0;
@@ -2533,12 +3764,15 @@ async function runStatus() {
2533
3764
  lines.push(` Injected: ~${estInjectedTokens.toLocaleString()} tokens`);
2534
3765
  }
2535
3766
  if (flagsRes) {
2536
- const provider = flagsRes.provider === "llm" ? pc.green("✓ llm") : pc.yellow("✗ noop (no key)");
2537
- const embed = flagsRes.embeddingProvider === "embeddings" ? pc.green("✓ embeddings") : pc.dim("bm25-only");
3767
+ const provider = flagsRes.provider === "llm" ? pc.green("✓ llm") : pc.yellow("✗ noop (LLM features off)");
3768
+ const embedding = embeddingStatusLabel(flagsRes.embeddingProvider);
3769
+ const embed = embedding ? pc.green(`✓ ${embedding}`) : pc.dim("bm25-only");
2538
3770
  const flagRows = (flagsRes.flags || []).map((f) => ` ${f.enabled ? pc.green("✓") : pc.dim("✗")} ${pc.bold(f.key.padEnd(32))} ${f.label}`);
2539
3771
  lines.push("");
2540
3772
  lines.push(`Provider: ${provider}`);
2541
3773
  lines.push(`Embeddings: ${embed}`);
3774
+ if (typeof flagsRes.providerHint === "string") lines.push(pc.yellow(` ${flagsRes.providerHint}`));
3775
+ if (typeof flagsRes.embeddingHint === "string") lines.push(pc.yellow(` ${flagsRes.embeddingHint}`));
2542
3776
  lines.push(`Flags:`);
2543
3777
  flagRows.forEach((r) => lines.push(r));
2544
3778
  }
@@ -2549,6 +3783,21 @@ async function runStatus() {
2549
3783
  lines.push("");
2550
3784
  lines.push(`Followup rate: ${hits}/${total} (${pct}%) within ${followupRes.windowSeconds}s — directional, may overcount on refinement`);
2551
3785
  }
3786
+ const managed = savedState;
3787
+ if (managed?.kind === "compose") try {
3788
+ const composeProject = ownedComposeProject(await composeCall({
3789
+ engineUrl: managed.engineUrl,
3790
+ namespace: managed.namespace,
3791
+ functionId: "compose::status",
3792
+ file: managed.file
3793
+ }), managed.file);
3794
+ const containers = composeProject?.containers ?? [];
3795
+ const failed = composeWorkerFailureSummary(composeProject ?? null);
3796
+ if (failed) lines.push("", "Worker Compose failure (restart attempts exhausted):", ...failed.split("\n").map((line) => ` ${line}`));
3797
+ lines.push("", "Compose workers:", ...containers.map((container) => ` ${container.container ?? "?"}: ${container.state ?? "unknown"}${container.last_error ? ` — ${container.last_error}` : ""}`));
3798
+ } catch (error) {
3799
+ lines.push("", `Compose status unavailable: ${error instanceof Error ? error.message : String(error)}`);
3800
+ }
2552
3801
  p.note(lines.join("\n"), "agentmemory");
2553
3802
  } catch (err) {
2554
3803
  p.log.error(err instanceof Error ? err.message : String(err));
@@ -2594,35 +3843,56 @@ function checkClaudeCodeHooks() {
2594
3843
  if (content.includes("Loading hooks from plugin: agentmemory")) return { state: "loaded" };
2595
3844
  return { state: "not-loaded" };
2596
3845
  }
3846
+ function doctorEnvPath() {
3847
+ return process.env["AGENTMEMORY_ENV_FILE"] || join(homedir(), ".agentmemory", ".env");
3848
+ }
2597
3849
  function buildDoctorContext() {
2598
3850
  return {
2599
3851
  baseUrl: getBaseUrl(),
2600
3852
  viewerUrl: getViewerUrl(),
2601
- envPath: join(homedir(), ".agentmemory", ".env"),
2602
- pidfilePath: enginePidfilePath(),
3853
+ envPath: doctorEnvPath(),
3854
+ pidfilePath: readEngineState()?.kind === "compose" ? engineStatePath() : enginePidfilePath(),
2603
3855
  enginePath: engineStatePath(),
2604
3856
  pinnedVersion: IIPINNED_VERSION
2605
3857
  };
2606
3858
  }
2607
3859
  function buildDoctorEffects() {
2608
3860
  return {
2609
- envFileExists: () => existsSync(join(homedir(), ".agentmemory", ".env")),
3861
+ envFileExists: () => existsSync(doctorEnvPath()),
2610
3862
  readEnvFile: () => {
2611
3863
  try {
2612
- return parseEnvFile(readFileSync(join(homedir(), ".agentmemory", ".env"), "utf-8"));
3864
+ return parseEnvFile(readFileSync(doctorEnvPath(), "utf-8"));
2613
3865
  } catch {
2614
3866
  return {};
2615
3867
  }
2616
3868
  },
2617
- pidfileExists: () => existsSync(enginePidfilePath()),
3869
+ processEnv: () => shellEnv,
3870
+ pidfileExists: () => {
3871
+ const state = readEngineState();
3872
+ return state?.kind === "compose" ? Boolean(state.composePid) : existsSync(enginePidfilePath());
3873
+ },
2618
3874
  pidfilePidIsAlive: () => {
3875
+ const state = readEngineState();
3876
+ if (state?.kind === "compose") return state.composePid ? pidAlive(state.composePid) : null;
2619
3877
  const pid = readEnginePidfile();
2620
3878
  if (pid === null) return null;
2621
3879
  return pidAlive(pid);
2622
3880
  },
2623
- findIiiBinary: () => whichBinary("iii"),
2624
- localBinIiiPath: () => privateIiiPath(),
3881
+ findIiiBinary: () => {
3882
+ const state = readEngineState();
3883
+ if (state?.kind === "compose") return existsSync(state.binPath) ? state.binPath : null;
3884
+ return whichBinary("iii");
3885
+ },
3886
+ localBinIiiPath: () => {
3887
+ const state = readEngineState();
3888
+ return state?.kind === "compose" ? state.binPath : privateIiiPath();
3889
+ },
2625
3890
  iiiBinaryVersion: (binPath) => iiiBinVersion(binPath),
3891
+ externalEngineRunning: async () => {
3892
+ const state = readEngineState();
3893
+ const remoteTarget = shouldUseAgentmemoryUrl() && process.env["AGENTMEMORY_URL"];
3894
+ return !(state?.kind === "compose" && state.restPort === getRestPort() && !remoteTarget) && await isEngineRunning();
3895
+ },
2626
3896
  viewerReachable: async (timeoutMs = 2e3) => {
2627
3897
  try {
2628
3898
  await discoverViewerPort();
@@ -2678,6 +3948,14 @@ function buildDoctorEffects() {
2678
3948
  },
2679
3949
  runStop: async () => {
2680
3950
  try {
3951
+ startupTimeouts();
3952
+ if (readEngineState()?.kind === "compose") {
3953
+ await runStop();
3954
+ return {
3955
+ ok: true,
3956
+ message: "Worker Compose project stopped."
3957
+ };
3958
+ }
2681
3959
  const portPids = findEnginePidsByPort(getRestPort());
2682
3960
  const pidfilePid = readEnginePidfile();
2683
3961
  if (portPids.length === 0 && pidfilePid === null) {
@@ -2708,16 +3986,18 @@ function buildDoctorEffects() {
2708
3986
  },
2709
3987
  runStart: async () => {
2710
3988
  try {
2711
- if (!await startEngine()) return {
2712
- ok: false,
2713
- message: "startEngine() returned false"
2714
- };
2715
- const ready = await waitForEngine(15e3);
3989
+ startupTimeouts();
3990
+ beginStartup();
3991
+ if (!await startEngine()) throw new Error("iii-engine could not be started.");
3992
+ if (!await waitForEngine()) throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
3993
+ await startWorkerForEngineState(true);
2716
3994
  return {
2717
- ok: ready,
2718
- message: ready ? "Engine ready" : "Engine did not become ready within 15s"
3995
+ ok: true,
3996
+ message: "Engine and agentmemory worker ready"
2719
3997
  };
2720
3998
  } catch (err) {
3999
+ await runStartupCleanup(1);
4000
+ finishStartup();
2721
4001
  return {
2722
4002
  ok: false,
2723
4003
  message: err instanceof Error ? err.message : String(err)
@@ -2746,8 +4026,9 @@ async function passiveServerChecks() {
2746
4026
  apiFetch(base, "graph/stats", 3e3)
2747
4027
  ]);
2748
4028
  const hasLlm = flags?.provider === "llm";
2749
- const hasEmbed = flags?.embeddingProvider === "embeddings";
2750
- const graphHas = Number(graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? 0) > 0;
4029
+ const hasEmbed = embeddingStatusLabel(flags?.embeddingProvider) !== null;
4030
+ const graphNodeCount = graph?.available === false ? null : graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? null;
4031
+ const graphHas = typeof graphNodeCount === "number" && graphNodeCount > 0;
2751
4032
  checks.push({
2752
4033
  name: "Health status",
2753
4034
  ok: health?.status === "healthy",
@@ -2755,11 +4036,11 @@ async function passiveServerChecks() {
2755
4036
  }, {
2756
4037
  name: "LLM provider",
2757
4038
  ok: hasLlm,
2758
- hint: hasLlm ? void 0 : "set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env"
4039
+ hint: hasLlm ? void 0 : typeof flags?.providerHint === "string" ? flags.providerHint : "set AGENTMEMORY_LLM_PROVIDER and that provider's key (for example ANTHROPIC_API_KEY) in ~/.agentmemory/.env"
2759
4040
  }, {
2760
4041
  name: "Embedding provider",
2761
4042
  ok: hasEmbed,
2762
- hint: hasEmbed ? void 0 : "Running BM25-only. Set EMBEDDING_PROVIDER=local for on-device semantic search, or add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST"
4043
+ hint: hasEmbed ? void 0 : typeof flags?.embeddingHint === "string" ? flags.embeddingHint : "Running BM25-only. Set EMBEDDING_PROVIDER=local for on-device semantic search, or EMBEDDING_PROVIDER=openai, voyage, cohere, gemini or openrouter with that provider's key"
2763
4044
  });
2764
4045
  for (const f of flags?.flags || []) checks.push({
2765
4046
  name: f.label,
@@ -2791,7 +4072,7 @@ async function passiveServerChecks() {
2791
4072
  checks.push({
2792
4073
  name: "Knowledge graph populated",
2793
4074
  ok: graphHas,
2794
- hint: graphHas ? void 0 : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true."
4075
+ hint: graphHas ? void 0 : graphNodeCount === null ? "Graph counts are unavailable. Run agentmemory graph-index status." : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true."
2795
4076
  });
2796
4077
  return checks;
2797
4078
  }
@@ -2863,6 +4144,8 @@ async function runDoctor() {
2863
4144
  let fixed = 0;
2864
4145
  let skipped = 0;
2865
4146
  let quit = false;
4147
+ const promptsAllowed = doctorPromptsAllowed(process.env, process.stdin.isTTY, process.stdout.isTTY);
4148
+ let autoFixHintShown = false;
2866
4149
  for (const d of diagnostics) {
2867
4150
  if (quit) {
2868
4151
  skipped++;
@@ -2875,13 +4158,25 @@ async function runDoctor() {
2875
4158
  }
2876
4159
  failed++;
2877
4160
  p.log.warn(`${d.id} ✗ ${status.detail ?? ""}`.trim());
4161
+ if (!canAutoFix(d, status)) {
4162
+ p.log.info(`manual fix: ${status.manualFix ?? d.fixPreview}`);
4163
+ skipped++;
4164
+ continue;
4165
+ }
2878
4166
  p.log.info(`why: ${d.fixPreview}`);
2879
- if (d.manualOnly) p.log.info(`(manual fix only — see "${d.id}" docs)`);
2880
4167
  if (applyAll) {
2881
4168
  if ((await applyFixWithReport(d, ctx, false)).ok) fixed++;
2882
4169
  if (!(await d.check(ctx)).ok) p.log.warn(`${d.id} still failing after fix.`);
2883
4170
  continue;
2884
4171
  }
4172
+ if (!promptsAllowed) {
4173
+ skipped++;
4174
+ if (!autoFixHintShown) {
4175
+ autoFixHintShown = true;
4176
+ p.log.info("Not prompting in a non-interactive shell. Run `agentmemory doctor --all` to apply the fixes.");
4177
+ }
4178
+ continue;
4179
+ }
2885
4180
  while (true) {
2886
4181
  const action = await askFixAction(d);
2887
4182
  if (action === "fix") {
@@ -3043,7 +4338,10 @@ async function seedDemoSession(base, project, session) {
3043
4338
  p.log.warn(`observe request failed for ${obs.toolName}: ${err instanceof Error ? err.message : String(err)}`);
3044
4339
  }
3045
4340
  }
3046
- await postJsonStrict(`${base}/agentmemory/session/end`, { sessionId: session.id });
4341
+ await postJsonStrict(`${base}/agentmemory/session/end`, {
4342
+ sessionId: session.id,
4343
+ final: true
4344
+ });
3047
4345
  return stored;
3048
4346
  }
3049
4347
  async function runDemoSearch(base, query) {
@@ -3095,49 +4393,33 @@ async function runInit() {
3095
4393
  "All keys are commented out by default. Uncomment the ones you want.",
3096
4394
  "",
3097
4395
  "Common next steps:",
3098
- " 1. Pick an LLM provider key (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY / etc.)",
4396
+ " 1. Optional: to send session content to an LLM for summaries, set AGENTMEMORY_LLM_PROVIDER and that provider's key (for example ANTHROPIC_API_KEY)",
3099
4397
  " 2. Run `npx @agentmemory/agentmemory doctor` to verify the daemon sees them",
3100
4398
  " 3. Run `npx @agentmemory/agentmemory` to start the worker"
3101
4399
  ].join("\n"), "Next steps");
3102
4400
  p.outro(`Edit ${target} and you're set.`);
3103
4401
  }
3104
4402
  async function startServerForDemo() {
4403
+ startupTimeouts();
3105
4404
  await assertRuntimePortOwnership();
3106
4405
  if (await isAgentmemoryReady()) return async () => {};
3107
- const startedEngine = !await isEngineRunning();
4406
+ beginStartup();
4407
+ const startedEngine = !skipEngine && !(await isEngineRunning() || await waitForPersistedNativeEngine());
3108
4408
  if (startedEngine) {
3109
4409
  if (!await startEngine()) {
3110
4410
  p.log.error("Could not start iii-engine for the demo.");
3111
4411
  p.note(installInstructions().join("\n"), "Setup required");
3112
- process.exit(1);
4412
+ throw new Error("iii-engine could not be started for the demo.");
3113
4413
  }
3114
- if (!await waitForEngine(15e3)) {
4414
+ if (!await waitForEngine()) {
3115
4415
  printCapturedStartupStderr();
3116
- printDockerStartupLogs();
3117
- p.log.error("iii-engine did not become ready within 15s.");
3118
- process.exit(1);
4416
+ throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
3119
4417
  }
3120
4418
  }
3121
4419
  await startWorkerForEngineState();
3122
- if (!await waitForAgentmemoryReady(15e3)) {
3123
- p.log.error("agentmemory worker did not become ready within 15s.");
3124
- process.exit(1);
3125
- }
3126
4420
  return async () => {
3127
- if (!startedEngine) return;
3128
- const port = getRestPort();
3129
- const state = readEngineState();
3130
- if (state?.kind === "docker") {
3131
- await stopDockerEngine(state, port).catch(() => {});
3132
- return;
3133
- }
3134
- const pids = new Set(findEnginePidsByPort(port));
3135
- const pidfilePid = readEnginePidfile();
3136
- if (pidfilePid) pids.add(pidfilePid);
3137
- for (const pid of pids) await signalAndWait(pid, "SIGTERM", 3e3).catch(() => {});
3138
- clearEnginePidfile();
3139
- clearEngineState();
3140
- clearWorkerPidfile();
4421
+ if (importedStartupWorker && readWorkerPidfile() === process.pid) clearWorkerPidfile();
4422
+ if (startedEngine) await cleanupFailedStartup();
3141
4423
  };
3142
4424
  }
3143
4425
  async function runDemo() {
@@ -3189,7 +4471,7 @@ async function runDemoBody(base) {
3189
4471
  ...semanticHits > 0 ? [c.accent(`Notice: searching "database performance optimization"`), c.accent(`found the N+1 query fix — keyword matching can't do that.`)] : [
3190
4472
  c.dim(`Note: "database performance optimization" found nothing —`),
3191
4473
  c.dim(`set EMBEDDING_PROVIDER=local for on-device semantic recall,`),
3192
- c.dim(`or add an embedding API key in ~/.agentmemory/.env.`)
4474
+ c.dim(`or set EMBEDDING_PROVIDER and its API key in ~/.agentmemory/.env.`)
3193
4475
  ],
3194
4476
  "",
3195
4477
  `Viewer: ${c.url(getViewerUrl())}`,
@@ -3204,7 +4486,11 @@ function runCommand(command, commandArgs, options = { label: "command" }) {
3204
4486
  const result = spawnSync(command, commandArgs, {
3205
4487
  cwd: options.cwd || process.cwd(),
3206
4488
  stdio: "pipe",
3207
- encoding: "utf-8"
4489
+ encoding: "utf-8",
4490
+ ...options.timeoutMs !== void 0 ? {
4491
+ timeout: options.timeoutMs,
4492
+ killSignal: "SIGKILL"
4493
+ } : {}
3208
4494
  });
3209
4495
  if (result.status === 0) {
3210
4496
  spinner.stop(`${options.label} ${pc.green("✓")}`);
@@ -3229,7 +4515,6 @@ async function runUpgrade() {
3229
4515
  const hasPnpmLock = existsSync(join(cwd, "pnpm-lock.yaml"));
3230
4516
  const pnpmBin = whichBinary("pnpm");
3231
4517
  const npmBin = whichBinary("npm");
3232
- const dockerBin = whichBinary("docker");
3233
4518
  p.log.info(`Working directory: ${cwd}`);
3234
4519
  const requireSuccess = (ok, label) => {
3235
4520
  if (!ok) {
@@ -3269,11 +4554,6 @@ async function runUpgrade() {
3269
4554
  }
3270
4555
  if (upgradeEngine === true) await runIiiInstaller();
3271
4556
  else p.log.info("Skipped iii-engine installer.");
3272
- if (dockerBin) runCommand(dockerBin, ["pull", `iiidev/iii:${IIPINNED_VERSION}`], {
3273
- label: `Pulling iii Docker image v${IIPINNED_VERSION} (pinned)`,
3274
- optional: true
3275
- });
3276
- else p.log.info("Docker not found. Skipping Docker image refresh.");
3277
4557
  p.note([
3278
4558
  "Upgrade flow completed.",
3279
4559
  "",
@@ -3341,9 +4621,14 @@ async function waitForEngineFlush() {
3341
4621
  try {
3342
4622
  stateBackend = getStateBackend();
3343
4623
  } catch {}
4624
+ if (readEngineState()?.kind === "compose") {
4625
+ const isolatedRuntime = dataDirResolution.dataDir !== join(homedir(), ".agentmemory");
4626
+ await new Promise((resolve) => setTimeout(resolve, getStateBackend() === "redis" || isolatedRuntime ? 0 : 3500));
4627
+ return;
4628
+ }
3344
4629
  const runtimePath = runtimeConfigPath(dataDirResolution.dataDir);
3345
4630
  const configTexts = [];
3346
- for (const path of engineStateConfigPaths(join(homedir(), ".agentmemory"), runtimePath)) try {
4631
+ for (const path of engineStateConfigPaths(dataDirResolution.dataDir, runtimePath)) try {
3347
4632
  configTexts.push(readFileSync(path, "utf-8"));
3348
4633
  } catch {}
3349
4634
  const waitMs = engineFlushWaitMs(stateBackend, configTexts);
@@ -3449,80 +4734,64 @@ function findEnginePidsByPort(port) {
3449
4734
  return [];
3450
4735
  }
3451
4736
  }
3452
- async function stopDockerEngine(state, port, suppressOutro = false) {
3453
- const dockerBin = whichBinary("docker");
3454
- if (!dockerBin) {
3455
- p.log.error("Engine was started via Docker, but `docker` is no longer on PATH. Lifecycle state was preserved.");
3456
- process.exit(1);
3457
- }
3458
- const inspection = inspectOwnedDockerEngine(state);
3459
- if (inspection.status === "unavailable") {
3460
- p.log.error(`The saved Docker engine cannot be verified: ${inspection.reason}. Refusing to stop or clear ownership state.`);
3461
- process.exit(1);
3462
- }
3463
- const resolvedState = persistDockerInspection(state, inspection);
3464
- const workerPid = readWorkerPidfile();
3465
- if (workerPid && !await stopWorkerPid(workerPid, 5e3)) {
3466
- p.log.error("The agentmemory worker could not be stopped; Docker ownership state was preserved.");
3467
- process.exit(1);
3468
- }
3469
- clearWorkerPidfile();
3470
- if (inspection.status === "running" || inspection.status === "stopped") {
3471
- if (inspection.status === "running" && !runCommand(dockerBin, [
3472
- "stop",
3473
- "--time",
3474
- "10",
3475
- inspection.containerId
3476
- ], { label: `Stopping preserved Docker container ${inspection.containerId}` })) {
3477
- p.log.error("The Docker container could not be stopped; ownership state was preserved.");
3478
- process.exit(1);
3479
- }
3480
- clearEnginePidfile();
3481
- writeEngineState(resolvedState);
3482
- if (!suppressOutro) p.outro("Stopped. The validated Docker container and data mount were preserved for a lossless restart.");
3483
- return;
3484
- }
3485
- const projectName = inspection.projectName ?? resolvedState.projectName;
3486
- if (!projectName) {
3487
- p.log.error("No verified Docker Compose project name is available. Refusing an unscoped stop.");
3488
- process.exit(1);
3489
- }
3490
- const effectiveComposeFile = existsSync(inspection.composeFile) ? inspection.composeFile : discoverComposeFile();
3491
- if (!effectiveComposeFile) {
3492
- p.log.error("No agentmemory compose file is available. Docker ownership state was preserved.");
3493
- process.exit(1);
3494
- }
3495
- const composeText = readFileSync(effectiveComposeFile, "utf-8");
3496
- const ownServices = ["iii-engine", "iii-init"].filter((service) => new RegExp(`^\\s+${service}:`, "m").test(composeText));
3497
- if (ownServices.length === 0) {
3498
- p.log.error(`${effectiveComposeFile} does not define iii-engine or iii-init. Refusing an unscoped stop.`);
4737
+ async function runStop() {
4738
+ const savedState = readEngineState();
4739
+ if (savedState?.kind === "docker") {
4740
+ p.intro("agentmemory stop");
4741
+ p.log.error(legacyDockerStateMessage(savedState));
3499
4742
  process.exit(1);
3500
4743
  }
3501
- const composeCommand = ["docker", ...dockerComposeArgs(effectiveComposeFile, projectName, [])].join(" ");
3502
- if (runCommand(dockerBin, dockerComposeArgs(effectiveComposeFile, projectName, [
3503
- "rm",
3504
- "-s",
3505
- "-f",
3506
- ...ownServices
3507
- ]), { label: `${composeCommand} rm -s -f ${ownServices.join(" ")}` })) {
3508
- clearEnginePidfile();
3509
- clearEngineState();
3510
- } else {
3511
- p.log.error(`docker compose rm failed. The engine may still be running on :${port}. Inspect with:\n ${composeCommand} ps`);
4744
+ if (savedState?.kind === "compose" && engineStateRestPort(savedState) !== getRestPort()) {
4745
+ p.intro("agentmemory stop");
4746
+ p.log.error(`Saved Compose lifecycle state belongs to REST port ${engineStateRestPort(savedState)}, not requested port ${getRestPort()}. Refusing to signal processes or clear metadata.`);
3512
4747
  process.exit(1);
3513
4748
  }
3514
- if (!suppressOutro) p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
3515
- }
3516
- async function runStop() {
3517
4749
  await assertRuntimePortOwnership();
3518
4750
  p.intro("agentmemory stop");
3519
4751
  const port = getRestPort();
3520
4752
  const state = readEngineState();
3521
4753
  const running = await isEngineRunning();
3522
4754
  const force = args.includes("--force");
3523
- if (state?.kind === "docker") {
3524
- if (!running) p.log.info(`No engine responding on port ${port}.`);
3525
- await stopDockerEngine(state, port);
4755
+ if (state?.kind === "compose") {
4756
+ const ownedDaemonAlive = Boolean(state.composePid && pidAlive(state.composePid));
4757
+ if (ownedDaemonAlive && !composePidMatches(state.composePid, state)) {
4758
+ p.log.error(`Recorded Compose pid ${state.composePid} does not match the saved daemon identity; refusing to invoke Compose control or signal it. Lifecycle state was preserved.`);
4759
+ process.exit(1);
4760
+ }
4761
+ if (running && !ownedDaemonAlive) {
4762
+ p.log.error("The REST endpoint is responding, but the recorded Compose daemon is not alive. Refusing to stop or clear an engine that may be unrelated; restore the owning daemon or inspect the endpoint manually.");
4763
+ process.exit(1);
4764
+ }
4765
+ if (running && ownedDaemonAlive) try {
4766
+ await composeCall({
4767
+ engineUrl: state.engineUrl,
4768
+ namespace: state.namespace,
4769
+ functionId: "compose::down",
4770
+ file: state.file,
4771
+ timeoutMs: composeDownTimeoutMs()
4772
+ });
4773
+ } catch (err) {
4774
+ const message = err instanceof Error ? err.message : String(err);
4775
+ if (!message.includes("ENGINE_RESTART_REQUIRED")) {
4776
+ p.log.error(`compose::down failed for ${state.file}: ${message}. Lifecycle state was preserved.`);
4777
+ process.exit(1);
4778
+ }
4779
+ p.log.warn("Compose requested an engine restart while stopping; terminating its verified owning daemon to finish shutdown.");
4780
+ }
4781
+ if (ownedDaemonAlive) {
4782
+ if (!await terminateComposeChildren(state)) {
4783
+ p.log.error(`One or more child workers of Compose ${state.composePid} did not stop; lifecycle state was preserved.`);
4784
+ process.exit(1);
4785
+ }
4786
+ if (!await signalAndWait(state.composePid, "SIGTERM", 2e4)) {
4787
+ p.log.error(`Worker Compose daemon pid ${state.composePid} did not stop; lifecycle state was preserved. Inspect ${state.logFile}.`);
4788
+ process.exit(1);
4789
+ }
4790
+ }
4791
+ clearEnginePidfile();
4792
+ clearWorkerPidfile();
4793
+ clearEngineState();
4794
+ p.outro("Stopped Worker Compose, engine, and agentmemory workers. Data was preserved.");
3526
4795
  return;
3527
4796
  }
3528
4797
  const portPids = findEnginePidsByPort(port);
@@ -3558,14 +4827,6 @@ async function runStop() {
3558
4827
  p.log.info(`Preserving ~/.agentmemory/iii.pid + worker.pid. Investigate before manual cleanup:\n ps -p ${[...survivors].join(",")} -o pid,ppid,comm,etime\n ${IS_WINDOWS ? "netstat -ano | findstr :" + port : "lsof -i :" + port}`);
3559
4828
  process.exit(1);
3560
4829
  }
3561
- if (!state) {
3562
- const compose = discoverComposeFile();
3563
- if (compose && pidfilePid === null) if (force) p.log.warn(`--force: bypassing Docker-heuristic guard. Falling back to native pidfile + lsof on :${port}.`);
3564
- else {
3565
- p.log.error(`Engine is running on :${port} but no pidfile or state file is present. It may have been started via Docker compose by a different shell. Refusing to signal host PIDs.\n\nStop it with:\n docker compose -f ${compose} down\n\nOr re-run with --force to signal whatever lsof finds on :${port}, or AGENTMEMORY_USE_DOCKER=1 to record state next time.`);
3566
- process.exit(1);
3567
- }
3568
- }
3569
4830
  const candidates = /* @__PURE__ */ new Set();
3570
4831
  if (pidfilePid) candidates.add(pidfilePid);
3571
4832
  for (const pid of portPids) candidates.add(pid);
@@ -3600,7 +4861,7 @@ async function runStop() {
3600
4861
  clearWorkerPidfile();
3601
4862
  if (skippedForeign.length > 0) {
3602
4863
  const list = skippedForeign.map((sf) => ` pid ${sf.pid} ${sf.comm}`).join("\n");
3603
- p.log.error(`Refused to signal process(es) holding :${port} that are not the iii engine:\n${list}\n\nIf the engine runs in Docker, stop it there:\n docker compose ps && docker compose rm -s -f <service>\n\nOr re-run with --force to signal them anyway.`);
4864
+ p.log.error(`Refused to signal process(es) holding :${port} that are not the iii engine:\n${list}\n\nInspect those processes and stop the engine with its owning runtime.\nOr re-run with --force to signal them anyway.`);
3604
4865
  process.exit(1);
3605
4866
  }
3606
4867
  if (!allStopped) {
@@ -3610,10 +4871,10 @@ async function runStop() {
3610
4871
  p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
3611
4872
  }
3612
4873
  async function runMcp() {
3613
- await import("./standalone-DKCdrf1d.mjs");
4874
+ await import("./standalone-BYo5USDb.mjs");
3614
4875
  }
3615
4876
  async function runConnectCmd() {
3616
- const { runConnect } = await import("./connect-B4_ca6BD.mjs").then((n) => n.n);
4877
+ const { runConnect } = await import("./connect-BX3njQ3Y.mjs").then((n) => n.n);
3617
4878
  await runConnect(args.slice(1));
3618
4879
  }
3619
4880
  async function runImportJsonl() {
@@ -3772,27 +5033,20 @@ async function runRemove() {
3772
5033
  const force = args.includes("--force");
3773
5034
  const keepData = args.includes("--keep-data");
3774
5035
  const ownedState = readEngineState();
5036
+ if (ownedState?.kind === "compose") {
5037
+ p.log.error("Stop the Worker Compose project with `agentmemory stop` before removing agentmemory; its data and daemon ownership are preserved.");
5038
+ process.exit(1);
5039
+ }
3775
5040
  if (ownedState?.kind === "docker") {
3776
- const inspection = inspectOwnedDockerEngine(ownedState);
3777
- if (inspection.status === "running" || inspection.status === "stopped") {
3778
- persistDockerInspection(ownedState, inspection);
3779
- if (!keepData) {
3780
- p.log.error(`Docker-backed removal requires --keep-data so the validated ${inspection.dataMountType} data mount (${inspection.dataMountSource}) and its recovery metadata remain usable. Re-run \`agentmemory remove --keep-data\`; destructive Docker data deletion is intentionally not automated.`);
3781
- process.exit(1);
3782
- }
3783
- } else {
3784
- const detail = inspection.status === "unavailable" ? inspection.reason : "the owned container is missing";
3785
- p.log.error(`remove cannot safely reconcile the saved Docker ownership (${detail}). Lifecycle state was preserved for manual recovery.`);
3786
- process.exit(1);
3787
- }
5041
+ p.log.error(legacyDockerStateMessage(ownedState));
5042
+ process.exit(1);
3788
5043
  }
3789
5044
  const home = homedir();
3790
5045
  const connectManifest = loadConnectManifest(home);
3791
5046
  const localBinIiiVersion = probeLocalBinIiiVersion(home);
3792
5047
  const options = {
3793
5048
  force,
3794
- keepData,
3795
- preserveRuntimeState: ownedState?.kind === "docker"
5049
+ keepData
3796
5050
  };
3797
5051
  const plan = buildRemovePlan({
3798
5052
  home,
@@ -3838,11 +5092,6 @@ async function runRemove() {
3838
5092
  }
3839
5093
  }
3840
5094
  if (item.id === "stop-engine") {
3841
- if (ownedState?.kind === "docker") {
3842
- await stopDockerEngine(ownedState, getRestPort(), true);
3843
- p.log.success("stopped Docker engine; container, data mount, and recovery state preserved");
3844
- continue;
3845
- }
3846
5095
  const stopped = await stopNativeEngineForRemoval();
3847
5096
  if (!stopped.ok) {
3848
5097
  p.log.error(`Removal aborted: ${stopped.reason}.`);
@@ -3859,52 +5108,13 @@ async function runRemove() {
3859
5108
  p.outro("Done. agentmemory cleanly removed. The npm package itself: npm uninstall -g @agentmemory/agentmemory");
3860
5109
  }
3861
5110
  async function runConsole() {
3862
- const iiiBin = pickCompatibleIii([whichBinary("iii"), ...fallbackIiiPaths().filter((candidate) => existsSync(candidate))]);
3863
- if (!iiiBin) {
3864
- p.log.error(`No iii v${IIPINNED_VERSION} found. Start agentmemory once so it installs the pinned engine into ${privateIiiPath()}, then run this again.`);
3865
- process.exit(1);
3866
- }
3867
- let parsed;
3868
- try {
3869
- parsed = parseConsoleArgs(args.slice(1), defaultConsolePort(getConfiguredViewerPort()));
3870
- } catch (err) {
3871
- p.log.error(err instanceof Error ? err.message : String(err));
3872
- process.exit(1);
3873
- }
3874
- const launch = {
3875
- iiiBin,
3876
- consolePort: parsed.consolePort,
3877
- restPort: getRestPort(),
3878
- streamPort: getStreamPort(),
3879
- enginePort: getEnginePort(),
3880
- extraArgs: parsed.extraArgs
3881
- };
3882
- if (!await isEngineRunning()) p.log.warn(`No engine responding on port ${launch.restPort}. The console starts anyway and connects once agentmemory is up.`);
3883
- p.log.info(`iii console: http://127.0.0.1:${launch.consolePort} (engine REST ${launch.restPort}, streams ${launch.streamPort}, bridge ${launch.enginePort}). The first run downloads the console binary next to the pinned iii.`);
3884
- const child = spawn(iiiBin, consoleArgs(launch), {
3885
- stdio: "inherit",
3886
- detached: true
3887
- });
3888
- const forward = (signal) => {
3889
- try {
3890
- process.kill(-child.pid, signal);
3891
- } catch {
3892
- child.kill(signal);
3893
- }
3894
- };
3895
- for (const signal of [
3896
- "SIGINT",
3897
- "SIGTERM",
3898
- "SIGHUP"
3899
- ]) process.on(signal, () => forward(signal));
3900
- const code = await new Promise((resolve) => {
3901
- child.on("exit", (status, signal) => resolve(status ?? (signal ? 1 : 0)));
3902
- child.on("error", (err) => {
3903
- p.log.error(err.message);
3904
- resolve(1);
3905
- });
3906
- });
3907
- process.exit(code);
5111
+ await discoverViewerPort();
5112
+ p.log.warn("iii 0.25 retired the bundled engine console; --console-port and console launch flags no longer apply.");
5113
+ p.note([
5114
+ `AgentMemory viewer: ${getViewerUrl()} (start agentmemory first if it is offline).`,
5115
+ "Use agentmemory status for runtime health and worker status.",
5116
+ "Engine console migration: https://github.com/iii-hq/iii/blob/iii/v0.25.0/docs/using-iii/console.mdx"
5117
+ ].join("\n"), "Console migration");
3908
5118
  }
3909
5119
  async function runCapture() {
3910
5120
  const code = await runCaptureCommand({
@@ -3914,6 +5124,23 @@ async function runCapture() {
3914
5124
  });
3915
5125
  process.exit(code);
3916
5126
  }
5127
+ async function runCompression() {
5128
+ const base = getBaseUrl();
5129
+ const code = await runCompressionCommand({
5130
+ base,
5131
+ args: args.slice(1),
5132
+ secret: resolveClientSecret(base)
5133
+ });
5134
+ process.exit(code);
5135
+ }
5136
+ async function runGraphIndex() {
5137
+ const base = getBaseUrl();
5138
+ process.exit(await runGraphIndexCommand({
5139
+ base,
5140
+ args: args.slice(1),
5141
+ secret: resolveClientSecret(base)
5142
+ }));
5143
+ }
3917
5144
  const commands = {
3918
5145
  init: runInit,
3919
5146
  connect: runConnectCmd,
@@ -3926,16 +5153,19 @@ const commands = {
3926
5153
  remove: runRemove,
3927
5154
  mcp: runMcp,
3928
5155
  "import-jsonl": runImportJsonl,
3929
- capture: runCapture
5156
+ capture: runCapture,
5157
+ compression: runCompression,
5158
+ "graph-index": runGraphIndex
3930
5159
  };
3931
5160
  const first = args[0] ?? "";
3932
5161
  async function unknownCommand() {
3933
5162
  p.log.error(`Unknown command: ${first}. Supported: ${Object.keys(commands).join(", ")}. Run \`agentmemory\` with no arguments to start the memory server, or \`agentmemory --help\` for usage.`);
3934
5163
  process.exit(1);
3935
5164
  }
3936
- (commands[first] ?? (first && !first.startsWith("-") ? unknownCommand : main))().catch((err) => {
5165
+ (commands[first] ?? (first && !first.startsWith("-") ? unknownCommand : main))().catch(async (err) => {
3937
5166
  p.log.error(err instanceof Error ? err.message : String(err));
3938
- process.exit(1);
5167
+ const exitCode = await runStartupCleanup(1).catch(() => 1);
5168
+ process.exit(exitCode);
3939
5169
  });
3940
5170
  //#endregion
3941
5171
  export { discoverViewerPort };