@agentmemory/agentmemory 0.9.30 → 0.9.31

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 (331) hide show
  1. package/.env.example +39 -33
  2. package/AGENTS.md +8 -8
  3. package/README.md +173 -558
  4. package/dist/.env.example +39 -33
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +1741 -966
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/compression-recovery-CnhLQC6N.mjs +5912 -0
  9. package/dist/compression-recovery-CnhLQC6N.mjs.map +1 -0
  10. package/dist/{connect-B4_ca6BD.mjs → connect-BSLIAMpw.mjs} +874 -267
  11. package/dist/connect-BSLIAMpw.mjs.map +1 -0
  12. package/dist/hooks/_capture.d.mts +5 -2
  13. package/dist/hooks/_capture.d.mts.map +1 -1
  14. package/dist/hooks/_capture.mjs +55 -19
  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 +25 -5
  19. package/dist/hooks/post-commit.mjs.map +1 -1
  20. package/dist/hooks/post-tool-failure.mjs +12 -4
  21. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  22. package/dist/hooks/post-tool-use.mjs +12 -4
  23. package/dist/hooks/post-tool-use.mjs.map +1 -1
  24. package/dist/hooks/pre-compact.mjs +37 -10
  25. package/dist/hooks/pre-compact.mjs.map +1 -1
  26. package/dist/hooks/pre-tool-use.mjs +25 -5
  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 +47 -16
  31. package/dist/hooks/session-end.mjs.map +1 -1
  32. package/dist/hooks/session-start.mjs +37 -10
  33. package/dist/hooks/session-start.mjs.map +1 -1
  34. package/dist/hooks/stop.mjs +74 -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 +18 -6
  39. package/dist/hooks/subagent-stop.mjs.map +1 -1
  40. package/dist/hooks/task-completed.mjs +13 -5
  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 +8539 -4203
  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 +1145 -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-D9GwO0xp.mjs} +535 -434
  53. package/dist/secret-store-D9GwO0xp.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-DaMtDzt_.mjs} +4087 -3788
  57. package/dist/src-DaMtDzt_.mjs.map +1 -0
  58. package/dist/{standalone-DKCdrf1d.mjs → standalone-BlqshyZ5.mjs} +213 -56
  59. package/dist/standalone-BlqshyZ5.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 +626 -73
  63. package/dist/standalone.mjs.map +1 -1
  64. package/dist/viewer/index.html +288 -140
  65. package/integrations/hermes/README.md +84 -0
  66. package/integrations/hermes/__init__.py +669 -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 +1450 -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 +33 -0
  79. package/integrations/omp/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  80. package/integrations/omp/skills/agentmemory-config/REFERENCE.md +90 -0
  81. package/integrations/omp/skills/agentmemory-config/SKILL.md +78 -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 +49 -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 +42 -0
  88. package/integrations/omp/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  89. package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +158 -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 +88 -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 +33 -0
  132. package/integrations/openclaw/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  133. package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +90 -0
  134. package/integrations/openclaw/skills/agentmemory-config/SKILL.md +78 -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 +49 -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 +42 -0
  141. package/integrations/openclaw/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  142. package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +158 -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 +45 -51
  177. package/integrations/pi/dist/index.mjs +1379 -0
  178. package/integrations/pi/index.ts +138 -171
  179. package/integrations/pi/package.json +16 -6
  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 +33 -0
  185. package/integrations/pi/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  186. package/integrations/pi/skills/agentmemory-config/REFERENCE.md +90 -0
  187. package/integrations/pi/skills/agentmemory-config/SKILL.md +78 -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 +49 -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 +42 -0
  194. package/integrations/pi/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  195. package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +158 -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 +32 -11
  229. package/plugin/.claude-plugin/plugin.json +1 -1
  230. package/plugin/.codex-plugin/plugin.json +4 -3
  231. package/plugin/.devin-plugin/plugin.json +5 -3
  232. package/plugin/cursor/hooks.json +23 -8
  233. package/plugin/hooks.json +67 -0
  234. package/plugin/opencode/README.md +1 -1
  235. package/plugin/opencode/agentmemory-capture.ts +25 -11
  236. package/plugin/opencode/plugin.json +1 -1
  237. package/plugin/plugin.json +1 -1
  238. package/plugin/scripts/_capture.d.mts +5 -2
  239. package/plugin/scripts/_capture.d.mts.map +1 -1
  240. package/plugin/scripts/_capture.mjs +55 -19
  241. package/plugin/scripts/_capture.mjs.map +1 -1
  242. package/plugin/scripts/cursor/run-detached.d.mts +1 -0
  243. package/plugin/scripts/cursor/run-detached.mjs +786 -0
  244. package/plugin/scripts/cursor/run-detached.mjs.map +1 -0
  245. package/plugin/scripts/cursor/run-hook.d.mts +1 -0
  246. package/plugin/scripts/cursor/run-hook.mjs +704 -0
  247. package/plugin/scripts/cursor/run-hook.mjs.map +1 -0
  248. package/plugin/scripts/notification.mjs +20 -6
  249. package/plugin/scripts/notification.mjs.map +1 -1
  250. package/plugin/scripts/plugin-bridge.mjs +37 -6
  251. package/plugin/scripts/post-commit.mjs +25 -5
  252. package/plugin/scripts/post-commit.mjs.map +1 -1
  253. package/plugin/scripts/post-tool-failure.mjs +12 -4
  254. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  255. package/plugin/scripts/post-tool-use.mjs +12 -4
  256. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  257. package/plugin/scripts/pre-compact.mjs +37 -10
  258. package/plugin/scripts/pre-compact.mjs.map +1 -1
  259. package/plugin/scripts/pre-tool-use.mjs +25 -5
  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 +47 -16
  264. package/plugin/scripts/session-end.mjs.map +1 -1
  265. package/plugin/scripts/session-start.mjs +37 -10
  266. package/plugin/scripts/session-start.mjs.map +1 -1
  267. package/plugin/scripts/stop.mjs +74 -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 +18 -6
  272. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  273. package/plugin/scripts/task-completed.mjs +13 -5
  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 +7 -6
  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 +4 -4
  280. package/plugin/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  281. package/plugin/skills/agentmemory-config/REFERENCE.md +12 -15
  282. package/plugin/skills/agentmemory-config/SKILL.md +7 -3
  283. package/plugin/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  284. package/plugin/skills/agentmemory-hooks/SKILL.md +15 -5
  285. package/plugin/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  286. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +4 -4
  287. package/plugin/skills/agentmemory-mcp-tools/SKILL.md +6 -3
  288. package/plugin/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  289. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +9 -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
package/dist/cli.mjs CHANGED
@@ -1,17 +1,20 @@
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-BSLIAMpw.mjs";
3
+ import { s as generateId } from "./keyed-mutex-DCUh7PG-.mjs";
4
+ import { A as engineFlushWaitMs, C as III_PINNED_VERSION, L as detectLlmProviderKind, M as redactCredentialUrls, N as runtimeConfigPath, O as setBootVerbose, X as getStateBackend, Z as getStateSaveIntervalMs, b as ESSENTIAL_TOOLS, et as hydrateProcessEnvFromFile, gt as parseEnvContent, i as resolveClientSecret, j as engineStateConfigPaths, q as getRedisUrl, t as bearerHeaders, w as VERSION, x as getAllTools } from "./secret-store-D9GwO0xp.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-CnhLQC6N.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 { delimiter, dirname, isAbsolute, join, resolve } 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
13
  import { createHash } 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
16
19
  /** Common placeholder values shipped in .env.example. */
17
20
  const PLACEHOLDER_VALUES = new Set([
@@ -31,20 +34,7 @@ const PROVIDER_KEY_NAMES = [
31
34
  "OPENROUTER_API_KEY",
32
35
  "MINIMAX_API_KEY"
33
36
  ];
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
- }
37
+ const parseEnvFile = parseEnvContent;
48
38
  /** Returns the list of provider keys that look real (non-placeholder). */
49
39
  function realProviderKeys(env) {
50
40
  return PROVIDER_KEY_NAMES.filter((k) => {
@@ -71,7 +61,7 @@ function buildDiagnostics(effects) {
71
61
  id: "env-missing",
72
62
  message: "~/.agentmemory/.env is missing.",
73
63
  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.",
64
+ 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
65
  check: async () => ({
76
66
  ok: effects.envFileExists(),
77
67
  detail: effects.envFileExists() ? void 0 : "no env file"
@@ -80,18 +70,32 @@ function buildDiagnostics(effects) {
80
70
  },
81
71
  {
82
72
  id: "no-llm-provider-key",
83
- message: "No LLM provider API key found in ~/.agentmemory/.env.",
73
+ message: "No LLM provider API key found in ~/.agentmemory/.env or the environment.",
84
74
  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.",
75
+ moreInfo: "Set at least one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY. Placeholder values count as missing. OPENAI_API_KEY counts only while OPENAI_API_KEY_FOR_LLM is not false, and 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, and counts even when the file is absent.",
86
76
  check: async () => {
87
- if (!effects.envFileExists()) return {
77
+ const file = effects.readEnvFile();
78
+ const env = { ...file };
79
+ for (const [k, v] of Object.entries(effects.processEnv())) if (v !== void 0) env[k] = v;
80
+ for (const k of placeholderProviderKeys(env)) delete env[k];
81
+ if (detectLlmProviderKind(env) !== "llm") return {
88
82
  ok: false,
89
- detail: "env file missing (run env-missing fix first)"
83
+ detail: effects.envFileExists() ? "no provider key set" : "no provider key set (run env-missing fix first)"
84
+ };
85
+ const real = realProviderKeys(env).filter((k) => detectLlmProviderKind({
86
+ [k]: env[k],
87
+ OPENAI_API_KEY_FOR_LLM: env.OPENAI_API_KEY_FOR_LLM
88
+ }) === "llm");
89
+ const fromEnv = real.filter((k) => file[k] !== env[k]);
90
+ const named = real.map((k) => fromEnv.includes(k) ? `${k} (environment)` : k);
91
+ if (named.length === 0) return {
92
+ ok: true,
93
+ detail: "agent-sdk fallback, no API key"
90
94
  };
91
- const real = realProviderKeys(effects.readEnvFile());
95
+ const caveat = fromEnv.length > 0 ? "; a daemon started elsewhere will not see it" : "";
92
96
  return {
93
- ok: real.length > 0,
94
- detail: real.length > 0 ? `found: ${real.join(", ")}` : "no provider key set"
97
+ ok: true,
98
+ detail: `found: ${named.join(", ")}${caveat}`
95
99
  };
96
100
  },
97
101
  fix: (ctx) => effects.openEditor(ctx.envPath)
@@ -99,8 +103,8 @@ function buildDiagnostics(effects) {
99
103
  {
100
104
  id: "engine-version-mismatch",
101
105
  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.",
106
+ fixPreview: "Reinstall the pinned iii-engine and restart the managed Worker Compose project.",
107
+ 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.",
104
108
  check: async (ctx) => {
105
109
  const bin = effects.findIiiBinary();
106
110
  if (!bin) return {
@@ -142,8 +146,8 @@ function buildDiagnostics(effects) {
142
146
  {
143
147
  id: "stale-pidfile",
144
148
  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.",
149
+ fixPreview: "Verify the Compose daemon ownership record and restart only the managed project.",
150
+ 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
151
  check: async () => {
148
152
  if (!effects.pidfileExists()) return {
149
153
  ok: true,
@@ -400,15 +404,22 @@ function portFlagSuffix(restPort, instance) {
400
404
  if (instance > 0 && restPort === DEFAULT_REST_PORT + instance * 100) return ` --instance ${instance}`;
401
405
  return restPort === DEFAULT_REST_PORT ? "" : ` --port ${restPort}`;
402
406
  }
407
+ //#endregion
408
+ //#region src/cli/embedding-status.ts
409
+ function embeddingStatusLabel(provider) {
410
+ if (typeof provider !== "string") return null;
411
+ const value = provider.trim();
412
+ return value && value !== "none" ? value : null;
413
+ }
403
414
  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"
415
+ const III_RELEASE_SHA256 = { "0.25.0": {
416
+ "iii-aarch64-apple-darwin.tar.gz": "5f23087a41ce1cf86f9859d2f430d1809a1e048094e69b1df0cdf1ebefc2a985",
417
+ "iii-x86_64-apple-darwin.tar.gz": "e2ac4ef109ddbf459b4683171c6b5b9c8366b16dac2e1f8c4fe08b4a4e19d49a",
418
+ "iii-x86_64-unknown-linux-gnu.tar.gz": "4c4d14063eb55d79663db553e960497731420df6d78eca3aafe670e2fbaba8e2",
419
+ "iii-aarch64-unknown-linux-gnu.tar.gz": "5e52d4738a0beb609f4d32970d37be7c00d4848ee9d06300248498a00ea4bf0e",
420
+ "iii-armv7-unknown-linux-gnueabihf.tar.gz": "9ee840351ff62c889b929b618f8cbb2d66d616cecf82cde7c26d4618aa137508",
421
+ "iii-x86_64-pc-windows-msvc.zip": "b80ea5c3408879380b6ce95f7b2855ce161d68642aeec8f7b54f104794cbb322",
422
+ "iii-aarch64-pc-windows-msvc.zip": "2b1e222b90ee18ed143186e8b307d8ad2076f44ea00fa7a7d071bd3c87cde002"
412
423
  } };
413
424
  function expectedIiiSha256(version, asset, checksums = III_RELEASE_SHA256) {
414
425
  return checksums[version]?.[asset] ?? null;
@@ -518,66 +529,147 @@ function createStartupStderrCapture(maxBytes = 16 * 1024) {
518
529
  };
519
530
  }
520
531
  //#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;
532
+ //#region src/cli/readiness.ts
533
+ const DEFAULT_ENGINE_TIMEOUT_MS = 12e4;
534
+ const DEFAULT_WORKER_TIMEOUT_MS = 3e5;
535
+ const MAX_STARTUP_TIMEOUT_MS = 36e5;
536
+ function startupTimeouts(env = process.env) {
537
+ const parse = (key, fallback) => {
538
+ const raw = env[key];
539
+ if (raw === void 0) return fallback;
540
+ const value = Number(raw);
541
+ 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.`);
542
+ return value;
543
+ };
544
+ const ownership = env.AGENTMEMORY_ENGINE_MANAGES_WORKER;
545
+ if (ownership !== void 0 && ownership !== "true" && ownership !== "false") throw new Error("AGENTMEMORY_ENGINE_MANAGES_WORKER must be true or false.");
546
+ return {
547
+ engineMs: parse("AGENTMEMORY_ENGINE_READY_TIMEOUT_MS", DEFAULT_ENGINE_TIMEOUT_MS),
548
+ workerMs: parse("AGENTMEMORY_READY_TIMEOUT_MS", DEFAULT_WORKER_TIMEOUT_MS),
549
+ engineManagesWorker: ownership === void 0 ? void 0 : ownership === "true"
550
+ };
540
551
  }
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;
552
+ function configStartsAgentmemoryWorker(raw, workerEntry, engineCwd) {
553
+ const lines = raw.split(/\r?\n/);
554
+ let workerIndent;
555
+ let execIndent;
556
+ for (const line of lines) {
557
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
558
+ const indent = line.length - line.trimStart().length;
559
+ if (workerIndent !== void 0 && indent <= workerIndent) {
560
+ workerIndent = void 0;
561
+ execIndent = void 0;
550
562
  }
551
- if (arg.startsWith("--console-port=")) {
552
- consolePort = readPort(arg.slice(15), "--console-port");
563
+ if (/^\s*-\s+name:\s*(?:iii-exec|"iii-exec"|'iii-exec')(?:\s+#.*)?\s*$/.test(line)) {
564
+ workerIndent = indent;
553
565
  continue;
554
566
  }
555
- if (GLOBAL_VALUE_FLAGS.has(arg)) {
556
- i++;
567
+ if (workerIndent === void 0) continue;
568
+ if (execIndent !== void 0 && indent <= execIndent) execIndent = void 0;
569
+ if (/^\s*exec:\s*(?:#.*)?$/.test(line)) {
570
+ execIndent = indent;
557
571
  continue;
558
572
  }
559
- if ([...GLOBAL_VALUE_FLAGS].some((flag) => arg.startsWith(`${flag}=`))) continue;
560
- if (GLOBAL_BOOLEAN_FLAGS.has(arg)) continue;
561
- extraArgs.push(arg);
573
+ if (execIndent === void 0) continue;
574
+ const scalar = line.match(/^\s*-\s+(.+)$/)?.[1]?.trim();
575
+ if (!scalar) continue;
576
+ const quoted = scalar.match(/^(["'])(.*?)\1(?:\s+#.*)?$/);
577
+ const tokens = (quoted ? quoted[2] : scalar).match(/"[^"]*"|'[^']*'|[^\s]+/g)?.map((token) => token.replace(/^("|')(.*)\1$/, "$2").replaceAll("\\", "/"));
578
+ if (!tokens) continue;
579
+ const comment = tokens.findIndex((token) => token.startsWith("#"));
580
+ if (comment !== -1) tokens.splice(comment);
581
+ const runtime = tokens[0]?.split("/").at(-1);
582
+ if (runtime !== "node" && runtime !== "node.exe" && runtime !== "tsx") continue;
583
+ if (runtime !== "tsx") while (tokens[1] && /^(?:--max[-_]old[-_]space[-_]size=\d+|--enable-source-maps|--no-warnings)$/.test(tokens[1])) tokens.splice(1, 1);
584
+ const entry = tokens.length === 2 ? tokens[1] : tokens.length === 4 && tokens[1] === "--import" && tokens[2] === "tsx" ? tokens[3] : void 0;
585
+ if (entry === workerEntry.replaceAll("\\", "/")) return true;
586
+ if (entry === "dist/index.mjs" || entry === "./dist/index.mjs" || entry === "src/index.ts" || entry === "./src/index.ts") {
587
+ if (engineCwd === void 0) return true;
588
+ try {
589
+ if (statSync(join(engineCwd, entry)).isFile()) return true;
590
+ } catch {}
591
+ }
562
592
  }
563
- return {
564
- consolePort,
565
- extraArgs
566
- };
593
+ return false;
567
594
  }
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
- ];
595
+ function startupTimeoutMessage(kind, timeoutMs) {
596
+ const name = kind === "engine" ? "iii-engine" : "agentmemory worker";
597
+ const variable = kind === "engine" ? "AGENTMEMORY_ENGINE_READY_TIMEOUT_MS" : "AGENTMEMORY_READY_TIMEOUT_MS";
598
+ return `${name} did not become ready within ${timeoutMs / 1e3}s. Set ${variable} to allow more startup time.`;
599
+ }
600
+ function startupDeadline(timeoutMs) {
601
+ const deadline = performance.now() + timeoutMs;
602
+ return { remaining: () => Math.max(0, deadline - performance.now()) };
603
+ }
604
+ /** Probe the loopback REST listener owned by this managed Compose instance.
605
+ * Client commands may honor AGENTMEMORY_URL, but startup readiness must always
606
+ * target the endpoint that this process asked Compose to create. */
607
+ function managedComposeReadinessUrl(restPort) {
608
+ if (!Number.isInteger(restPort) || restPort < 1 || restPort > 65535) throw new Error(`Invalid managed Compose REST port: ${restPort}`);
609
+ return `http://127.0.0.1:${restPort}`;
610
+ }
611
+ async function withinStartupDeadline(deadline, work, maximumMs = Infinity) {
612
+ const remaining = Math.min(deadline.remaining(), maximumMs);
613
+ if (remaining <= 0) return { completed: false };
614
+ const controller = new AbortController();
615
+ let timer;
616
+ const expired = new Promise((resolve) => {
617
+ timer = setTimeout(() => {
618
+ controller.abort();
619
+ resolve({ completed: false });
620
+ }, Math.ceil(remaining));
621
+ });
622
+ try {
623
+ const result = await Promise.race([Promise.resolve().then(() => work(controller.signal)).then((value) => ({
624
+ completed: true,
625
+ value
626
+ })), expired]);
627
+ return deadline.remaining() > 0 ? result : { completed: false };
628
+ } finally {
629
+ clearTimeout(timer);
630
+ controller.abort();
631
+ }
632
+ }
633
+ async function waitForReadiness(deadline, probe, options = {}) {
634
+ while (deadline.remaining() > 0 && !options.stopped?.()) {
635
+ const result = await withinStartupDeadline(deadline, probe, options.probeTimeoutMs ?? 1e3).catch(() => ({ completed: false }));
636
+ if (options.stopped?.()) return false;
637
+ if (result.completed && result.value) return true;
638
+ const remaining = deadline.remaining();
639
+ if (remaining <= 0) break;
640
+ const pause = Math.min(options.intervalMs ?? 250, remaining);
641
+ if (pause > 0) await new Promise((resolve) => setTimeout(resolve, pause));
642
+ }
643
+ return false;
644
+ }
645
+ async function probeEngine(base, signal) {
646
+ if (signal.aborted) return false;
647
+ (await fetch(`${base}/`, { signal })).body?.cancel().catch(() => {});
648
+ return true;
649
+ }
650
+ async function probeWorker(base, signal, onViewerPort) {
651
+ if (signal.aborted) return false;
652
+ const response = await fetch(`${base}/agentmemory/livez`, { signal });
653
+ if (!response.ok) {
654
+ response.body?.cancel().catch(() => {});
655
+ return false;
656
+ }
657
+ const data = await response.json();
658
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
659
+ const { viewerPort, viewerSkipped } = data;
660
+ if (typeof viewerPort === "number" && Number.isInteger(viewerPort) && viewerPort > 0 && viewerPort <= 65535) {
661
+ onViewerPort?.(viewerPort);
662
+ return true;
663
+ }
664
+ return viewerSkipped === true;
665
+ }
666
+ async function startWorkerAndWait(options) {
667
+ const deadline = startupDeadline(options.timeoutMs);
668
+ if (options.stopped?.()) return false;
669
+ if (!options.hasLiveWorker() && !options.engineStartsWorker()) {
670
+ if (!(await withinStartupDeadline(deadline, () => options.startWorker())).completed) return false;
671
+ }
672
+ return waitForReadiness(deadline, options.probe, { stopped: options.stopped });
581
673
  }
582
674
  //#endregion
583
675
  //#region src/cli/process-state.ts
@@ -585,6 +677,64 @@ function processStatIsRunning(stat) {
585
677
  return !stat.trimStart().startsWith("Z");
586
678
  }
587
679
  //#endregion
680
+ //#region src/cli/compose-process.ts
681
+ /**
682
+ * On macOS, ask lsof for the process's text executable and accept only a
683
+ * response explicitly associated with this PID. The `-d txt` filter limits
684
+ * names to text descriptors (the process executable).
685
+ */
686
+ function readMacProcessExecutable(pid) {
687
+ if (!Number.isSafeInteger(pid) || pid <= 0) return "";
688
+ try {
689
+ const output = execFileSync("lsof", [
690
+ "-a",
691
+ "-p",
692
+ String(pid),
693
+ "-d",
694
+ "txt",
695
+ "-Fpn"
696
+ ], {
697
+ encoding: "utf8",
698
+ stdio: [
699
+ "ignore",
700
+ "pipe",
701
+ "ignore"
702
+ ]
703
+ });
704
+ let matchedPid = false;
705
+ for (const line of output.split(/\r?\n/)) {
706
+ if (line.startsWith("p")) {
707
+ if (line !== `p${pid}`) return "";
708
+ matchedPid = true;
709
+ continue;
710
+ }
711
+ if (matchedPid && line.startsWith("n")) {
712
+ const path = line.slice(1);
713
+ if (isAbsolute(path)) return path;
714
+ }
715
+ }
716
+ } catch {}
717
+ return "";
718
+ }
719
+ function sameResolvedExecutable(actual, expected, windows = false) {
720
+ try {
721
+ const actualPath = realpathSync(actual);
722
+ const expectedPath = realpathSync(expected);
723
+ return windows ? actualPath.toLowerCase() === expectedPath.toLowerCase() : actualPath === expectedPath;
724
+ } catch {
725
+ return false;
726
+ }
727
+ }
728
+ function readUnixProcessExecutable(pid, os) {
729
+ if (!Number.isSafeInteger(pid) || pid <= 0) return "";
730
+ if (os === "darwin") return readMacProcessExecutable(pid);
731
+ try {
732
+ return readlinkSync(`/proc/${pid}/exe`);
733
+ } catch {
734
+ return "";
735
+ }
736
+ }
737
+ //#endregion
588
738
  //#region src/cli/splash.ts
589
739
  const IS_COLOR_TTY = !!process.stdout.isTTY && !process.env["NO_COLOR"];
590
740
  function accent(s) {
@@ -1052,6 +1202,732 @@ async function runCaptureCommand(options) {
1052
1202
  return 0;
1053
1203
  }
1054
1204
  //#endregion
1205
+ //#region src/cli/compression.ts
1206
+ async function runCompressionCommand(options) {
1207
+ const log = options.log ?? console.log;
1208
+ const json = options.args.includes("--json");
1209
+ try {
1210
+ const input = {};
1211
+ let retry = false;
1212
+ let dryRun = false;
1213
+ const valueFlags = {
1214
+ "--session": "sessionId",
1215
+ "--project": "project",
1216
+ "--agent-id": "agentId",
1217
+ "--limit": "limit",
1218
+ "--cursor": "cursor"
1219
+ };
1220
+ for (let i = 0; i < options.args.length; i++) {
1221
+ const flag = options.args[i];
1222
+ if (flag === "--json") continue;
1223
+ if (flag === "--retry") {
1224
+ retry = true;
1225
+ continue;
1226
+ }
1227
+ if (flag === "--dry-run") {
1228
+ dryRun = true;
1229
+ continue;
1230
+ }
1231
+ if (flag.startsWith("--port=") || flag.startsWith("--instance=")) {
1232
+ if (!flag.slice(flag.indexOf("=") + 1)) throw new Error(`Missing value for ${flag.split("=")[0]}`);
1233
+ continue;
1234
+ }
1235
+ if (flag === "--port" || flag === "--instance") {
1236
+ if (!options.args[++i] || options.args[i].startsWith("--")) throw new Error(`Missing value for ${flag}`);
1237
+ continue;
1238
+ }
1239
+ const name = valueFlags[flag];
1240
+ if (!name) throw new Error(`Unknown compression option: ${flag}`);
1241
+ if (input[name] !== void 0) throw new Error(`Duplicate option: ${flag}`);
1242
+ const value = options.args[++i];
1243
+ if (value === void 0 || value.startsWith("--")) throw new Error(`Missing value for ${flag}`);
1244
+ input[name] = value;
1245
+ }
1246
+ if (retry && dryRun) throw new Error("Use either --retry or --dry-run");
1247
+ const scope = parseCompressionScope(input);
1248
+ const headers = { "Content-Type": "application/json" };
1249
+ if (options.secret) headers.Authorization = `Bearer ${options.secret}`;
1250
+ const query = new URLSearchParams(Object.entries(scope).filter(([, value]) => value !== void 0).map(([key, value]) => [key, String(value)]));
1251
+ const response = await fetch(`${options.base}/agentmemory/compression${retry ? "/retry" : `?${query}`}`, {
1252
+ method: retry ? "POST" : "GET",
1253
+ headers,
1254
+ ...retry ? { body: JSON.stringify(scope) } : {},
1255
+ signal: AbortSignal.timeout(retry ? 12e4 : 1e4)
1256
+ });
1257
+ if (!response.ok) throw new Error(`Compression endpoint returned HTTP ${response.status}`);
1258
+ const result = await response.json();
1259
+ if (typeof result.success !== "boolean" || !Array.isArray(result.items)) throw new Error("Invalid compression response");
1260
+ if (json) log(JSON.stringify(result, null, 2));
1261
+ else {
1262
+ 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)`);
1263
+ if (result.error === "cursor_stale") log("Compression cursor changed. Restart without --cursor.");
1264
+ 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]" : ""}`);
1265
+ if (result.nextCursor) log(`Continue with --cursor ${result.nextCursor}`);
1266
+ 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.");
1267
+ }
1268
+ return result.success ? 0 : 1;
1269
+ } catch (error) {
1270
+ const message = error instanceof Error ? error.message : "Compression request failed";
1271
+ log(json ? JSON.stringify({
1272
+ success: false,
1273
+ error: message
1274
+ }) : `Compression: ${message}`);
1275
+ return 1;
1276
+ }
1277
+ }
1278
+ //#endregion
1279
+ //#region src/cli/graph-index.ts
1280
+ const VALUE_FLAGS = new Set([
1281
+ "--port",
1282
+ "--instance",
1283
+ "--data-dir",
1284
+ "--tools"
1285
+ ]);
1286
+ async function runGraphIndexCommand(options) {
1287
+ const log = options.log ?? console.log;
1288
+ const json = options.args.includes("--json");
1289
+ const output = (value) => {
1290
+ if (json) {
1291
+ log(JSON.stringify(value, null, 2));
1292
+ return;
1293
+ }
1294
+ if (value.error) {
1295
+ log(String(value.error));
1296
+ return;
1297
+ }
1298
+ log(`Graph index: ${value.state ?? "unknown"}`);
1299
+ if (value.jobId) log(`Job: ${value.jobId}, processed ${value.processed ?? 0} of ${value.total ?? "unknown"}`);
1300
+ if (value.action) log(String(value.action));
1301
+ };
1302
+ let command;
1303
+ let allowLegacyScan = false;
1304
+ try {
1305
+ for (let i = 0; i < options.args.length; i++) {
1306
+ const arg = options.args[i];
1307
+ if (arg === "--json") continue;
1308
+ if (arg === "--allow-legacy-scan") {
1309
+ allowLegacyScan = true;
1310
+ continue;
1311
+ }
1312
+ if (VALUE_FLAGS.has(arg)) {
1313
+ if (!options.args[++i] || options.args[i].startsWith("--")) throw new Error(`Missing value for ${arg}`);
1314
+ continue;
1315
+ }
1316
+ if ([...VALUE_FLAGS].some((flag) => arg.startsWith(`${flag}=`) && arg.length > flag.length + 1)) continue;
1317
+ if (arg === "--verbose" || arg === "-v") continue;
1318
+ if (!command && [
1319
+ "status",
1320
+ "migrate",
1321
+ "resume"
1322
+ ].includes(arg)) {
1323
+ command = arg;
1324
+ continue;
1325
+ }
1326
+ throw new Error(`Unknown graph-index argument: ${arg}`);
1327
+ }
1328
+ command ??= "status";
1329
+ if (command === "status" && allowLegacyScan) throw new Error("status does not accept --allow-legacy-scan");
1330
+ 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.");
1331
+ } catch (error) {
1332
+ output({ error: error instanceof Error ? error.message : String(error) });
1333
+ return 2;
1334
+ }
1335
+ try {
1336
+ const headers = { Accept: "application/json" };
1337
+ if (options.secret) headers.Authorization = `Bearer ${options.secret}`;
1338
+ if (command !== "status") headers["Content-Type"] = "application/json";
1339
+ const response = await fetch(`${options.base}/agentmemory/graph/index${command === "status" ? "" : `/${command}`}`, {
1340
+ method: command === "status" ? "GET" : "POST",
1341
+ headers,
1342
+ ...command === "status" ? {} : { body: JSON.stringify({ allowLegacyScan }) },
1343
+ signal: AbortSignal.timeout(1e4)
1344
+ });
1345
+ const value = await response.json();
1346
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Server returned an invalid graph-index response");
1347
+ output({
1348
+ ...value,
1349
+ ...!response.ok ? { httpStatus: response.status } : {}
1350
+ });
1351
+ return response.ok ? 0 : 1;
1352
+ } catch (error) {
1353
+ output({ error: error instanceof Error ? error.message : String(error) });
1354
+ return 1;
1355
+ }
1356
+ }
1357
+ //#endregion
1358
+ //#region src/cli/compose-project.ts
1359
+ /** Registry versions are pinned here so every generated project uses one reviewed set. */
1360
+ const COMPOSE_WORKER_VERSIONS = {
1361
+ state: "0.22.20",
1362
+ http: "0.22.7"
1363
+ };
1364
+ function composeNamespace(restPort) {
1365
+ return `agentmemory-${restPort}`;
1366
+ }
1367
+ function composeProjectPaths(dataDir) {
1368
+ const directory = join(dataDir, "compose");
1369
+ return {
1370
+ directory,
1371
+ file: join(directory, "worker-compose.yaml"),
1372
+ stateDir: join(dataDir, "compose-state"),
1373
+ logFile: join(directory, "compose-daemon.log"),
1374
+ configDir: join(directory, "config")
1375
+ };
1376
+ }
1377
+ const AGENTMEMORY_WORKER_ENV_ALLOWLIST = new Set([
1378
+ "EMBEDDING_PROVIDER",
1379
+ "OPENAI_API_KEY",
1380
+ "OPENAI_API_KEY_FOR_LLM",
1381
+ "OPENAI_BASE_URL",
1382
+ "OPENAI_MODEL",
1383
+ "OPENAI_EMBEDDING_API_KEY",
1384
+ "OPENAI_EMBEDDING_BASE_URL",
1385
+ "OPENAI_EMBEDDING_MODEL",
1386
+ "OPENAI_EMBEDDING_DIMENSIONS",
1387
+ "ANTHROPIC_API_KEY",
1388
+ "ANTHROPIC_BASE_URL",
1389
+ "ANTHROPIC_MODEL",
1390
+ "GEMINI_API_KEY",
1391
+ "GEMINI_MODEL",
1392
+ "GOOGLE_API_KEY",
1393
+ "VOYAGE_API_KEY",
1394
+ "VOYAGE_MODEL",
1395
+ "COHERE_API_KEY",
1396
+ "COHERE_MODEL",
1397
+ "OPENROUTER_API_KEY",
1398
+ "OPENROUTER_MODEL",
1399
+ "MINIMAX_API_KEY",
1400
+ "MINIMAX_MODEL",
1401
+ "OLLAMA_HOST",
1402
+ "LLM_PROVIDER",
1403
+ "BM25_WEIGHT",
1404
+ "VECTOR_WEIGHT",
1405
+ "MAX_TOKENS",
1406
+ "TOKEN_BUDGET",
1407
+ "MAX_OBS_PER_SESSION",
1408
+ "CONSOLIDATION_ENABLED",
1409
+ "GRAPH_EXTRACTION_ENABLED",
1410
+ "AGENTMEMORY_AUTO_COMPRESS",
1411
+ "AGENTMEMORY_INJECT_CONTEXT",
1412
+ "AGENTMEMORY_ALLOW_AGENT_SDK",
1413
+ "AGENTMEMORY_SESSION_SWEEP_ENABLED",
1414
+ "AGENTMEMORY_SESSION_SWEEP_STALE_HOURS",
1415
+ "AGENTMEMORY_FINALIZE_IDLE_MS",
1416
+ "AGENTMEMORY_GRAPH_COMPACT_ON_BOOT",
1417
+ "AGENTMEMORY_DROP_STALE_INDEX",
1418
+ "AGENTMEMORY_AUDIT_RETENTION_MONTHS",
1419
+ "AGENTMEMORY_AUDIT_MIGRATE_MAX_BYTES",
1420
+ "AGENTMEMORY_STATE_SAVE_INTERVAL_MS",
1421
+ "AGENTMEMORY_TOOLS",
1422
+ "AGENTMEMORY_VIEWER_HOST",
1423
+ "VIEWER_ALLOWED_HOSTS",
1424
+ "VIEWER_ALLOWED_ORIGINS",
1425
+ "AGENTMEMORY_SECRET",
1426
+ "AGENT_ID",
1427
+ "TEAM_ID",
1428
+ "TEAM_MODE",
1429
+ "USER_ID",
1430
+ "AGENTMEMORY_AGENT_SCOPE",
1431
+ "SNAPSHOT_DIR",
1432
+ "SNAPSHOT_ENABLED",
1433
+ "SNAPSHOT_INTERVAL",
1434
+ "STANDALONE_PERSIST_PATH",
1435
+ "CLAUDE_MEMORY_BRIDGE",
1436
+ "CLAUDE_MEMORY_LINE_BUDGET",
1437
+ "CLAUDE_PROJECT_PATH",
1438
+ "AGENTMEMORY_SUPPRESS_COST_WARNING",
1439
+ "FALLBACK_PROVIDERS",
1440
+ "OPENAI_API_VERSION",
1441
+ "OPENAI_REASONING_EFFORT",
1442
+ "OPENAI_TIMEOUT_MS",
1443
+ "MINIMAX_BASE_URL",
1444
+ "OPENROUTER_EMBEDDING_MODEL",
1445
+ "OPENROUTER_EMBEDDING_DIMENSIONS",
1446
+ "AGENTMEMORY_LLM_TIMEOUT_MS",
1447
+ "AGENTMEMORY_CAPTURE_MAX_ATTEMPTS",
1448
+ "AGENTMEMORY_CAPTURE_RETRY_INTERVAL_MS",
1449
+ "AGENTMEMORY_CAPTURE_DEDUP_HOURS",
1450
+ "AGENTMEMORY_CAPTURE_INBOX_MAX",
1451
+ "AGENTMEMORY_CAPTURE_DEAD_MAX",
1452
+ "AGENTMEMORY_CAPTURE_EVENTS_MAX",
1453
+ "AGENTMEMORY_CAPTURE_SPOOL",
1454
+ "AGENTMEMORY_CAPTURE_SPOOL_DIR",
1455
+ "AGENTMEMORY_CAPTURE_SPOOL_MAX_BYTES",
1456
+ "AGENTMEMORY_CAPTURE_SPOOL_MAX_AGE_HOURS",
1457
+ "AGENTMEMORY_IMPORT_ROOT",
1458
+ "AGENTMEMORY_EXPORT_ROOT",
1459
+ "AGENTMEMORY_GRAPH_WEIGHT",
1460
+ "SUMMARIZE_CHUNK_SIZE",
1461
+ "SUMMARIZE_CHUNK_CONCURRENCY",
1462
+ "CONSOLIDATION_DECAY_DAYS",
1463
+ "CONSOLIDATION_INTERVAL_MS",
1464
+ "AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS",
1465
+ "AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS",
1466
+ "GRAPH_EXTRACTION_BATCH_SIZE",
1467
+ "AGENTMEMORY_LLM_NOTHINK",
1468
+ "AGENTMEMORY_REFLECT",
1469
+ "AGENTMEMORY_IMAGE_EMBEDDINGS",
1470
+ "AGENTMEMORY_IMAGE_STORE_MAX_BYTES",
1471
+ "AGENTMEMORY_AUDIT_INDEX_PERSIST",
1472
+ "AGENTMEMORY_INDEX_SAVE_INTERVAL_MS",
1473
+ "AGENTMEMORY_VECTOR_BUCKET_SIZE",
1474
+ "AGENTMEMORY_VECTOR_BACKFILL_MAX",
1475
+ "AGENTMEMORY_VECTOR_BACKFILL",
1476
+ "AGENTMEMORY_SLOTS",
1477
+ "AGENTMEMORY_PROVIDER",
1478
+ "AGENTMEMORY_VERBOSE",
1479
+ "OBSIDIAN_AUTO_EXPORT",
1480
+ "AUTO_FORGET_ENABLED",
1481
+ "AUTO_FORGET_INTERVAL_MS",
1482
+ "LESSON_DECAY_ENABLED",
1483
+ "INSIGHT_DECAY_ENABLED",
1484
+ "REBUILD_EMBED_BATCH_SIZE",
1485
+ "RERANK_ENABLED"
1486
+ ]);
1487
+ const COMPOSE_RESERVED_ENV = new Set([
1488
+ "III_URL",
1489
+ "III_ENGINE_URL",
1490
+ "III_ENGINE_PORT",
1491
+ "III_REST_PORT",
1492
+ "III_VIEWER_PORT",
1493
+ "III_NAMESPACE",
1494
+ "III_COMPOSE_NAMESPACE",
1495
+ "III_COMPOSE_FILE",
1496
+ "III_COMPOSE_DIR",
1497
+ "III_CONFIG_NAME",
1498
+ "III_WORKER_NAME"
1499
+ ]);
1500
+ function stateSaveInterval(value) {
1501
+ if (value === void 0 || value.trim() === "") return getStateSaveIntervalMs();
1502
+ if (!/^\d+$/.test(value.trim())) throw new Error("AGENTMEMORY_STATE_SAVE_INTERVAL_MS must be an integer between 100 and 3600000.");
1503
+ const parsed = Number(value);
1504
+ if (!Number.isSafeInteger(parsed) || parsed < 100 || parsed > 36e5) throw new Error("AGENTMEMORY_STATE_SAVE_INTERVAL_MS must be an integer between 100 and 3600000.");
1505
+ return parsed;
1506
+ }
1507
+ function restartPolicyLines(indent, enabled) {
1508
+ if (!enabled) return [`${indent}restart: no`];
1509
+ return [
1510
+ `${indent}restart:`,
1511
+ `${indent} condition: on-failure`,
1512
+ `${indent} delay: 500ms`,
1513
+ `${indent} max_delay: 30s`,
1514
+ `${indent} max_attempts: 5`,
1515
+ `${indent} window: 60s`
1516
+ ];
1517
+ }
1518
+ function shellQuote$1(value, windows) {
1519
+ if (/[\u0000-\u001f\u007f]/.test(value)) throw new Error("A shell command argument contains an unsupported control character.");
1520
+ if (windows) {
1521
+ if (value.includes("\"")) throw new Error("Windows shell arguments cannot contain a double quote.");
1522
+ return `"${value.replace(/%/g, "%%")}"`;
1523
+ }
1524
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
1525
+ }
1526
+ function shellCommand(program, args, windows) {
1527
+ const command = [program, ...args].map((part) => shellQuote$1(part, windows)).join(" ");
1528
+ return windows ? `"${command}"` : command;
1529
+ }
1530
+ function yamlString(value) {
1531
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) throw new Error("A Compose path or configuration value contains an unsupported control character.");
1532
+ return `'${value.replace(/'/g, "''")}'`;
1533
+ }
1534
+ function localWorkerDirectory(value, cwd, projectRoot) {
1535
+ if (isAbsolute(value)) return resolve(value);
1536
+ const fromCwd = resolve(cwd, value);
1537
+ const fromProject = resolve(projectRoot, value);
1538
+ if (fromCwd === fromProject) return fromCwd;
1539
+ if (!fromCwd.startsWith(`${resolve(projectRoot)}/`)) return fromCwd;
1540
+ return fromProject;
1541
+ }
1542
+ function localWorkerProjectDir(paths, name) {
1543
+ return join(paths.directory, "local-workers", name);
1544
+ }
1545
+ function ensureEngineTraceStorageConfig(configDir, dataDir) {
1546
+ const configFile = join(configDir, "iii-observability.yaml");
1547
+ const traceDirectory = resolve(dataDir, "observability", "traces");
1548
+ const defaultTraceDirectory = "./data/observability/traces";
1549
+ let entry;
1550
+ if (existsSync(configFile)) {
1551
+ const parsed = parse(readFileSync(configFile, "utf8"));
1552
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Invalid persisted iii-observability configuration at ${configFile}.`);
1553
+ entry = parsed;
1554
+ if (entry.id !== "iii-observability") throw new Error(`Unexpected configuration id in ${configFile}; refusing to replace it.`);
1555
+ } else entry = {
1556
+ id: "iii-observability",
1557
+ name: "Observability",
1558
+ description: "",
1559
+ schema: {},
1560
+ value: {
1561
+ sampling_ratio: .1,
1562
+ logs_console_output: false
1563
+ }
1564
+ };
1565
+ const value = entry.value && typeof entry.value === "object" && !Array.isArray(entry.value) ? entry.value : {};
1566
+ let changed = false;
1567
+ if (value.sampling_ratio === void 0) {
1568
+ value.sampling_ratio = .1;
1569
+ changed = true;
1570
+ }
1571
+ if (value.logs_console_output === void 0) {
1572
+ value.logs_console_output = false;
1573
+ changed = true;
1574
+ }
1575
+ const existingStorage = value.trace_storage;
1576
+ const storage = existingStorage && typeof existingStorage === "object" && !Array.isArray(existingStorage) ? existingStorage : {};
1577
+ const existingDirectory = storage.directory;
1578
+ if (typeof existingDirectory === "string" && existingDirectory.trim() !== "" && existingDirectory !== defaultTraceDirectory) {
1579
+ if (!changed) return;
1580
+ } else {
1581
+ storage.directory = traceDirectory;
1582
+ changed = true;
1583
+ }
1584
+ if (!changed) return;
1585
+ if (existingDirectory === void 0 || existingDirectory === defaultTraceDirectory || existingDirectory === "") value.trace_storage = storage;
1586
+ entry.value = value;
1587
+ const temporaryFile = `${configFile}.tmp`;
1588
+ writeFileSync(temporaryFile, stringify(entry), {
1589
+ encoding: "utf8",
1590
+ mode: 384
1591
+ });
1592
+ chmodSync(temporaryFile, 384);
1593
+ renameSync(temporaryFile, configFile);
1594
+ chmodSync(configFile, 384);
1595
+ }
1596
+ function renderWorkerCompose(options) {
1597
+ const { restPort, viewerPort, enginePort } = options.ports;
1598
+ const paths = composeProjectPaths(options.dataDir);
1599
+ const namespace = composeNamespace(restPort);
1600
+ const workerBinDir = options.workerBinDir ? options.workerBinDirIsAbsolute ? resolve(options.workerBinDir) : localWorkerDirectory(options.workerBinDir, options.cwd ?? process.cwd(), options.projectRoot) : null;
1601
+ const startupSeconds = Math.ceil((options.startupTimeoutMs ?? 3e5) / 1e3);
1602
+ const restartEnabled = !options.disableRestartPolicy;
1603
+ if (!Number.isSafeInteger(startupSeconds) || startupSeconds < 1 || startupSeconds * 1e3 > 3600999) throw new Error("Compose worker startup timeout must be between 1 and 3600000 milliseconds.");
1604
+ const workerSource = (name) => workerBinDir ? `path://${localWorkerProjectDir(paths, name)}` : `package://api.workers.iii.dev/${name}`;
1605
+ const workerScript = (name) => workerBinDir ? join(workerBinDir, name) : "";
1606
+ const filteredEnvFile = options.envFile ? join(paths.directory, ".agentmemory-filtered.env") : void 0;
1607
+ const lines = [
1608
+ `namespace: ${namespace}`,
1609
+ "startup_timeout: 60s",
1610
+ "stop_timeout: 20s",
1611
+ "required_default: true",
1612
+ "engine:",
1613
+ ` url: ws://127.0.0.1:${enginePort}`,
1614
+ " workers:",
1615
+ " configuration:",
1616
+ " adapter:",
1617
+ " name: fs",
1618
+ " config:",
1619
+ ` directory: ${yamlString(paths.configDir)}`,
1620
+ " iii-worker-manager:",
1621
+ " host: 127.0.0.1",
1622
+ ` port: ${enginePort}`,
1623
+ "containers:"
1624
+ ];
1625
+ const stateScript = workerScript("state");
1626
+ lines.push(" state:", ` worker: ${yamlString(workerSource("state"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.state)}`] : [], " config_name: state", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(shellCommand(stateScript, [], options.windows ?? process.platform === "win32"))}`] : [], " config_override:", " adapter:", " name: kv", " config:");
1627
+ if (options.stateBackend?.kind === "redis") {
1628
+ const redisUrl = options.redisUrl ?? options.stateBackend.redisUrl;
1629
+ if (!redisUrl) throw new Error("AGENTMEMORY_STATE_BACKEND=redis requires AGENTMEMORY_REDIS_URL to be set (e.g. redis://localhost:6379).");
1630
+ if (/[\u0000-\u001f\u007f]/.test(redisUrl)) throw new Error("AGENTMEMORY_REDIS_URL contains a control character; percent-encode it before using Redis.");
1631
+ lines.splice(lines.length - 2, 2, " name: redis", " config:", ` redis_url: ${yamlString(redisUrl)}`);
1632
+ } 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)}`);
1633
+ const httpScript = workerScript("http");
1634
+ lines.push(" http:", ` worker: ${yamlString(workerSource("http"))}`, ...!workerBinDir ? [` version: ${yamlString(COMPOSE_WORKER_VERSIONS.http)}`] : [], " config_name: http", ...restartPolicyLines(" ", restartEnabled), ...workerBinDir ? [` scripts:`, ` run: ${yamlString(shellCommand(httpScript, [], options.windows ?? process.platform === "win32"))}`] : [], " 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]");
1635
+ lines.push(" agentmemory:", ` worker: ${yamlString(`path://${options.projectRoot}`)}`, " start_after: [state, http]", ` startup_timeout: ${startupSeconds}s`, " config_name: agentmemory", ...restartPolicyLines(" ", restartEnabled), " scripts:", ` run: ${yamlString(shellCommand(options.nodeBin, ["dist/index.mjs"], options.windows ?? process.platform === "win32"))}`);
1636
+ if (filteredEnvFile) lines.push(` env_file: [${yamlString(filteredEnvFile)}]`);
1637
+ 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));
1638
+ lines.push(" environment:", ...explicitWorkerEnvironment.map(([key, value]) => ` ${key}: ${yamlString(value)}`), ` HOME: ${yamlString(options.homeDir)}`, ` PATH: ${yamlString(`${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'"] : [], "");
1639
+ return lines.join("\n");
1640
+ }
1641
+ function writeWorkerCompose(options) {
1642
+ const paths = composeProjectPaths(options.dataDir);
1643
+ mkdirSync(paths.directory, {
1644
+ recursive: true,
1645
+ mode: 448
1646
+ });
1647
+ mkdirSync(paths.configDir, {
1648
+ recursive: true,
1649
+ mode: 448
1650
+ });
1651
+ mkdirSync(paths.stateDir, {
1652
+ recursive: true,
1653
+ mode: 448
1654
+ });
1655
+ ensureEngineTraceStorageConfig(paths.configDir, options.dataDir);
1656
+ mkdirSync(join(options.dataDir, "observability", "traces"), {
1657
+ recursive: true,
1658
+ mode: 448
1659
+ });
1660
+ if (options.workerBinDir) {
1661
+ const binDir = options.workerBinDirIsAbsolute ? resolve(options.workerBinDir) : localWorkerDirectory(options.workerBinDir, options.cwd ?? process.cwd(), options.projectRoot);
1662
+ for (const name of Object.keys(COMPOSE_WORKER_VERSIONS)) {
1663
+ const projectDir = localWorkerProjectDir(paths, name);
1664
+ mkdirSync(projectDir, {
1665
+ recursive: true,
1666
+ mode: 448
1667
+ });
1668
+ const manifest = [
1669
+ "iii: v1",
1670
+ `name: ${name}`,
1671
+ "language: rust",
1672
+ "scripts:",
1673
+ ` start: ${yamlString(join(binDir, name))}`,
1674
+ ""
1675
+ ].join("\n");
1676
+ writeFileSync(join(projectDir, "iii.worker.yaml"), manifest, {
1677
+ encoding: "utf8",
1678
+ mode: 384
1679
+ });
1680
+ chmodSync(join(projectDir, "iii.worker.yaml"), 384);
1681
+ }
1682
+ }
1683
+ const text = renderWorkerCompose(options);
1684
+ if (options.envFile) {
1685
+ const filteredPath = join(paths.directory, ".agentmemory-filtered.env");
1686
+ writeFileSync(filteredPath, `${readFileSync(options.envFile, "utf8").split(/\r?\n/).filter((line) => {
1687
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
1688
+ return !match || !COMPOSE_RESERVED_ENV.has(match[1]);
1689
+ }).join("\n")}\n`, {
1690
+ encoding: "utf8",
1691
+ mode: 384
1692
+ });
1693
+ chmodSync(filteredPath, 384);
1694
+ }
1695
+ writeFileSync(paths.file, text, {
1696
+ encoding: "utf8",
1697
+ mode: 384
1698
+ });
1699
+ chmodSync(paths.file, 384);
1700
+ return {
1701
+ file: paths.file,
1702
+ stateDir: paths.stateDir,
1703
+ logFile: paths.logFile,
1704
+ text
1705
+ };
1706
+ }
1707
+ //#endregion
1708
+ //#region src/cli/compose-control.ts
1709
+ function composeRegistryFailureMessage(logText, workerBinDir) {
1710
+ if (![
1711
+ /REGISTRY_UNREACHABLE|REGISTRY_ERROR|PACKAGE_DOWNLOAD_FAILED/i,
1712
+ /cannot reach the registry/i,
1713
+ /registry (?:returned|responded|response)[^\n]*HTTP 5\d\d|HTTP 5\d\d[^\n]*registry/i,
1714
+ /failed to resolve (?:a )?package|package resolution (?:failed|unavailable)|could not resolve .*registry/i,
1715
+ /could not download https:\/\/api\.workers\.iii\.dev\/download\//i
1716
+ ].some((pattern) => pattern.test(logText))) return null;
1717
+ return `Worker registry unavailable while resolving state/http; retry later or set AGENTMEMORY_WORKER_BIN_DIR.${workerBinDir ? ` Configured local workers directory: ${workerBinDir}.` : ""}`;
1718
+ }
1719
+ function composeStateRegistryFailure(dataDir, file) {
1720
+ const stateRoot = join(dataDir, "compose-state");
1721
+ try {
1722
+ const files = [];
1723
+ const walk = (dir) => {
1724
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1725
+ const path = join(dir, entry.name);
1726
+ if (entry.isDirectory()) walk(path);
1727
+ else if (entry.name === "state.json") files.push(path);
1728
+ }
1729
+ };
1730
+ walk(stateRoot);
1731
+ for (const stateFile of files) {
1732
+ const state = JSON.parse(readFileSync(stateFile, "utf8"));
1733
+ const message = Object.entries(state.containers ?? {}).filter(([, value]) => value.status === "failed").map(([name, value]) => `${name}: ${value.last_error ?? "failed"}`).join("\n");
1734
+ if (state.compose_path !== file) continue;
1735
+ if (message && composeRegistryFailureMessage(message)) return message;
1736
+ }
1737
+ } catch {}
1738
+ return null;
1739
+ }
1740
+ function composeWorkerFailureSummary(project) {
1741
+ if (!project) return null;
1742
+ const failed = (project.containers ?? []).filter((container) => container.state === "failed");
1743
+ if (!failed.length) return null;
1744
+ return failed.map((container) => `${container.container ?? "worker"}: ${container.last_error ?? "failed after restart attempts were exhausted"}`).join("\n");
1745
+ }
1746
+ function ownedComposeProject(status, file) {
1747
+ return status.file === file ? status : status.projects?.find((item) => item.file === file) ?? null;
1748
+ }
1749
+ function cleanDaemonEnv(env) {
1750
+ const out = {};
1751
+ for (const key of [
1752
+ "HOME",
1753
+ "PATH",
1754
+ "LANG",
1755
+ "LC_ALL",
1756
+ "TERM",
1757
+ "NO_COLOR",
1758
+ "III_TELEMETRY_ENABLED",
1759
+ "MALLOC_ARENA_MAX",
1760
+ "III_BUNDLE_WORKERS_DISABLED",
1761
+ "III_COMPOSE_MAX_PARALLEL_WORKERS",
1762
+ "AGENTMEMORY_TOOLS",
1763
+ "AGENTMEMORY_VIEWER_HOST",
1764
+ "VIEWER_ALLOWED_HOSTS",
1765
+ "VIEWER_ALLOWED_ORIGINS"
1766
+ ]) {
1767
+ const value = env[key];
1768
+ if (value !== void 0) out[key] = value;
1769
+ }
1770
+ return out;
1771
+ }
1772
+ async function assertComposePortsFree(ports) {
1773
+ for (const port of [...new Set(ports)]) if (await new Promise((resolve) => {
1774
+ const socket = createConnection({
1775
+ host: "127.0.0.1",
1776
+ port
1777
+ });
1778
+ const done = (value) => {
1779
+ socket.destroy();
1780
+ resolve(value);
1781
+ };
1782
+ socket.setTimeout(350);
1783
+ socket.once("connect", () => done(true));
1784
+ socket.once("timeout", () => done(false));
1785
+ socket.once("error", () => done(false));
1786
+ })) 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.`);
1787
+ }
1788
+ function composeEnvironment(env, stateDir) {
1789
+ return {
1790
+ ...cleanDaemonEnv(env),
1791
+ III_COMPOSE_STATE_DIR: stateDir,
1792
+ III_TELEMETRY_ENABLED: "false",
1793
+ ...process.platform === "linux" ? { MALLOC_ARENA_MAX: "2" } : {},
1794
+ ...env.AGENTMEMORY_DISABLE_COMPOSE_RESTART === "1" ? { AGENTMEMORY_DISABLE_COMPOSE_RESTART: "1" } : {}
1795
+ };
1796
+ }
1797
+ async function composeCall(options) {
1798
+ const client = registerWorker(options.engineUrl, {
1799
+ workerName: "agentmemory-cli",
1800
+ namespace: options.namespace,
1801
+ invocationTimeoutMs: options.timeoutMs ?? 3e4,
1802
+ otel: { enabled: false },
1803
+ enableMetricsReporting: false
1804
+ });
1805
+ try {
1806
+ const connectionDeadline = Date.now() + 8e3;
1807
+ while (client.getConnectionState() !== "connected" && Date.now() < connectionDeadline) {
1808
+ const fatal = client.getFatalError();
1809
+ if (fatal) throw fatal;
1810
+ await new Promise((resolve) => setTimeout(resolve, 50));
1811
+ }
1812
+ if (client.getConnectionState() !== "connected") throw new Error("Could not connect the Compose control client to its instance engine.");
1813
+ const deadline = Date.now() + 8e3;
1814
+ let lastError;
1815
+ while (Date.now() < deadline) try {
1816
+ const result = await client.trigger({
1817
+ function_id: options.functionId,
1818
+ payload: { file: options.file },
1819
+ timeoutMs: options.timeoutMs ?? 3e4
1820
+ });
1821
+ if (result && typeof result === "object" && result.status === "failed") {
1822
+ const detail = result.error?.message ?? result.error?.code ?? "unknown Compose failure";
1823
+ throw Object.assign(/* @__PURE__ */ new Error(`${result.error?.code ? `${result.error.code}: ` : ""}${detail}`), { retryable: false });
1824
+ }
1825
+ return result;
1826
+ } catch (err) {
1827
+ lastError = err;
1828
+ if (err && typeof err === "object" && "retryable" in err && err.retryable === false) throw err;
1829
+ await new Promise((resolve) => setTimeout(resolve, 150));
1830
+ }
1831
+ throw lastError ?? /* @__PURE__ */ new Error("Compose daemon did not accept the control connection.");
1832
+ } finally {
1833
+ await client.shutdown().catch(() => {});
1834
+ }
1835
+ }
1836
+ function startComposeDaemon(runtime) {
1837
+ const project = writeWorkerCompose({
1838
+ ...runtime,
1839
+ envFile: runtime.envFile
1840
+ });
1841
+ const logFile = runtime.logFile;
1842
+ mkdirSync(dirname(logFile), {
1843
+ recursive: true,
1844
+ mode: 448
1845
+ });
1846
+ chmodSync(dirname(logFile), 448);
1847
+ const fd = openSync(logFile, "a", 384);
1848
+ const logStartOffset = fstatSync(fd).size;
1849
+ writeFileSync(fd, `\n[${(/* @__PURE__ */ new Date()).toISOString()}] Starting Worker Compose\n`);
1850
+ const environment = composeEnvironment(runtime.env, project.stateDir);
1851
+ const child = spawn(runtime.iiiBin, [
1852
+ "compose",
1853
+ "--namespace",
1854
+ composeNamespace(runtime.ports.restPort),
1855
+ "--up",
1856
+ "--file",
1857
+ project.file
1858
+ ], {
1859
+ cwd: runtime.projectRoot,
1860
+ detached: true,
1861
+ stdio: [
1862
+ "ignore",
1863
+ fd,
1864
+ fd
1865
+ ],
1866
+ windowsHide: true,
1867
+ env: environment
1868
+ });
1869
+ closeSync(fd);
1870
+ child.unref();
1871
+ return {
1872
+ child,
1873
+ file: project.file,
1874
+ namespace: composeNamespace(runtime.ports.restPort),
1875
+ stateDir: project.stateDir,
1876
+ logFile,
1877
+ logStartOffset
1878
+ };
1879
+ }
1880
+ function readComposeLogFromOffset(file, offset) {
1881
+ const fd = openSync(file, "r");
1882
+ try {
1883
+ const size = fstatSync(fd).size;
1884
+ let position = Math.min(Math.max(0, offset), size);
1885
+ const chunks = [];
1886
+ while (position < size) {
1887
+ const chunk = Buffer.allocUnsafe(Math.min(16 * 1024, size - position));
1888
+ const count = readSync(fd, chunk, 0, chunk.length, position);
1889
+ if (count === 0) break;
1890
+ chunks.push(chunk.subarray(0, count));
1891
+ position += count;
1892
+ }
1893
+ return Buffer.concat(chunks).toString("utf8");
1894
+ } finally {
1895
+ closeSync(fd);
1896
+ }
1897
+ }
1898
+ function writeComposeState(path, state) {
1899
+ mkdirSync(dirname(path), {
1900
+ recursive: true,
1901
+ mode: 448
1902
+ });
1903
+ writeFileSync(path, `${JSON.stringify(state)}\n`, {
1904
+ encoding: "utf8",
1905
+ mode: 384
1906
+ });
1907
+ chmodSync(path, 384);
1908
+ }
1909
+ //#endregion
1910
+ //#region src/cli/legacy-docker-state.ts
1911
+ function shellQuote(value) {
1912
+ return `'${value.replaceAll("'", "'\"'\"'")}'`;
1913
+ }
1914
+ function legacyDockerStopCommand(state) {
1915
+ const project = state.projectName?.trim();
1916
+ const composeFile = state.composeFile?.trim();
1917
+ if (composeFile) return `docker compose${project ? ` -p ${shellQuote(project)}` : ""} -f ${shellQuote(composeFile)} stop`;
1918
+ if (project) return `docker compose -p ${shellQuote(project)} stop`;
1919
+ if (state.containerId?.trim()) return `docker stop ${shellQuote(state.containerId.trim())}`;
1920
+ return "Cannot construct a stop command: legacy lifecycle state has no Compose file, project, or container ID.";
1921
+ }
1922
+ function legacyDockerStateMessage(state) {
1923
+ return [
1924
+ `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.`,
1925
+ "Stop the old container explicitly before switching to Worker Compose:",
1926
+ ` ${legacyDockerStopCommand(state)}`,
1927
+ "The legacy lifecycle state is preserved; no container or data was changed."
1928
+ ].join("\n");
1929
+ }
1930
+ //#endregion
1055
1931
  //#region src/cli.ts
1056
1932
  const c = {
1057
1933
  url: pc.cyan,
@@ -1071,7 +1947,7 @@ const IS_WINDOWS = platform() === "win32";
1071
1947
  const IS_VERBOSE = args.includes("--verbose") || args.includes("-v") || process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
1072
1948
  setBootVerbose(IS_VERBOSE);
1073
1949
  const IS_RESET = args.includes("--reset");
1074
- hydrateProcessEnvFromFile();
1950
+ const shellEnv = hydrateProcessEnvFromFile();
1075
1951
  if (args.includes("--version") || args.includes("-V")) {
1076
1952
  process.stdout.write(`${VERSION}\n`);
1077
1953
  process.exit(0);
@@ -1119,15 +1995,14 @@ agentmemory — persistent memory for AI coding agents
1119
1995
  Usage: agentmemory [command] [options]
1120
1996
 
1121
1997
  Commands:
1122
- (default) Start agentmemory worker
1998
+ (default) Start agentmemory via a per-instance Worker Compose project
1123
1999
  init Copy bundled .env.example to ~/.agentmemory/.env if absent
1124
2000
  connect [agent] Wire agentmemory into an installed agent
1125
2001
  (${wrapList(knownAgents(), 21)}).
1126
2002
  No arg = interactive picker. --all wires every detected agent.
1127
2003
  --dry-run shows what would change. --force re-installs.
1128
2004
  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
2005
+ console Show migration guidance for the retired iii console
1131
2006
  doctor Interactive diagnostic + fixer. [F]ix · [S]kip · [?]more · [Q]uit
1132
2007
  --all: apply every fix without prompting (CI)
1133
2008
  --dry-run: show what each fix would do, don't execute
@@ -1138,14 +2013,20 @@ Commands:
1138
2013
  in one command (no second terminal).
1139
2014
  upgrade Upgrade local deps + iii runtime (best effort)
1140
2015
  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).
2016
+ --force bypasses the native engine PID identity guard
2017
+ (use when the engine was started natively but state file is missing).
1144
2018
  mcp Start standalone MCP shim — opt-in surface for MCP-only clients
1145
2019
  (Cursor, Gemini CLI, etc). REST always available at :3111.
1146
2020
  capture [--drain] Show capture health: the local offline spool and the server
1147
2021
  inbox (pending, retrying, dead letters). --drain sends
1148
2022
  spooled observations now. --json for machine output.
2023
+ compression Inspect pending or failed observation compression.
2024
+ --session <id> or --project <name> selects the scope.
2025
+ --agent-id <id> narrows it; --limit <N> bounds the batch.
2026
+ --retry explicitly retries compression; --json for machine output.
2027
+ graph-index Inspect bounded graph index status. --json for machine output.
2028
+ migrate --allow-legacy-scan explicitly scans legacy graph data.
2029
+ resume continues a sealed migration without another full scan.
1149
2030
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
1150
2031
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
1151
2032
  out-of-range is rejected; for trees >1000 files, batch by subdirectory)
@@ -1156,30 +2037,32 @@ Options:
1156
2037
  --reset Wipe ~/.agentmemory/preferences.json and re-run onboarding
1157
2038
  --tools all|core Tool visibility (default: all = ${ALL_TOOLS_COUNT} tools; core = ${CORE_TOOLS_COUNT} essentials)
1158
2039
  --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.
2040
+ --port <N> Override REST port (default: 3111). Viewer (N+2) and
2041
+ iii engine (N+46023) derive from N for side-by-side instances.
1162
2042
  --instance <N> Shortcut for --port (3111 + N*100) to run multiple
1163
2043
  daemons side-by-side without env gymnastics.
1164
- --instance 1 -> 3211/3212/3213/49234, etc. (max N=50)
2044
+ --instance 1 -> 3211/3213/49234, etc. (max N=50)
1165
2045
  --data-dir <path> Store iii-engine state outside the current repo
1166
2046
 
1167
2047
  Environment:
1168
2048
  AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
1169
2049
  Honored by status, doctor, and MCP shim commands.
1170
2050
  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
2051
  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).
2052
+ AGENTMEMORY_ENGINE_READY_TIMEOUT_MS
2053
+ Engine readiness deadline in milliseconds (default 120000).
2054
+ AGENTMEMORY_READY_TIMEOUT_MS
2055
+ Worker readiness deadline in milliseconds (default 300000).
2056
+ Both deadlines accept integers from 1 through 3600000.
2057
+ AGENTMEMORY_ENGINE_MANAGES_WORKER
2058
+ true for custom engine-managed worker wrappers; false
2059
+ for CLI-managed workers. Unset detects direct worker commands.
1177
2060
  AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS
1178
2061
  Window (seconds) for the smart-search follow-up diagnostic
1179
2062
  (default 30). Long values overcount, short values undercount.
1180
2063
 
1181
2064
  Quick start:
1182
- npx @agentmemory/agentmemory # start with local iii-engine or Docker
2065
+ npx @agentmemory/agentmemory # start pinned iii Worker Compose runtime
1183
2066
  npx @agentmemory/agentmemory demo # see semantic recall in 30 seconds
1184
2067
  npx @agentmemory/agentmemory doctor # diagnose config + feature flags
1185
2068
  npx @agentmemory/agentmemory status # health + memory count + flags
@@ -1199,7 +2082,9 @@ const URL_CLIENT_COMMANDS = new Set([
1199
2082
  "status",
1200
2083
  "doctor",
1201
2084
  "mcp",
1202
- "capture"
2085
+ "capture",
2086
+ "compression",
2087
+ "graph-index"
1203
2088
  ]);
1204
2089
  let hasExplicitLocalPortOverride = false;
1205
2090
  let selectedInstance = 0;
@@ -1232,7 +2117,6 @@ if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance="))) {
1232
2117
  }
1233
2118
  const restPort = parseInt(process.env["III_REST_PORT"] || "3111", 10);
1234
2119
  if (Number.isFinite(restPort) && restPort > 0) {
1235
- process.env["III_STREAM_PORT"] ??= process.env["III_STREAMS_PORT"] ?? String(restPort + 1);
1236
2120
  process.env["III_VIEWER_PORT"] ??= String(restPort + 2);
1237
2121
  const configuredEngineUrl = process.env["III_ENGINE_URL"];
1238
2122
  if (configuredEngineUrl) try {
@@ -1293,9 +2177,6 @@ function getViewerUrl() {
1293
2177
  return `http://localhost:${getConfiguredViewerPort()}`;
1294
2178
  }
1295
2179
  }
1296
- function getStreamPort() {
1297
- return parseInt(process.env["III_STREAM_PORT"] || "", 10) || parseInt(process.env["III_STREAMS_PORT"] || "", 10) || getRestPort() + 1;
1298
- }
1299
2180
  function getEnginePort() {
1300
2181
  const url = process.env["III_ENGINE_URL"];
1301
2182
  if (url) try {
@@ -1307,43 +2188,19 @@ function getEnginePort() {
1307
2188
  return getRestPort() + 46023;
1308
2189
  }
1309
2190
  async function isEngineRunning() {
1310
- try {
1311
- await fetch(`${getBaseUrl()}/`, { signal: AbortSignal.timeout(2e3) });
1312
- return true;
1313
- } catch {
1314
- return false;
1315
- }
2191
+ const state = readEngineState();
2192
+ const endpoint = state?.kind === "compose" && state.restPort === getRestPort() && !shouldUseAgentmemoryUrl() ? `http://127.0.0.1:${state.restPort}` : getBaseUrl();
2193
+ const result = await withinStartupDeadline(startupDeadline(2e3), (signal) => probeEngine(endpoint, signal)).catch(() => null);
2194
+ return result?.completed === true && result.value;
1316
2195
  }
1317
2196
  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
- }
2197
+ const result = await withinStartupDeadline(startupDeadline(2e3), workerReadinessProbe).catch(() => null);
2198
+ return result?.completed === true && result.value;
1335
2199
  }
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 "";
2200
+ function workerReadinessProbe(signal) {
2201
+ return probeWorker(getBaseUrl(), signal, (port) => {
2202
+ discoveredViewerPort = port;
2203
+ });
1347
2204
  }
1348
2205
  function warnIfRelocatedDataDir() {
1349
2206
  if (!dataDirResolution.relocatedFrom) return;
@@ -1431,15 +2288,6 @@ function enginePidfilePath() {
1431
2288
  function engineStatePath() {
1432
2289
  return runtimeMetadataPath("engine-state.json");
1433
2290
  }
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
2291
  function readEnginePidfile() {
1444
2292
  try {
1445
2293
  const pidStr = readFileSync(enginePidfilePath(), "utf-8").trim();
@@ -1471,28 +2319,11 @@ function clearWorkerPidfile() {
1471
2319
  unlinkSync(workerPidfilePath());
1472
2320
  } catch {}
1473
2321
  }
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
2322
  function readEngineState() {
1492
2323
  try {
1493
2324
  const raw = readFileSync(engineStatePath(), "utf-8");
1494
2325
  const parsed = JSON.parse(raw);
1495
- if (parsed && (parsed.kind === "native" || parsed.kind === "docker")) return parsed;
2326
+ if (parsed && (parsed.kind === "native" || parsed.kind === "docker" || parsed.kind === "compose")) return parsed;
1496
2327
  return null;
1497
2328
  } catch {
1498
2329
  return null;
@@ -1505,271 +2336,138 @@ function clearEngineState() {
1505
2336
  }
1506
2337
  function engineStateRestPort(state) {
1507
2338
  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
- }
2339
+ if (state.kind === "compose") return 3111;
1513
2340
  try {
1514
2341
  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
2342
  if (configuredPort) return parseInt(configuredPort, 10);
1516
2343
  } catch {}
1517
2344
  return 3111;
1518
2345
  }
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");
2346
+ async function startWorkerForEngineState(background = false) {
2347
+ if (readEngineState()?.kind === "compose") {
2348
+ const timeoutMs = startupTimeouts().workerMs;
2349
+ if (!await waitForReadiness(startupDeadline(timeoutMs), workerReadinessProbe, { stopped: () => {
2350
+ const state = readEngineState();
2351
+ if (state?.kind === "compose") try {
2352
+ const failure = composeRegistryFailureMessage(readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0), state.workerBinDir);
2353
+ if (failure) startupFailure = {
2354
+ kind: "engine-crashed",
2355
+ stderr: failure
2356
+ };
2357
+ } catch {}
2358
+ return startupInterrupted || engineStartupFailed() || Boolean(startupFailure?.stderr?.startsWith("Worker registry unavailable"));
2359
+ } })) {
2360
+ printComposeStartupLogs();
2361
+ const state = readEngineState();
2362
+ let registryFailure = state?.kind === "compose" ? composeStateRegistryFailure(dataDirResolution.dataDir, state.file) : null;
2363
+ if (state?.kind === "compose" && !registryFailure) registryFailure = (() => {
2364
+ try {
2365
+ return composeRegistryFailureMessage(readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0), state.workerBinDir);
2366
+ } catch {
2367
+ return null;
2368
+ }
2369
+ })();
2370
+ if (state?.kind === "compose" && await isEngineRunning()) try {
2371
+ const failures = (ownedComposeProject(await composeCall({
2372
+ engineUrl: state.engineUrl,
2373
+ namespace: state.namespace,
2374
+ functionId: "compose::status",
2375
+ file: state.file
2376
+ }), state.file)?.containers ?? []).filter((container) => container.state === "failed" || container.last_error).map((container) => {
2377
+ let detail = container.last_error ?? container.state ?? "failed";
2378
+ if (container.log_path) try {
2379
+ const log = readFileSync(container.log_path, "utf8").trim();
2380
+ if (log) detail += `\n${log.split(/\r?\n/).slice(-12).join("\n")}`;
2381
+ } catch {}
2382
+ return `${container.container ?? "worker"}: ${detail}`;
2383
+ });
2384
+ if (failures.length) {
2385
+ state.workerFailure = failures.join("\n");
2386
+ writeComposeState(engineStatePath(), {
2387
+ ...state,
2388
+ workerVersions: state.workerVersions ?? (state.workerBinDir ? { local: true } : COMPOSE_WORKER_VERSIONS)
2389
+ });
2390
+ const aggregate = composeRegistryFailureMessage(state.workerFailure, state.workerBinDir);
2391
+ if (aggregate) registryFailure = `${aggregate} Compose reported: ${state.workerFailure}`;
2392
+ else {
2393
+ const logTail = readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0).trim().split(/\r?\n/).slice(-12).join("\n");
2394
+ registryFailure = `Worker Compose failed: ${state.workerFailure}${logTail ? `\nRecent Compose output:\n${logTail}` : ""}`;
2395
+ }
2396
+ }
2397
+ } catch {}
2398
+ throw new Error(registryFailure || startupFailure?.stderr || startupTimeoutMessage("worker", timeoutMs));
2399
+ }
2400
+ composeStartupCompleted = true;
2401
+ finishStartup();
2402
+ return;
2403
+ }
2404
+ const timeoutMs = startupTimeouts().workerMs;
2405
+ startupWorkerFailure = null;
2406
+ if (!await startWorkerAndWait({
2407
+ timeoutMs,
2408
+ hasLiveWorker: () => {
2409
+ const workerPid = readWorkerPidfile();
2410
+ return Boolean(workerPid && pidAlive(workerPid));
2411
+ },
2412
+ engineStartsWorker: configuredEngineMayStartWorker,
2413
+ startWorker: async () => {
2414
+ if (background) {
2415
+ const builtEntry = join(__dirname, "index.mjs");
2416
+ const workerArgs = existsSync(builtEntry) ? [builtEntry] : [
2417
+ "--import",
2418
+ "tsx",
2419
+ join(__dirname, "index.ts")
2420
+ ];
2421
+ const child = spawn(process.execPath, workerArgs, {
2422
+ detached: true,
2423
+ stdio: "ignore",
2424
+ windowsHide: true,
2425
+ env: { ...process.env }
2426
+ });
2427
+ spawnedStartupWorker = child;
2428
+ child.on("error", (error) => {
2429
+ if (spawnedStartupWorker === child) startupWorkerFailure = error;
2430
+ });
2431
+ child.on("exit", (code, signal) => {
2432
+ if (spawnedStartupWorker === child) startupWorkerFailure = /* @__PURE__ */ new Error(`agentmemory worker exited during startup (${signal ?? code}).`);
2433
+ });
2434
+ child.unref();
2435
+ } else {
2436
+ importedStartupWorker = true;
2437
+ await import("./src-DaMtDzt_.mjs");
2438
+ }
2439
+ },
2440
+ probe: workerReadinessProbe,
2441
+ stopped: () => startupInterrupted || engineStartupFailed() || startupWorkerFailure !== null
2442
+ })) {
2443
+ if (startupWorkerFailure) throw startupWorkerFailure;
2444
+ if (engineStartupFailed()) throw new Error("iii-engine exited before the agentmemory worker became ready.");
2445
+ throw new Error(startupTimeoutMessage("worker", timeoutMs));
2446
+ }
2447
+ finishStartup();
1524
2448
  }
1525
2449
  function configuredEngineMayStartWorker() {
2450
+ const explicit = startupTimeouts().engineManagesWorker;
2451
+ if (explicit !== void 0) return explicit;
1526
2452
  const state = readEngineState();
1527
- if (state?.kind !== "native") return false;
2453
+ if (state?.kind !== "native" || state.attached) return false;
1528
2454
  try {
1529
- return readFileSync(state.configPath, "utf-8").includes("- name: iii-exec");
2455
+ return configStartsAgentmemoryWorker(readFileSync(state.configPath, "utf-8"), join(__dirname, "index.mjs"), state.cwd);
1530
2456
  } catch {
1531
2457
  return false;
1532
2458
  }
1533
2459
  }
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
2460
  async function assertRuntimePortOwnership() {
1754
2461
  const state = readEngineState();
1755
2462
  if (!state) return;
1756
- const ownerPort = engineStateRestPort(state);
1757
- const requestedPort = getRestPort();
1758
- if (ownerPort === requestedPort) return;
1759
2463
  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.`);
2464
+ p.log.error(legacyDockerStateMessage(state));
1771
2465
  process.exit(1);
1772
2466
  }
2467
+ const ownerPort = engineStateRestPort(state);
2468
+ if (state.kind === "compose" && ownerPort !== getRestPort()) return;
2469
+ const requestedPort = getRestPort();
2470
+ if (ownerPort === requestedPort) return;
1773
2471
  const enginePid = readEnginePidfile();
1774
2472
  const workerPid = readWorkerPidfile();
1775
2473
  let active = Boolean(enginePid && pidAlive(enginePid) && !isForeignPortHolder(pidCommand(enginePid)) || workerPid && pidAlive(workerPid));
@@ -1785,14 +2483,6 @@ async function assertRuntimePortOwnership() {
1785
2483
  clearWorkerPidfile();
1786
2484
  clearEngineState();
1787
2485
  }
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
2486
  function isInvokedViaNpx() {
1797
2487
  if (process.env["npm_lifecycle_event"] === "npx") return true;
1798
2488
  if ((process.argv[1] ?? "").includes("_npx")) return true;
@@ -1824,49 +2514,25 @@ async function maybeOfferGlobalInstall() {
1824
2514
  if (runCommand(npmBin, [
1825
2515
  "install",
1826
2516
  "-g",
1827
- `@agentmemory/agentmemory@0.9.30`
1828
- ], { label: `Installing @agentmemory/agentmemory@0.9.30 globally` })) {
2517
+ `@agentmemory/agentmemory@0.9.31`
2518
+ ], { label: `Installing @agentmemory/agentmemory@0.9.31 globally` })) {
1829
2519
  p.log.success("Installed globally. `agentmemory stop` etc. will now work in new shells.");
1830
2520
  writePrefs({ skipGlobalInstall: true });
1831
2521
  } else p.log.warn("Global install failed. Try manually: npm install -g @agentmemory/agentmemory");
1832
2522
  }
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
2523
  async function runIiiInstaller() {
1858
2524
  const releaseUrl = iiiReleaseUrl();
1859
2525
  const asset = iiiReleaseAsset();
1860
2526
  const isZipAsset = asset?.endsWith(".zip") === true;
1861
2527
  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}.`);
2528
+ 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
2529
  return {
1864
2530
  ok: false,
1865
2531
  binPath: null
1866
2532
  };
1867
2533
  }
1868
2534
  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}`);
2535
+ 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)\nInstall the pinned iii binary from ${releaseUrl}`);
1870
2536
  return {
1871
2537
  ok: false,
1872
2538
  binPath: null
@@ -1905,7 +2571,7 @@ async function runIiiInstaller() {
1905
2571
  const sha256 = expectedIiiSha256(IIPINNED_VERSION, asset ?? "");
1906
2572
  const heading = outcome.reason === "checksum" ? "iii-engine download failed verification" : outcome.reason === "no-checksum" ? "iii-engine auto-install skipped" : "iii-engine download failed";
1907
2573
  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}`);
2574
+ 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
2575
  return {
1910
2576
  ok: false,
1911
2577
  binPath: null
@@ -1919,159 +2585,305 @@ async function runIiiInstaller() {
1919
2585
  }
1920
2586
  let startupFailure = null;
1921
2587
  let activeStartupStderr = createStartupStderrCapture();
2588
+ let spawnedNativeEngine = null;
2589
+ let spawnedComposeDaemon = null;
2590
+ let composeStartupCompleted = false;
2591
+ let importedStartupWorker = false;
2592
+ let spawnedStartupWorker = null;
2593
+ let startupWorkerFailure = null;
2594
+ let engineReadinessDeadline = null;
2595
+ let startupInterrupted = false;
2596
+ let finishStartup = () => {};
2597
+ function beginStartup() {
2598
+ finishStartup();
2599
+ startupInterrupted = false;
2600
+ const unregister = registerStartupCleanup(cleanupFailedStartup);
2601
+ const interrupt = async (exitCode) => {
2602
+ startupInterrupted = true;
2603
+ process.exit(await runStartupCleanup(exitCode).catch(() => exitCode));
2604
+ };
2605
+ const onInterrupt = () => {
2606
+ interrupt(130);
2607
+ };
2608
+ const onTerminate = () => {
2609
+ interrupt(143);
2610
+ };
2611
+ process.on("SIGINT", onInterrupt);
2612
+ process.on("SIGTERM", onTerminate);
2613
+ finishStartup = () => {
2614
+ process.off("SIGINT", onInterrupt);
2615
+ process.off("SIGTERM", onTerminate);
2616
+ unregister();
2617
+ finishStartup = () => {};
2618
+ };
2619
+ }
2620
+ function engineStartupFailed() {
2621
+ return startupFailure?.kind === "engine-crashed";
2622
+ }
2623
+ async function cleanupFailedStartup() {
2624
+ engineReadinessDeadline = null;
2625
+ if (importedStartupWorker && readWorkerPidfile() === process.pid) clearWorkerPidfile();
2626
+ const worker = spawnedStartupWorker;
2627
+ spawnedStartupWorker = null;
2628
+ if (worker && typeof worker.pid === "number") {
2629
+ if (worker.exitCode === null && worker.signalCode === null && !await signalAndWait(worker.pid, "SIGTERM", 3e3)) {
2630
+ p.log.warn(`Started worker pid ${worker.pid} survived shutdown; lifecycle state was preserved.`);
2631
+ return;
2632
+ }
2633
+ if (readWorkerPidfile() === worker.pid) clearWorkerPidfile();
2634
+ }
2635
+ const composeChild = spawnedComposeDaemon;
2636
+ spawnedComposeDaemon = null;
2637
+ 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.`);
2638
+ else clearEngineState();
2639
+ const child = spawnedNativeEngine;
2640
+ spawnedNativeEngine = null;
2641
+ if (!child || typeof child.pid !== "number") return;
2642
+ if (!IS_WINDOWS) try {
2643
+ process.kill(-child.pid, "SIGTERM");
2644
+ } catch {}
2645
+ if (child.exitCode === null && child.signalCode === null) {
2646
+ if (!await signalAndWait(child.pid, "SIGTERM", 3e3)) {
2647
+ p.log.warn(`Started engine pid ${child.pid} survived shutdown; lifecycle state was preserved.`);
2648
+ return;
2649
+ }
2650
+ }
2651
+ if (readEnginePidfile() === child.pid) {
2652
+ clearEnginePidfile();
2653
+ clearEngineState();
2654
+ }
2655
+ }
1922
2656
  function printCapturedStartupStderr() {
1923
2657
  const stderr = redactCredentialUrls(activeStartupStderr.text().trim());
1924
2658
  if (IS_VERBOSE && stderr) p.note(stderr, "engine stderr");
1925
2659
  }
1926
- function printDockerStartupLogs() {
2660
+ function readComposeStartupLogs() {
1927
2661
  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");
2662
+ if (state?.kind !== "compose") return null;
2663
+ try {
2664
+ const text = readComposeLogFromOffset(state.logFile, state.logStartOffset ?? 0).trim();
2665
+ return {
2666
+ path: state.logFile,
2667
+ text: redactCredentialUrls(text.slice(-16 * 1024))
2668
+ };
2669
+ } catch {
2670
+ return {
2671
+ path: state.logFile,
2672
+ text: "(Compose log file could not be read.)"
2673
+ };
2674
+ }
1950
2675
  }
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
- }
2676
+ function printComposeStartupLogs() {
2677
+ const logs = readComposeStartupLogs();
2678
+ if (!logs) return;
2679
+ if (logs.text) p.note(`Log file: ${logs.path}\n\n${logs.text}`, "Recent Worker Compose output");
2680
+ }
2681
+ function composePidMatches(pid, state) {
2682
+ try {
2683
+ let command = "";
2684
+ let executable = "";
2685
+ if (IS_WINDOWS) {
2686
+ const result = execFileSync("powershell.exe", [
2687
+ "-NoProfile",
2688
+ "-NonInteractive",
2689
+ "-Command",
2690
+ `$process = Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'; if ($process) { @{ command = $process.CommandLine; executable = $process.ExecutablePath } | ConvertTo-Json -Compress }`
2691
+ ], {
2692
+ encoding: "utf8",
2693
+ stdio: [
2694
+ "ignore",
2695
+ "pipe",
2696
+ "ignore"
2697
+ ]
2698
+ }).trim();
2699
+ const processInfo = JSON.parse(result);
2700
+ command = processInfo.command ?? "";
2701
+ executable = processInfo.executable ?? "";
2702
+ } else if (platform() === "darwin") {
2703
+ command = execFileSync("ps", [
2704
+ "-p",
2705
+ String(pid),
2706
+ "-o",
2707
+ "command="
2708
+ ], {
2709
+ encoding: "utf8",
2710
+ stdio: [
2711
+ "ignore",
2712
+ "pipe",
2713
+ "ignore"
2714
+ ]
2715
+ }).trim();
2716
+ executable = readUnixProcessExecutable(pid, "darwin");
2717
+ } else {
2718
+ command = execFileSync("ps", [
2719
+ "-p",
2720
+ String(pid),
2721
+ "-o",
2722
+ "command="
2723
+ ], {
2724
+ encoding: "utf8",
2725
+ stdio: [
2726
+ "ignore",
2727
+ "pipe",
2728
+ "ignore"
2729
+ ]
2730
+ }).trim();
2731
+ executable = readUnixProcessExecutable(pid, "linux");
1984
2732
  }
1985
- });
1986
- child.unref();
1987
- return child;
2733
+ if (!(command.includes("compose") && command.includes("compose --namespace") && command.includes(state.namespace) && command.includes(state.file))) return false;
2734
+ if (!executable) return false;
2735
+ return sameResolvedExecutable(executable, state.binPath, IS_WINDOWS);
2736
+ } catch {
2737
+ return false;
2738
+ }
1988
2739
  }
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();
2740
+ function composeChildProcesses(state) {
2741
+ if (IS_WINDOWS || !state.composePid) return [];
1994
2742
  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);
2743
+ const rows = execFileSync("ps", ["-eo", "pid=,ppid=,pgid=,command="], {
2744
+ encoding: "utf8",
2745
+ stdio: [
2746
+ "ignore",
2747
+ "pipe",
2748
+ "ignore"
2749
+ ]
2750
+ }).split(/\r?\n/).map((row) => {
2751
+ const match = row.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
2752
+ return match ? {
2753
+ pid: Number(match[1]),
2754
+ ppid: Number(match[2]),
2755
+ pgid: Number(match[3]),
2756
+ args: match[4]
2757
+ } : null;
2758
+ }).filter((row) => row !== null);
2759
+ const descendants = new Set([state.composePid]);
2760
+ let changed = true;
2761
+ while (changed) {
2762
+ changed = false;
2763
+ for (const row of rows) if (!descendants.has(row.pid) && descendants.has(row.ppid)) {
2764
+ descendants.add(row.pid);
2765
+ changed = true;
2766
+ }
2001
2767
  }
2002
- return {
2003
- configPath,
2004
- cwd: process.cwd()
2005
- };
2768
+ return rows.filter((row) => row.pid !== state.composePid && descendants.has(row.pid)).map(({ pid, pgid, args }) => ({
2769
+ pid,
2770
+ pgid,
2771
+ args
2772
+ }));
2773
+ } catch {
2774
+ return [];
2006
2775
  }
2007
- 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");
2776
+ }
2777
+ function composeChildPids(state) {
2778
+ return [...new Set(composeChildProcesses(state).map((child) => child.pid))];
2779
+ }
2780
+ function composeEnginePid(state) {
2781
+ return composeChildProcesses(state).find(({ args }) => args.includes(" --config ") && args.includes(state.namespace))?.pid;
2782
+ }
2783
+ async function terminateComposeChildren(state) {
2784
+ if (IS_WINDOWS) return true;
2785
+ const groups = [...new Set(composeChildProcesses(state).map((child) => child.pgid).filter((pgid) => pgid !== state.composePid))];
2786
+ let stopped = true;
2787
+ for (const pgid of groups) {
2027
2788
  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`);
2789
+ process.kill(-pgid, "SIGTERM");
2030
2790
  } 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);
2791
+ if (err.code !== "ESRCH") stopped = false;
2792
+ continue;
2033
2793
  }
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)}`);
2794
+ const deadline = Date.now() + 5e3;
2795
+ while (Date.now() < deadline) {
2796
+ try {
2797
+ process.kill(-pgid, 0);
2798
+ } catch {
2799
+ break;
2041
2800
  }
2801
+ await new Promise((resolve) => setTimeout(resolve, 100));
2042
2802
  }
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
- };
2803
+ try {
2804
+ process.kill(-pgid, 0);
2805
+ process.kill(-pgid, "SIGKILL");
2806
+ } catch {}
2058
2807
  }
2808
+ for (const pid of composeChildPids(state)) if (pidAlive(pid) && !await signalAndWait(pid, "SIGTERM", 1e3)) stopped = false;
2809
+ return stopped;
2059
2810
  }
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
2811
+ async function startIiiBin(iiiBin) {
2812
+ const workerBinDir = process.env["AGENTMEMORY_WORKER_BIN_DIR"]?.trim() || void 0;
2813
+ 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}`);
2814
+ const envPath = process.env["AGENTMEMORY_ENV_FILE"] || join(homedir(), ".agentmemory", ".env");
2815
+ const namespace = composeNamespace(getRestPort());
2816
+ const projectRoot = resolve(__dirname, "..");
2817
+ const ports = {
2818
+ restPort: getRestPort(),
2819
+ viewerPort: getConfiguredViewerPort(),
2820
+ enginePort: getEnginePort()
2821
+ };
2822
+ await assertComposePortsFree([
2823
+ ports.restPort,
2824
+ ports.viewerPort,
2825
+ ports.enginePort
2826
+ ]);
2827
+ const compose = startComposeDaemon({
2828
+ ports,
2829
+ dataDir: dataDirResolution.dataDir,
2830
+ projectRoot,
2831
+ nodeBin: process.execPath,
2832
+ homeDir: homedir(),
2833
+ ...existsSync(envPath) ? { envFile: envPath } : {},
2834
+ ...workerBinDir ? { workerBinDir } : {},
2835
+ cwd: process.cwd(),
2836
+ stateBackend: { kind: getStateBackend() },
2837
+ ...getStateBackend() === "redis" && getRedisUrl() ? { redisUrl: getRedisUrl() } : {},
2838
+ workerEnvironment: process.env,
2839
+ disableRestartPolicy: process.env["AGENTMEMORY_DISABLE_COMPOSE_RESTART"] === "1",
2840
+ startupTimeoutMs: startupTimeouts().workerMs,
2841
+ linux: process.platform === "linux",
2842
+ stateDir: join(dataDirResolution.dataDir, "compose-state"),
2843
+ logFile: join(homedir(), ".agentmemory", `compose-${getRestPort()}.log`),
2844
+ iiiBin,
2845
+ env: process.env
2846
+ });
2847
+ spawnedComposeDaemon = compose.child;
2848
+ composeStartupCompleted = false;
2849
+ if (typeof compose.child.pid !== "number") throw new Error("Failed to spawn the iii Compose daemon.");
2850
+ const state = {
2851
+ kind: "compose",
2852
+ restPort: getRestPort(),
2853
+ viewerPort: getConfiguredViewerPort(),
2854
+ enginePort: getEnginePort(),
2855
+ engineUrl: `ws://127.0.0.1:${getEnginePort()}`,
2856
+ namespace,
2857
+ file: compose.file,
2858
+ stateDir: compose.stateDir,
2859
+ logFile: compose.logFile,
2860
+ logStartOffset: compose.logStartOffset,
2861
+ binPath: iiiBin,
2862
+ composePid: compose.child.pid,
2863
+ ...workerBinDir ? { workerBinDir: resolve(process.cwd(), workerBinDir) } : {}
2864
+ };
2865
+ writeComposeState(engineStatePath(), {
2866
+ ...state,
2867
+ workerVersions: workerBinDir ? { local: true } : COMPOSE_WORKER_VERSIONS
2868
+ });
2869
+ engineReadinessDeadline = startupDeadline(startupTimeouts().engineMs);
2870
+ compose.child.on("error", (error) => {
2871
+ if (spawnedComposeDaemon === compose.child) startupFailure = {
2872
+ kind: "engine-crashed",
2873
+ stderr: redactCredentialUrls(error.message),
2874
+ binary: iiiBin
2875
+ };
2876
+ });
2877
+ compose.child.on("exit", (code, signal) => {
2878
+ if (spawnedComposeDaemon === compose.child && !composeStartupCompleted && (code !== 0 || signal !== null)) startupFailure = {
2879
+ kind: "engine-crashed",
2880
+ stderr: signal ? `Compose exited from ${signal}` : `Compose exited with code ${code}`,
2881
+ binary: iiiBin
2882
+ };
2068
2883
  });
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"));
2884
+ const s = p.spinner();
2885
+ s.start(`Starting iii Worker Compose: ${iiiBin}`);
2886
+ s.stop(c.ok(`Compose daemon started (pid ${compose.child.pid})`));
2075
2887
  return true;
2076
2888
  }
2077
2889
  function pickCompatibleIii(candidates) {
@@ -2082,11 +2894,9 @@ function pickCompatibleIii(candidates) {
2082
2894
  }
2083
2895
  return null;
2084
2896
  }
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
2897
  async function startEngine() {
2898
+ startupFailure = null;
2899
+ engineReadinessDeadline = null;
2090
2900
  let stateBackendKind;
2091
2901
  try {
2092
2902
  stateBackendKind = getStateBackend();
@@ -2098,55 +2908,43 @@ async function startEngine() {
2098
2908
  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
2909
  process.exit(1);
2100
2910
  }
2101
- await assertRuntimePortOwnership();
2102
2911
  const persistedState = readEngineState();
2103
2912
  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;
2913
+ p.log.error(legacyDockerStateMessage(persistedState));
2914
+ return false;
2915
+ }
2916
+ await assertRuntimePortOwnership();
2917
+ if (persistedState?.kind === "compose") {
2918
+ if (await isEngineRunning()) {
2919
+ p.log.error("The managed Worker Compose engine is already running; it should have been reconciled before attempting a fresh start.");
2920
+ process.exit(1);
2921
+ }
2922
+ if (persistedState.restPort === getRestPort()) {
2923
+ if (persistedState.composePid && pidAlive(persistedState.composePid)) {
2924
+ p.log.error(`Worker Compose daemon ${persistedState.composePid} is still alive. Stop it before retrying.`);
2925
+ process.exit(1);
2926
+ }
2927
+ clearEngineState();
2110
2928
  }
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
2929
  }
2116
- const configPath = findIiiConfig();
2117
2930
  warnIfRelocatedDataDir();
2118
2931
  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";
2932
+ vlog(`iii binary: ${pathIii ?? "(not on PATH)"}`);
2127
2933
  const fallbacks = fallbackIiiPaths().filter((p) => existsSync(p));
2128
2934
  for (const f of fallbacks) vlog(`fallback iii at ${f} reports version: ${iiiBinVersion(f) ?? "unknown"}`);
2129
2935
  let iiiBin = pickCompatibleIii([pathIii, ...fallbacks]);
2130
- if (iiiBin && configPath && !(dockerOptIn && dockerBin && composeFile)) {
2936
+ if (iiiBin) {
2131
2937
  if (iiiBin !== pathIii) {
2132
2938
  p.log.info(`Using iii at: ${c.dim(iiiBin)} (v${c.accent(IIPINNED_VERSION)})`);
2133
2939
  process.env["PATH"] = `${dirname(iiiBin)}${delimiter}${process.env["PATH"] ?? ""}`;
2134
2940
  }
2135
- return startIiiBin(iiiBin, configPath);
2941
+ return startIiiBin(iiiBin);
2136
2942
  }
2137
2943
  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
2944
  mkdirSync(dataDirResolution.dataDir, { recursive: true });
2144
- vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
2145
2945
  const interactive = !!process.stdin.isTTY && !process.env["CI"];
2146
2946
  let choice;
2147
- const pathIiiMismatch = pathIii !== null && resolveCompatibleIii(pathIii) === null;
2148
- if (dockerOptIn && dockerBin && composeFile) choice = "docker";
2149
- else if (pathIiiMismatch) {
2947
+ if (pathIii !== null && resolveCompatibleIii(pathIii) === null) {
2150
2948
  choice = "install";
2151
2949
  const detected = iiiBinVersion(pathIii);
2152
2950
  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).`);
@@ -2160,11 +2958,6 @@ async function startEngine() {
2160
2958
  label: `Install iii v${IIPINNED_VERSION} to ~/.agentmemory/bin (~6MB, ~5s)`,
2161
2959
  hint: "recommended"
2162
2960
  }];
2163
- if (dockerBin && composeFile) options.push({
2164
- value: "docker",
2165
- label: "Use Docker compose",
2166
- hint: "advanced"
2167
- });
2168
2961
  options.push({
2169
2962
  value: "manual",
2170
2963
  label: "Show manual install steps and exit"
@@ -2189,87 +2982,29 @@ async function startEngine() {
2189
2982
  if (result.ok && result.binPath) {
2190
2983
  process.env["PATH"] = `${dirname(result.binPath)}${delimiter}${process.env["PATH"] ?? ""}`;
2191
2984
  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;
2985
+ return startIiiBin(iiiBin);
2207
2986
  }
2987
+ startupFailure = { kind: "no-engine" };
2988
+ return false;
2208
2989
  }
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" };
2990
+ startupFailure = { kind: "no-engine" };
2231
2991
  return false;
2232
2992
  }
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;
2993
+ async function waitForEngine(timeoutMs = startupTimeouts().engineMs) {
2994
+ const state = readEngineState();
2995
+ const endpoint = state?.kind === "compose" ? managedComposeReadinessUrl(state.restPort) : getBaseUrl();
2996
+ const probe = state?.kind === "compose" ? (signal) => probeEngine(endpoint, signal) : (signal) => probeEngine(endpoint, signal);
2997
+ return waitForReadiness(engineReadinessDeadline ?? startupDeadline(timeoutMs), probe, {
2998
+ intervalMs: 250,
2999
+ probeTimeoutMs: 750,
3000
+ stopped: () => startupInterrupted || engineStartupFailed()
3001
+ });
2240
3002
  }
2241
- async function reconcilePersistedDockerEngine() {
3003
+ async function waitForPersistedNativeEngine() {
2242
3004
  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();
3005
+ const pid = readEnginePidfile();
3006
+ if (state?.kind !== "native" || !pid || !pidAlive(pid) || isForeignPortHolder(pidCommand(pid))) return false;
3007
+ if (!await waitForEngine()) throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
2273
3008
  return true;
2274
3009
  }
2275
3010
  function installInstructions() {
@@ -2288,9 +3023,6 @@ function installInstructions() {
2288
3023
  " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
2289
3024
  " 4. Re-run: npx @agentmemory/agentmemory",
2290
3025
  "",
2291
- ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
2292
- " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
2293
- "",
2294
3026
  "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
2295
3027
  "",
2296
3028
  "Docs: https://iii.dev/docs"
@@ -2303,9 +3035,6 @@ function installInstructions() {
2303
3035
  linuxInstall,
2304
3036
  " Then re-run: npx @agentmemory/agentmemory",
2305
3037
  "",
2306
- ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
2307
- " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
2308
- "",
2309
3038
  "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
2310
3039
  "",
2311
3040
  "Docs: https://iii.dev/docs"
@@ -2314,85 +3043,101 @@ function installInstructions() {
2314
3043
  function portInUseDiagnostic(port) {
2315
3044
  return IS_WINDOWS ? ` netstat -ano | findstr :${port}` : ` lsof -i :${port} # or: ss -tlnp | grep :${port}`;
2316
3045
  }
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
3046
  function printReadyHint() {
2338
3047
  const restUrl = getBaseUrl();
2339
3048
  const viewerUrl = getViewerUrl();
2340
- const engineHost = getEngineHost();
2341
- const streamUrl = `ws://${engineHost}:${getStreamPort()}`;
2342
- const engineUrl = `ws://${engineHost}:${getEnginePort()}`;
3049
+ const engineUrl = process.env["III_ENGINE_URL"] || `ws://localhost:${getEnginePort()}`;
2343
3050
  const invocation = isInvokedViaNpx() ? "npx @agentmemory/agentmemory" : "agentmemory";
2344
- const consolePort = defaultConsolePort(getConfiguredViewerPort());
2345
3051
  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
3052
  const lines = [
2348
3053
  `${c.label("REST API")} ${c.url(restUrl)}`,
2349
3054
  `${c.label("Viewer")} ${c.url(viewerUrl)}`,
2350
- `${c.label("Streams")} ${c.url(streamUrl)}`,
2351
- `${c.label("Engine")} ${c.url(engineUrl)}`,
2352
- consoleLine
3055
+ `${c.label("Engine")} ${c.url(engineUrl)}`
2353
3056
  ];
2354
3057
  p.note(lines.join("\n"), `agentmemory v${c.accent(VERSION)}`);
2355
3058
  const demoCommand = `${invocation} demo${portFlags}`;
2356
3059
  process.stdout.write(`\n${c.dim("Try:")} ${c.cmd(demoCommand)}\n`);
2357
3060
  }
3061
+ async function reconcilePersistedComposeEngine() {
3062
+ const state = readEngineState();
3063
+ if (state?.kind !== "compose") return false;
3064
+ if (!await isEngineRunning()) {
3065
+ if (state.composePid && pidAlive(state.composePid)) {
3066
+ 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.`);
3067
+ process.exit(1);
3068
+ }
3069
+ if (await isEngineRunning()) {
3070
+ 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.`);
3071
+ process.exit(1);
3072
+ }
3073
+ clearEngineState();
3074
+ p.log.info("Found stale Worker Compose lifecycle state; starting a fresh managed instance.");
3075
+ return false;
3076
+ }
3077
+ const version = iiiBinVersion(state.binPath);
3078
+ if (state.composePid && !composePidMatches(state.composePid, state)) {
3079
+ p.log.error(`Compose metadata points to PID ${state.composePid}, but its process identity does not match the saved engine/file/namespace.`);
3080
+ process.exit(1);
3081
+ }
3082
+ if (version !== IIPINNED_VERSION) {
3083
+ p.log.error(`Managed Compose engine is ${version ?? "unverified"}; expected ${IIPINNED_VERSION}. Run agentmemory stop before changing the runtime.`);
3084
+ process.exit(1);
3085
+ }
3086
+ try {
3087
+ if (!ownedComposeProject(await composeCall({
3088
+ engineUrl: state.engineUrl,
3089
+ namespace: state.namespace,
3090
+ functionId: "compose::status",
3091
+ file: state.file
3092
+ }), state.file)) throw new Error("Compose daemon does not report the owned worker-compose.yaml project.");
3093
+ } catch (err) {
3094
+ p.log.error(`Could not verify the managed Compose project: ${err instanceof Error ? err.message : String(err)}. See ${state.logFile}.`);
3095
+ process.exit(1);
3096
+ }
3097
+ return true;
3098
+ }
2358
3099
  async function main() {
3100
+ startupTimeouts();
2359
3101
  await assertRuntimePortOwnership();
2360
3102
  try {
2361
- if ((await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(1500) })).ok) {
3103
+ if ((await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(1500) })).ok && readEngineState()?.kind !== "compose") {
2362
3104
  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
3105
  process.exit(1);
2364
3106
  }
2365
3107
  } catch {}
3108
+ const legacyState = readEngineState();
3109
+ if (legacyState?.kind === "docker") {
3110
+ p.intro("agentmemory");
3111
+ p.log.error(legacyDockerStateMessage(legacyState));
3112
+ process.exit(1);
3113
+ }
2366
3114
  if (IS_RESET) resetPrefs();
2367
3115
  const firstRun = isFirstRun();
2368
3116
  const prefs = readPrefs();
2369
3117
  if (firstRun || IS_RESET || IS_VERBOSE || !prefs.skipSplash) renderSplash(VERSION);
2370
3118
  if (firstRun || IS_RESET) await runOnboarding();
3119
+ beginStartup();
2371
3120
  if (skipEngine) {
2372
3121
  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
- }
3122
+ await startWorkerForEngineState();
3123
+ await maybeOfferGlobalInstall();
3124
+ printReadyHint();
3125
+ return;
3126
+ }
3127
+ if (await reconcilePersistedComposeEngine()) {
3128
+ await startWorkerForEngineState();
3129
+ await maybeOfferGlobalInstall();
3130
+ printReadyHint();
2378
3131
  return;
2379
3132
  }
2380
- if (await reconcilePersistedDockerEngine()) return;
2381
- if (await isEngineRunning()) {
3133
+ if (await isEngineRunning() || await waitForPersistedNativeEngine()) {
2382
3134
  if (IS_VERBOSE) p.log.success("iii-engine is running");
2383
3135
  const persisted = readEngineState();
2384
3136
  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;
3137
+ const detected = persisted?.kind === "compose" ? iiiBinVersion(persisted.binPath) : attachedBin ? iiiBinVersion(attachedBin) : null;
2386
3138
  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;
3139
+ 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.`);
3140
+ process.exit(1);
2396
3141
  }
2397
3142
  const detectedLabel = detected ? `v${detected}` : "an unverified version";
2398
3143
  const base = isInvokedViaNpx() ? "npx @agentmemory/agentmemory" : "agentmemory";
@@ -2419,17 +3164,42 @@ async function main() {
2419
3164
  if (!await startEngine()) {
2420
3165
  p.log.error("Could not start iii-engine.");
2421
3166
  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
3167
  p.note(lines.join("\n"), "Setup required");
2424
- process.exit(1);
3168
+ throw new Error("iii-engine could not be started.");
2425
3169
  }
2426
3170
  const s = p.spinner();
2427
3171
  s.start("Waiting for iii-engine to be ready...");
2428
- if (!await waitForEngine(15e3)) {
3172
+ const ready = await waitForEngine();
3173
+ if (ready) {
3174
+ const composeState = readEngineState();
3175
+ if (composeState?.kind === "compose") {
3176
+ composeState.enginePid = composeEnginePid(composeState);
3177
+ writeComposeState(engineStatePath(), {
3178
+ ...composeState,
3179
+ workerVersions: composeState.workerVersions ?? COMPOSE_WORKER_VERSIONS
3180
+ });
3181
+ }
3182
+ }
3183
+ if (!ready) {
2429
3184
  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") {
3185
+ let composeState = readEngineState();
3186
+ let composeLog = composeState?.kind === "compose" ? readComposeStartupLogs() : null;
3187
+ if (composeState?.kind === "compose" && spawnedComposeDaemon?.exitCode !== null) {
3188
+ await new Promise((resolve) => setTimeout(resolve, 150));
3189
+ composeLog = readComposeStartupLogs() ?? composeLog;
3190
+ }
3191
+ let registryFailure = null;
3192
+ if (composeState?.kind === "compose") {
3193
+ const stateFailure = composeState.workerFailure ?? composeStateRegistryFailure(dataDirResolution.dataDir, composeState.file);
3194
+ const logFailure = composeLog ? composeRegistryFailureMessage(composeLog.text, composeState.workerBinDir) : null;
3195
+ if (stateFailure) registryFailure = `Worker registry unavailable while resolving state/http; retry later or set AGENTMEMORY_WORKER_BIN_DIR. Compose reported: ${stateFailure}`;
3196
+ else registryFailure = logFailure;
3197
+ }
3198
+ const composeLogTail = composeLog?.text.trim().split(/\r?\n/).slice(-12).join("\n");
3199
+ const shortFailure = registryFailure || startupFailure?.stderr || (composeState?.kind === "compose" && composeLogTail ? `Worker Compose startup failed. Recent Compose output:\n${composeLogTail}` : startupTimeoutMessage("engine", startupTimeouts().engineMs));
3200
+ s.stop(shortFailure);
3201
+ printComposeStartupLogs();
3202
+ if (startupFailure?.kind === "engine-crashed") {
2433
3203
  p.log.error("The iii-engine process crashed on startup.");
2434
3204
  if (startupFailure.binary) p.log.info(`Binary: ${startupFailure.binary}`);
2435
3205
  if (startupFailure.stderr) p.note(startupFailure.stderr, "engine stderr");
@@ -2438,7 +3208,6 @@ async function main() {
2438
3208
  "Common causes:",
2439
3209
  ` - iii-engine version mismatch — reinstall the pinned v${IIPINNED_VERSION} binary`,
2440
3210
  " (sh script on macOS/Linux, GitHub release zip on Windows)",
2441
- " - Docker Desktop not running (if you're using the Docker path)",
2442
3211
  " - Port already in use (see below)",
2443
3212
  "",
2444
3213
  "See https://iii.dev/docs for current install instructions."
@@ -2456,17 +3225,26 @@ async function main() {
2456
3225
  "Re-run with --verbose to see engine stderr."
2457
3226
  ].join("\n"), "Troubleshooting");
2458
3227
  }
2459
- process.exit(1);
3228
+ if (composeLog?.text) p.log.error(`Compose log: ${composeLog.path}`);
3229
+ throw new Error(shortFailure);
2460
3230
  }
2461
3231
  s.stop(c.ok("iii-engine is ready"));
2462
3232
  await startWorkerForEngineState();
2463
- if (!await waitForAgentmemoryReady(15e3)) {
2464
- p.log.error("agentmemory worker did not become ready within 15s.");
2465
- process.exit(1);
3233
+ const managedCompose = readEngineState();
3234
+ if (managedCompose?.kind === "compose" && !managedCompose.enginePid) {
3235
+ managedCompose.enginePid = composeChildPids(managedCompose)[0];
3236
+ writeComposeState(engineStatePath(), {
3237
+ ...managedCompose,
3238
+ workerVersions: managedCompose.workerVersions ?? COMPOSE_WORKER_VERSIONS
3239
+ });
2466
3240
  }
2467
3241
  await maybeOfferGlobalInstall();
2468
3242
  printReadyHint();
2469
3243
  writePrefs({ skipSplash: true });
3244
+ if (managedCompose?.kind === "compose") {
3245
+ await new Promise((resolve) => process.stdout.write("", () => resolve()));
3246
+ process.exit(0);
3247
+ }
2470
3248
  }
2471
3249
  async function apiFetch(base, path, timeoutMs = 5e3) {
2472
3250
  try {
@@ -2480,10 +3258,29 @@ async function apiFetch(base, path, timeoutMs = 5e3) {
2480
3258
  }
2481
3259
  }
2482
3260
  async function runStatus() {
2483
- getRestPort();
2484
3261
  const base = getBaseUrl();
2485
3262
  p.intro("agentmemory status");
3263
+ const state = readEngineState();
3264
+ const savedState = !(shouldUseAgentmemoryUrl() && process.env["AGENTMEMORY_URL"]) && (state?.kind !== "compose" || state.restPort === getRestPort()) ? state : null;
3265
+ if (savedState?.kind === "docker") p.log.warn(legacyDockerStateMessage(savedState));
2486
3266
  if (!await isEngineRunning()) {
3267
+ if (savedState?.kind === "compose" && savedState.composePid && pidAlive(savedState.composePid)) try {
3268
+ const failure = composeWorkerFailureSummary(ownedComposeProject(await composeCall({
3269
+ engineUrl: savedState.engineUrl,
3270
+ namespace: savedState.namespace,
3271
+ functionId: "compose::status",
3272
+ file: savedState.file
3273
+ }), savedState.file));
3274
+ if (failure) {
3275
+ p.log.error(`Worker Compose failed after exhausting restart attempts; REST is unavailable at ${base}.`);
3276
+ p.log.error(failure);
3277
+ p.log.info(`Inspect ${savedState.logFile}, then run agentmemory stop before retrying.`);
3278
+ process.exit(1);
3279
+ }
3280
+ } catch (error) {
3281
+ const message = error instanceof Error ? error.message : String(error);
3282
+ p.log.warn(`Worker Compose status unavailable: ${message}`);
3283
+ }
2487
3284
  p.log.error(`Not running — no response at ${base}`);
2488
3285
  p.log.info("Start with: npx @agentmemory/agentmemory");
2489
3286
  process.exit(1);
@@ -2503,8 +3300,8 @@ async function runStatus() {
2503
3300
  const version = healthRes?.version || "?";
2504
3301
  const sessionList = Array.isArray(sessionsRes?.sessions) ? sessionsRes.sessions : [];
2505
3302
  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);
3303
+ const nodes = graphRes?.available === false ? "unknown" : graphRes?.totalNodes ?? graphRes?.nodes ?? graphRes?.nodeCount ?? "unknown";
3304
+ const edges = graphRes?.available === false ? "unknown" : graphRes?.totalEdges ?? graphRes?.edges ?? graphRes?.edgeCount ?? "unknown";
2508
3305
  const cb = healthRes?.circuitBreaker?.state || "closed";
2509
3306
  const heapMB = h?.memory ? Math.round(h.memory.heapUsed / 1048576) : 0;
2510
3307
  const uptime = h?.uptimeSeconds ? Math.round(h.uptimeSeconds) : 0;
@@ -2534,7 +3331,8 @@ async function runStatus() {
2534
3331
  }
2535
3332
  if (flagsRes) {
2536
3333
  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");
3334
+ const embedding = embeddingStatusLabel(flagsRes.embeddingProvider);
3335
+ const embed = embedding ? pc.green(`✓ ${embedding}`) : pc.dim("bm25-only");
2538
3336
  const flagRows = (flagsRes.flags || []).map((f) => ` ${f.enabled ? pc.green("✓") : pc.dim("✗")} ${pc.bold(f.key.padEnd(32))} ${f.label}`);
2539
3337
  lines.push("");
2540
3338
  lines.push(`Provider: ${provider}`);
@@ -2549,6 +3347,21 @@ async function runStatus() {
2549
3347
  lines.push("");
2550
3348
  lines.push(`Followup rate: ${hits}/${total} (${pct}%) within ${followupRes.windowSeconds}s — directional, may overcount on refinement`);
2551
3349
  }
3350
+ const managed = savedState;
3351
+ if (managed?.kind === "compose") try {
3352
+ const composeProject = ownedComposeProject(await composeCall({
3353
+ engineUrl: managed.engineUrl,
3354
+ namespace: managed.namespace,
3355
+ functionId: "compose::status",
3356
+ file: managed.file
3357
+ }), managed.file);
3358
+ const containers = composeProject?.containers ?? [];
3359
+ const failed = composeWorkerFailureSummary(composeProject ?? null);
3360
+ if (failed) lines.push("", "Worker Compose failure (restart attempts exhausted):", ...failed.split("\n").map((line) => ` ${line}`));
3361
+ lines.push("", "Compose workers:", ...containers.map((container) => ` ${container.container ?? "?"}: ${container.state ?? "unknown"}${container.last_error ? ` — ${container.last_error}` : ""}`));
3362
+ } catch (error) {
3363
+ lines.push("", `Compose status unavailable: ${error instanceof Error ? error.message : String(error)}`);
3364
+ }
2552
3365
  p.note(lines.join("\n"), "agentmemory");
2553
3366
  } catch (err) {
2554
3367
  p.log.error(err instanceof Error ? err.message : String(err));
@@ -2594,34 +3407,50 @@ function checkClaudeCodeHooks() {
2594
3407
  if (content.includes("Loading hooks from plugin: agentmemory")) return { state: "loaded" };
2595
3408
  return { state: "not-loaded" };
2596
3409
  }
3410
+ function doctorEnvPath() {
3411
+ return process.env["AGENTMEMORY_ENV_FILE"] || join(homedir(), ".agentmemory", ".env");
3412
+ }
2597
3413
  function buildDoctorContext() {
2598
3414
  return {
2599
3415
  baseUrl: getBaseUrl(),
2600
3416
  viewerUrl: getViewerUrl(),
2601
- envPath: join(homedir(), ".agentmemory", ".env"),
2602
- pidfilePath: enginePidfilePath(),
3417
+ envPath: doctorEnvPath(),
3418
+ pidfilePath: readEngineState()?.kind === "compose" ? engineStatePath() : enginePidfilePath(),
2603
3419
  enginePath: engineStatePath(),
2604
3420
  pinnedVersion: IIPINNED_VERSION
2605
3421
  };
2606
3422
  }
2607
3423
  function buildDoctorEffects() {
2608
3424
  return {
2609
- envFileExists: () => existsSync(join(homedir(), ".agentmemory", ".env")),
3425
+ envFileExists: () => existsSync(doctorEnvPath()),
2610
3426
  readEnvFile: () => {
2611
3427
  try {
2612
- return parseEnvFile(readFileSync(join(homedir(), ".agentmemory", ".env"), "utf-8"));
3428
+ return parseEnvFile(readFileSync(doctorEnvPath(), "utf-8"));
2613
3429
  } catch {
2614
3430
  return {};
2615
3431
  }
2616
3432
  },
2617
- pidfileExists: () => existsSync(enginePidfilePath()),
3433
+ processEnv: () => shellEnv,
3434
+ pidfileExists: () => {
3435
+ const state = readEngineState();
3436
+ return state?.kind === "compose" ? Boolean(state.composePid) : existsSync(enginePidfilePath());
3437
+ },
2618
3438
  pidfilePidIsAlive: () => {
3439
+ const state = readEngineState();
3440
+ if (state?.kind === "compose") return state.composePid ? pidAlive(state.composePid) : null;
2619
3441
  const pid = readEnginePidfile();
2620
3442
  if (pid === null) return null;
2621
3443
  return pidAlive(pid);
2622
3444
  },
2623
- findIiiBinary: () => whichBinary("iii"),
2624
- localBinIiiPath: () => privateIiiPath(),
3445
+ findIiiBinary: () => {
3446
+ const state = readEngineState();
3447
+ if (state?.kind === "compose") return existsSync(state.binPath) ? state.binPath : null;
3448
+ return whichBinary("iii");
3449
+ },
3450
+ localBinIiiPath: () => {
3451
+ const state = readEngineState();
3452
+ return state?.kind === "compose" ? state.binPath : privateIiiPath();
3453
+ },
2625
3454
  iiiBinaryVersion: (binPath) => iiiBinVersion(binPath),
2626
3455
  viewerReachable: async (timeoutMs = 2e3) => {
2627
3456
  try {
@@ -2678,6 +3507,14 @@ function buildDoctorEffects() {
2678
3507
  },
2679
3508
  runStop: async () => {
2680
3509
  try {
3510
+ startupTimeouts();
3511
+ if (readEngineState()?.kind === "compose") {
3512
+ await runStop();
3513
+ return {
3514
+ ok: true,
3515
+ message: "Worker Compose project stopped."
3516
+ };
3517
+ }
2681
3518
  const portPids = findEnginePidsByPort(getRestPort());
2682
3519
  const pidfilePid = readEnginePidfile();
2683
3520
  if (portPids.length === 0 && pidfilePid === null) {
@@ -2708,16 +3545,18 @@ function buildDoctorEffects() {
2708
3545
  },
2709
3546
  runStart: async () => {
2710
3547
  try {
2711
- if (!await startEngine()) return {
2712
- ok: false,
2713
- message: "startEngine() returned false"
2714
- };
2715
- const ready = await waitForEngine(15e3);
3548
+ startupTimeouts();
3549
+ beginStartup();
3550
+ if (!await startEngine()) throw new Error("iii-engine could not be started.");
3551
+ if (!await waitForEngine()) throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
3552
+ await startWorkerForEngineState(true);
2716
3553
  return {
2717
- ok: ready,
2718
- message: ready ? "Engine ready" : "Engine did not become ready within 15s"
3554
+ ok: true,
3555
+ message: "Engine and agentmemory worker ready"
2719
3556
  };
2720
3557
  } catch (err) {
3558
+ await runStartupCleanup(1);
3559
+ finishStartup();
2721
3560
  return {
2722
3561
  ok: false,
2723
3562
  message: err instanceof Error ? err.message : String(err)
@@ -2746,8 +3585,9 @@ async function passiveServerChecks() {
2746
3585
  apiFetch(base, "graph/stats", 3e3)
2747
3586
  ]);
2748
3587
  const hasLlm = flags?.provider === "llm";
2749
- const hasEmbed = flags?.embeddingProvider === "embeddings";
2750
- const graphHas = Number(graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? 0) > 0;
3588
+ const hasEmbed = embeddingStatusLabel(flags?.embeddingProvider) !== null;
3589
+ const graphNodeCount = graph?.available === false ? null : graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? null;
3590
+ const graphHas = typeof graphNodeCount === "number" && graphNodeCount > 0;
2751
3591
  checks.push({
2752
3592
  name: "Health status",
2753
3593
  ok: health?.status === "healthy",
@@ -2791,7 +3631,7 @@ async function passiveServerChecks() {
2791
3631
  checks.push({
2792
3632
  name: "Knowledge graph populated",
2793
3633
  ok: graphHas,
2794
- hint: graphHas ? void 0 : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true."
3634
+ 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
3635
  });
2796
3636
  return checks;
2797
3637
  }
@@ -3043,7 +3883,10 @@ async function seedDemoSession(base, project, session) {
3043
3883
  p.log.warn(`observe request failed for ${obs.toolName}: ${err instanceof Error ? err.message : String(err)}`);
3044
3884
  }
3045
3885
  }
3046
- await postJsonStrict(`${base}/agentmemory/session/end`, { sessionId: session.id });
3886
+ await postJsonStrict(`${base}/agentmemory/session/end`, {
3887
+ sessionId: session.id,
3888
+ final: true
3889
+ });
3047
3890
  return stored;
3048
3891
  }
3049
3892
  async function runDemoSearch(base, query) {
@@ -3102,42 +3945,26 @@ async function runInit() {
3102
3945
  p.outro(`Edit ${target} and you're set.`);
3103
3946
  }
3104
3947
  async function startServerForDemo() {
3948
+ startupTimeouts();
3105
3949
  await assertRuntimePortOwnership();
3106
3950
  if (await isAgentmemoryReady()) return async () => {};
3107
- const startedEngine = !await isEngineRunning();
3951
+ beginStartup();
3952
+ const startedEngine = !skipEngine && !(await isEngineRunning() || await waitForPersistedNativeEngine());
3108
3953
  if (startedEngine) {
3109
3954
  if (!await startEngine()) {
3110
3955
  p.log.error("Could not start iii-engine for the demo.");
3111
3956
  p.note(installInstructions().join("\n"), "Setup required");
3112
- process.exit(1);
3957
+ throw new Error("iii-engine could not be started for the demo.");
3113
3958
  }
3114
- if (!await waitForEngine(15e3)) {
3959
+ if (!await waitForEngine()) {
3115
3960
  printCapturedStartupStderr();
3116
- printDockerStartupLogs();
3117
- p.log.error("iii-engine did not become ready within 15s.");
3118
- process.exit(1);
3961
+ throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
3119
3962
  }
3120
3963
  }
3121
3964
  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
3965
  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();
3966
+ if (importedStartupWorker && readWorkerPidfile() === process.pid) clearWorkerPidfile();
3967
+ if (startedEngine) await cleanupFailedStartup();
3141
3968
  };
3142
3969
  }
3143
3970
  async function runDemo() {
@@ -3204,7 +4031,11 @@ function runCommand(command, commandArgs, options = { label: "command" }) {
3204
4031
  const result = spawnSync(command, commandArgs, {
3205
4032
  cwd: options.cwd || process.cwd(),
3206
4033
  stdio: "pipe",
3207
- encoding: "utf-8"
4034
+ encoding: "utf-8",
4035
+ ...options.timeoutMs !== void 0 ? {
4036
+ timeout: options.timeoutMs,
4037
+ killSignal: "SIGKILL"
4038
+ } : {}
3208
4039
  });
3209
4040
  if (result.status === 0) {
3210
4041
  spinner.stop(`${options.label} ${pc.green("✓")}`);
@@ -3229,7 +4060,6 @@ async function runUpgrade() {
3229
4060
  const hasPnpmLock = existsSync(join(cwd, "pnpm-lock.yaml"));
3230
4061
  const pnpmBin = whichBinary("pnpm");
3231
4062
  const npmBin = whichBinary("npm");
3232
- const dockerBin = whichBinary("docker");
3233
4063
  p.log.info(`Working directory: ${cwd}`);
3234
4064
  const requireSuccess = (ok, label) => {
3235
4065
  if (!ok) {
@@ -3269,11 +4099,6 @@ async function runUpgrade() {
3269
4099
  }
3270
4100
  if (upgradeEngine === true) await runIiiInstaller();
3271
4101
  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
4102
  p.note([
3278
4103
  "Upgrade flow completed.",
3279
4104
  "",
@@ -3341,9 +4166,14 @@ async function waitForEngineFlush() {
3341
4166
  try {
3342
4167
  stateBackend = getStateBackend();
3343
4168
  } catch {}
4169
+ if (readEngineState()?.kind === "compose") {
4170
+ const isolatedRuntime = dataDirResolution.dataDir !== join(homedir(), ".agentmemory");
4171
+ await new Promise((resolve) => setTimeout(resolve, getStateBackend() === "redis" || isolatedRuntime ? 0 : 3500));
4172
+ return;
4173
+ }
3344
4174
  const runtimePath = runtimeConfigPath(dataDirResolution.dataDir);
3345
4175
  const configTexts = [];
3346
- for (const path of engineStateConfigPaths(join(homedir(), ".agentmemory"), runtimePath)) try {
4176
+ for (const path of engineStateConfigPaths(dataDirResolution.dataDir, runtimePath)) try {
3347
4177
  configTexts.push(readFileSync(path, "utf-8"));
3348
4178
  } catch {}
3349
4179
  const waitMs = engineFlushWaitMs(stateBackend, configTexts);
@@ -3449,80 +4279,64 @@ function findEnginePidsByPort(port) {
3449
4279
  return [];
3450
4280
  }
3451
4281
  }
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.`);
4282
+ async function runStop() {
4283
+ const savedState = readEngineState();
4284
+ if (savedState?.kind === "docker") {
4285
+ p.intro("agentmemory stop");
4286
+ p.log.error(legacyDockerStateMessage(savedState));
3499
4287
  process.exit(1);
3500
4288
  }
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`);
4289
+ if (savedState?.kind === "compose" && engineStateRestPort(savedState) !== getRestPort()) {
4290
+ p.intro("agentmemory stop");
4291
+ 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
4292
  process.exit(1);
3513
4293
  }
3514
- if (!suppressOutro) p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
3515
- }
3516
- async function runStop() {
3517
4294
  await assertRuntimePortOwnership();
3518
4295
  p.intro("agentmemory stop");
3519
4296
  const port = getRestPort();
3520
4297
  const state = readEngineState();
3521
4298
  const running = await isEngineRunning();
3522
4299
  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);
4300
+ if (state?.kind === "compose") {
4301
+ const ownedDaemonAlive = Boolean(state.composePid && pidAlive(state.composePid));
4302
+ if (ownedDaemonAlive && !composePidMatches(state.composePid, state)) {
4303
+ 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.`);
4304
+ process.exit(1);
4305
+ }
4306
+ if (running && !ownedDaemonAlive) {
4307
+ 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.");
4308
+ process.exit(1);
4309
+ }
4310
+ if (running && ownedDaemonAlive) try {
4311
+ await composeCall({
4312
+ engineUrl: state.engineUrl,
4313
+ namespace: state.namespace,
4314
+ functionId: "compose::down",
4315
+ file: state.file,
4316
+ timeoutMs: 6e4
4317
+ });
4318
+ } catch (err) {
4319
+ const message = err instanceof Error ? err.message : String(err);
4320
+ if (!message.includes("ENGINE_RESTART_REQUIRED")) {
4321
+ p.log.error(`compose::down failed for ${state.file}: ${message}. Lifecycle state was preserved.`);
4322
+ process.exit(1);
4323
+ }
4324
+ p.log.warn("Compose requested an engine restart while stopping; terminating its verified owning daemon to finish shutdown.");
4325
+ }
4326
+ if (ownedDaemonAlive) {
4327
+ if (!await terminateComposeChildren(state)) {
4328
+ p.log.error(`One or more child workers of Compose ${state.composePid} did not stop; lifecycle state was preserved.`);
4329
+ process.exit(1);
4330
+ }
4331
+ if (!await signalAndWait(state.composePid, "SIGTERM", 2e4)) {
4332
+ p.log.error(`Worker Compose daemon pid ${state.composePid} did not stop; lifecycle state was preserved. Inspect ${state.logFile}.`);
4333
+ process.exit(1);
4334
+ }
4335
+ }
4336
+ clearEnginePidfile();
4337
+ clearWorkerPidfile();
4338
+ clearEngineState();
4339
+ p.outro("Stopped Worker Compose, engine, and agentmemory workers. Data was preserved.");
3526
4340
  return;
3527
4341
  }
3528
4342
  const portPids = findEnginePidsByPort(port);
@@ -3558,14 +4372,6 @@ async function runStop() {
3558
4372
  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
4373
  process.exit(1);
3560
4374
  }
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
4375
  const candidates = /* @__PURE__ */ new Set();
3570
4376
  if (pidfilePid) candidates.add(pidfilePid);
3571
4377
  for (const pid of portPids) candidates.add(pid);
@@ -3600,7 +4406,7 @@ async function runStop() {
3600
4406
  clearWorkerPidfile();
3601
4407
  if (skippedForeign.length > 0) {
3602
4408
  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.`);
4409
+ 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
4410
  process.exit(1);
3605
4411
  }
3606
4412
  if (!allStopped) {
@@ -3610,10 +4416,10 @@ async function runStop() {
3610
4416
  p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
3611
4417
  }
3612
4418
  async function runMcp() {
3613
- await import("./standalone-DKCdrf1d.mjs");
4419
+ await import("./standalone-BlqshyZ5.mjs");
3614
4420
  }
3615
4421
  async function runConnectCmd() {
3616
- const { runConnect } = await import("./connect-B4_ca6BD.mjs").then((n) => n.n);
4422
+ const { runConnect } = await import("./connect-BSLIAMpw.mjs").then((n) => n.n);
3617
4423
  await runConnect(args.slice(1));
3618
4424
  }
3619
4425
  async function runImportJsonl() {
@@ -3772,27 +4578,20 @@ async function runRemove() {
3772
4578
  const force = args.includes("--force");
3773
4579
  const keepData = args.includes("--keep-data");
3774
4580
  const ownedState = readEngineState();
4581
+ if (ownedState?.kind === "compose") {
4582
+ p.log.error("Stop the Worker Compose project with `agentmemory stop` before removing agentmemory; its data and daemon ownership are preserved.");
4583
+ process.exit(1);
4584
+ }
3775
4585
  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
- }
4586
+ p.log.error(legacyDockerStateMessage(ownedState));
4587
+ process.exit(1);
3788
4588
  }
3789
4589
  const home = homedir();
3790
4590
  const connectManifest = loadConnectManifest(home);
3791
4591
  const localBinIiiVersion = probeLocalBinIiiVersion(home);
3792
4592
  const options = {
3793
4593
  force,
3794
- keepData,
3795
- preserveRuntimeState: ownedState?.kind === "docker"
4594
+ keepData
3796
4595
  };
3797
4596
  const plan = buildRemovePlan({
3798
4597
  home,
@@ -3838,11 +4637,6 @@ async function runRemove() {
3838
4637
  }
3839
4638
  }
3840
4639
  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
4640
  const stopped = await stopNativeEngineForRemoval();
3847
4641
  if (!stopped.ok) {
3848
4642
  p.log.error(`Removal aborted: ${stopped.reason}.`);
@@ -3859,52 +4653,13 @@ async function runRemove() {
3859
4653
  p.outro("Done. agentmemory cleanly removed. The npm package itself: npm uninstall -g @agentmemory/agentmemory");
3860
4654
  }
3861
4655
  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);
4656
+ await discoverViewerPort();
4657
+ p.log.warn("iii 0.25 retired the bundled engine console; --console-port and console launch flags no longer apply.");
4658
+ p.note([
4659
+ `AgentMemory viewer: ${getViewerUrl()} (start agentmemory first if it is offline).`,
4660
+ "Use agentmemory status for runtime health and worker status.",
4661
+ "Engine console migration: https://github.com/iii-hq/iii/blob/iii/v0.25.0/docs/using-iii/console.mdx"
4662
+ ].join("\n"), "Console migration");
3908
4663
  }
3909
4664
  async function runCapture() {
3910
4665
  const code = await runCaptureCommand({
@@ -3914,6 +4669,23 @@ async function runCapture() {
3914
4669
  });
3915
4670
  process.exit(code);
3916
4671
  }
4672
+ async function runCompression() {
4673
+ const base = getBaseUrl();
4674
+ const code = await runCompressionCommand({
4675
+ base,
4676
+ args: args.slice(1),
4677
+ secret: resolveClientSecret(base)
4678
+ });
4679
+ process.exit(code);
4680
+ }
4681
+ async function runGraphIndex() {
4682
+ const base = getBaseUrl();
4683
+ process.exit(await runGraphIndexCommand({
4684
+ base,
4685
+ args: args.slice(1),
4686
+ secret: resolveClientSecret(base)
4687
+ }));
4688
+ }
3917
4689
  const commands = {
3918
4690
  init: runInit,
3919
4691
  connect: runConnectCmd,
@@ -3926,16 +4698,19 @@ const commands = {
3926
4698
  remove: runRemove,
3927
4699
  mcp: runMcp,
3928
4700
  "import-jsonl": runImportJsonl,
3929
- capture: runCapture
4701
+ capture: runCapture,
4702
+ compression: runCompression,
4703
+ "graph-index": runGraphIndex
3930
4704
  };
3931
4705
  const first = args[0] ?? "";
3932
4706
  async function unknownCommand() {
3933
4707
  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
4708
  process.exit(1);
3935
4709
  }
3936
- (commands[first] ?? (first && !first.startsWith("-") ? unknownCommand : main))().catch((err) => {
4710
+ (commands[first] ?? (first && !first.startsWith("-") ? unknownCommand : main))().catch(async (err) => {
3937
4711
  p.log.error(err instanceof Error ? err.message : String(err));
3938
- process.exit(1);
4712
+ const exitCode = await runStartupCleanup(1).catch(() => 1);
4713
+ process.exit(exitCode);
3939
4714
  });
3940
4715
  //#endregion
3941
4716
  export { discoverViewerPort };