@agentmemory/agentmemory 0.9.29 → 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 (352) hide show
  1. package/.env.example +102 -35
  2. package/AGENTS.md +19 -26
  3. package/README.md +417 -525
  4. package/dist/.env.example +102 -35
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +2316 -655
  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-Qaeq15TQ.mjs → connect-BSLIAMpw.mjs} +900 -302
  11. package/dist/connect-BSLIAMpw.mjs.map +1 -0
  12. package/dist/hooks/_capture.d.mts +25 -0
  13. package/dist/hooks/_capture.d.mts.map +1 -0
  14. package/dist/hooks/_capture.mjs +944 -0
  15. package/dist/hooks/_capture.mjs.map +1 -0
  16. package/dist/hooks/antigravity-bridge.d.mts +1 -1
  17. package/dist/hooks/antigravity-bridge.d.mts.map +1 -1
  18. package/dist/hooks/antigravity-bridge.mjs +24 -15
  19. package/dist/hooks/antigravity-bridge.mjs.map +1 -1
  20. package/dist/hooks/notification.mjs +35 -29
  21. package/dist/hooks/notification.mjs.map +1 -1
  22. package/dist/hooks/post-commit.mjs +91 -1
  23. package/dist/hooks/post-commit.mjs.map +1 -1
  24. package/dist/hooks/post-tool-failure.mjs +71 -29
  25. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  26. package/dist/hooks/post-tool-use.mjs +99 -39
  27. package/dist/hooks/post-tool-use.mjs.map +1 -1
  28. package/dist/hooks/pre-compact.mjs +113 -6
  29. package/dist/hooks/pre-compact.mjs.map +1 -1
  30. package/dist/hooks/pre-tool-use.mjs +91 -1
  31. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  32. package/dist/hooks/prompt-submit.mjs +25 -25
  33. package/dist/hooks/prompt-submit.mjs.map +1 -1
  34. package/dist/hooks/session-end.mjs +67 -31
  35. package/dist/hooks/session-end.mjs.map +1 -1
  36. package/dist/hooks/session-start.mjs +110 -6
  37. package/dist/hooks/session-start.mjs.map +1 -1
  38. package/dist/hooks/stop.mjs +80 -8
  39. package/dist/hooks/stop.mjs.map +1 -1
  40. package/dist/hooks/subagent-start.mjs +28 -29
  41. package/dist/hooks/subagent-start.mjs.map +1 -1
  42. package/dist/hooks/subagent-stop.mjs +33 -29
  43. package/dist/hooks/subagent-stop.mjs.map +1 -1
  44. package/dist/hooks/task-completed.mjs +31 -31
  45. package/dist/hooks/task-completed.mjs.map +1 -1
  46. package/dist/{image-refs-BKLmp_sD.mjs → image-refs-BaT-9oMU.mjs} +2 -2
  47. package/dist/image-refs-BaT-9oMU.mjs.map +1 -0
  48. package/dist/{image-refs-CJgZmb9X.mjs → image-refs-Cyta4Lf9.mjs} +3 -3
  49. package/dist/image-refs-Cyta4Lf9.mjs.map +1 -0
  50. package/dist/{keyed-mutex-DTHtcZrt.mjs → image-store-BsN4vyjr.mjs} +2 -14
  51. package/dist/image-store-BsN4vyjr.mjs.map +1 -0
  52. package/dist/index.mjs +15180 -4119
  53. package/dist/index.mjs.map +1 -1
  54. package/dist/integrations/dsh.d.mts +55 -0
  55. package/dist/integrations/dsh.d.mts.map +1 -0
  56. package/dist/integrations/dsh.mjs +1145 -0
  57. package/dist/integrations/dsh.mjs.map +1 -0
  58. package/dist/{schema-BFZhNTF7.mjs → keyed-mutex-DCUh7PG-.mjs} +81 -8
  59. package/dist/keyed-mutex-DCUh7PG-.mjs.map +1 -0
  60. package/dist/secret-store-D9GwO0xp.mjs +2449 -0
  61. package/dist/secret-store-D9GwO0xp.mjs.map +1 -0
  62. package/dist/session-arguments-zxsbbd8y.mjs +113 -0
  63. package/dist/session-arguments-zxsbbd8y.mjs.map +1 -0
  64. package/dist/{src-G7yt8gGm.mjs → src-DaMtDzt_.mjs} +8320 -4260
  65. package/dist/src-DaMtDzt_.mjs.map +1 -0
  66. package/dist/{standalone-BqNJDJ04.mjs → standalone-BlqshyZ5.mjs} +246 -62
  67. package/dist/standalone-BlqshyZ5.mjs.map +1 -0
  68. package/dist/standalone.d.mts +1 -1
  69. package/dist/standalone.d.mts.map +1 -1
  70. package/dist/standalone.mjs +866 -78
  71. package/dist/standalone.mjs.map +1 -1
  72. package/dist/viewer/index.html +7394 -2816
  73. package/integrations/hermes/README.md +84 -0
  74. package/integrations/hermes/__init__.py +669 -0
  75. package/integrations/hermes/plugin.yaml +27 -0
  76. package/integrations/omp/LICENSE +190 -0
  77. package/integrations/omp/README.md +76 -0
  78. package/integrations/omp/capture.ts +1 -0
  79. package/integrations/omp/dist/index.mjs +1450 -0
  80. package/integrations/omp/index.ts +488 -0
  81. package/integrations/omp/package.json +13 -0
  82. package/integrations/omp/security.ts +67 -0
  83. package/integrations/omp/skills/agentmemory-agents/REFERENCE.md +32 -0
  84. package/integrations/omp/skills/agentmemory-agents/SKILL.md +48 -0
  85. package/integrations/omp/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  86. package/integrations/omp/skills/agentmemory-architecture/SKILL.md +33 -0
  87. package/integrations/omp/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  88. package/integrations/omp/skills/agentmemory-config/REFERENCE.md +90 -0
  89. package/integrations/omp/skills/agentmemory-config/SKILL.md +78 -0
  90. package/integrations/omp/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  91. package/integrations/omp/skills/agentmemory-hooks/REFERENCE.md +20 -0
  92. package/integrations/omp/skills/agentmemory-hooks/SKILL.md +49 -0
  93. package/integrations/omp/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  94. package/integrations/omp/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
  95. package/integrations/omp/skills/agentmemory-mcp-tools/SKILL.md +42 -0
  96. package/integrations/omp/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  97. package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +158 -0
  98. package/integrations/omp/skills/agentmemory-rest-api/SKILL.md +49 -0
  99. package/integrations/omp/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  100. package/integrations/omp/skills/commit-context/EXAMPLES.md +71 -0
  101. package/integrations/omp/skills/commit-context/SKILL.md +64 -0
  102. package/integrations/omp/skills/commit-context/TROUBLESHOOTING.md +70 -0
  103. package/integrations/omp/skills/commit-history/EXAMPLES.md +71 -0
  104. package/integrations/omp/skills/commit-history/SKILL.md +62 -0
  105. package/integrations/omp/skills/commit-history/TROUBLESHOOTING.md +70 -0
  106. package/integrations/omp/skills/forget/EXAMPLES.md +40 -0
  107. package/integrations/omp/skills/forget/SKILL.md +73 -0
  108. package/integrations/omp/skills/forget/TROUBLESHOOTING.md +70 -0
  109. package/integrations/omp/skills/handoff/EXAMPLES.md +57 -0
  110. package/integrations/omp/skills/handoff/SKILL.md +75 -0
  111. package/integrations/omp/skills/handoff/TROUBLESHOOTING.md +70 -0
  112. package/integrations/omp/skills/lesson/SKILL.md +64 -0
  113. package/integrations/omp/skills/lesson/TROUBLESHOOTING.md +70 -0
  114. package/integrations/omp/skills/memory-discipline/SKILL.md +65 -0
  115. package/integrations/omp/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  116. package/integrations/omp/skills/recall/EXAMPLES.md +74 -0
  117. package/integrations/omp/skills/recall/SKILL.md +74 -0
  118. package/integrations/omp/skills/recall/TROUBLESHOOTING.md +70 -0
  119. package/integrations/omp/skills/recap/EXAMPLES.md +62 -0
  120. package/integrations/omp/skills/recap/SKILL.md +68 -0
  121. package/integrations/omp/skills/recap/TROUBLESHOOTING.md +70 -0
  122. package/integrations/omp/skills/remember/EXAMPLES.md +67 -0
  123. package/integrations/omp/skills/remember/SKILL.md +66 -0
  124. package/integrations/omp/skills/remember/TROUBLESHOOTING.md +70 -0
  125. package/integrations/omp/skills/session-history/EXAMPLES.md +59 -0
  126. package/integrations/omp/skills/session-history/SKILL.md +67 -0
  127. package/integrations/omp/skills/session-history/TROUBLESHOOTING.md +70 -0
  128. package/integrations/omp/skills/write-agentmemory-skill/SKILL.md +41 -0
  129. package/integrations/omp/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  130. package/integrations/openclaw/LICENSE +190 -0
  131. package/integrations/openclaw/README.md +88 -0
  132. package/integrations/openclaw/openclaw.plugin.json +71 -0
  133. package/integrations/openclaw/package.json +38 -0
  134. package/integrations/openclaw/plugin.mjs +425 -0
  135. package/integrations/openclaw/plugin.yaml +26 -0
  136. package/integrations/openclaw/skills/agentmemory-agents/REFERENCE.md +32 -0
  137. package/integrations/openclaw/skills/agentmemory-agents/SKILL.md +48 -0
  138. package/integrations/openclaw/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  139. package/integrations/openclaw/skills/agentmemory-architecture/SKILL.md +33 -0
  140. package/integrations/openclaw/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  141. package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +90 -0
  142. package/integrations/openclaw/skills/agentmemory-config/SKILL.md +78 -0
  143. package/integrations/openclaw/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  144. package/integrations/openclaw/skills/agentmemory-hooks/REFERENCE.md +20 -0
  145. package/integrations/openclaw/skills/agentmemory-hooks/SKILL.md +49 -0
  146. package/integrations/openclaw/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  147. package/integrations/openclaw/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
  148. package/integrations/openclaw/skills/agentmemory-mcp-tools/SKILL.md +42 -0
  149. package/integrations/openclaw/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  150. package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +158 -0
  151. package/integrations/openclaw/skills/agentmemory-rest-api/SKILL.md +49 -0
  152. package/integrations/openclaw/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  153. package/integrations/openclaw/skills/commit-context/EXAMPLES.md +71 -0
  154. package/integrations/openclaw/skills/commit-context/SKILL.md +64 -0
  155. package/integrations/openclaw/skills/commit-context/TROUBLESHOOTING.md +70 -0
  156. package/integrations/openclaw/skills/commit-history/EXAMPLES.md +71 -0
  157. package/integrations/openclaw/skills/commit-history/SKILL.md +62 -0
  158. package/integrations/openclaw/skills/commit-history/TROUBLESHOOTING.md +70 -0
  159. package/integrations/openclaw/skills/forget/EXAMPLES.md +40 -0
  160. package/integrations/openclaw/skills/forget/SKILL.md +73 -0
  161. package/integrations/openclaw/skills/forget/TROUBLESHOOTING.md +70 -0
  162. package/integrations/openclaw/skills/handoff/EXAMPLES.md +57 -0
  163. package/integrations/openclaw/skills/handoff/SKILL.md +75 -0
  164. package/integrations/openclaw/skills/handoff/TROUBLESHOOTING.md +70 -0
  165. package/integrations/openclaw/skills/lesson/SKILL.md +64 -0
  166. package/integrations/openclaw/skills/lesson/TROUBLESHOOTING.md +70 -0
  167. package/integrations/openclaw/skills/memory-discipline/SKILL.md +65 -0
  168. package/integrations/openclaw/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  169. package/integrations/openclaw/skills/recall/EXAMPLES.md +74 -0
  170. package/integrations/openclaw/skills/recall/SKILL.md +74 -0
  171. package/integrations/openclaw/skills/recall/TROUBLESHOOTING.md +70 -0
  172. package/integrations/openclaw/skills/recap/EXAMPLES.md +62 -0
  173. package/integrations/openclaw/skills/recap/SKILL.md +68 -0
  174. package/integrations/openclaw/skills/recap/TROUBLESHOOTING.md +70 -0
  175. package/integrations/openclaw/skills/remember/EXAMPLES.md +67 -0
  176. package/integrations/openclaw/skills/remember/SKILL.md +66 -0
  177. package/integrations/openclaw/skills/remember/TROUBLESHOOTING.md +70 -0
  178. package/integrations/openclaw/skills/session-history/EXAMPLES.md +59 -0
  179. package/integrations/openclaw/skills/session-history/SKILL.md +67 -0
  180. package/integrations/openclaw/skills/session-history/TROUBLESHOOTING.md +70 -0
  181. package/integrations/openclaw/skills/write-agentmemory-skill/SKILL.md +41 -0
  182. package/integrations/openclaw/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  183. package/integrations/pi/LICENSE +190 -0
  184. package/integrations/pi/README.md +45 -51
  185. package/integrations/pi/dist/index.mjs +1379 -0
  186. package/integrations/pi/index.ts +139 -173
  187. package/integrations/pi/package.json +16 -6
  188. package/integrations/pi/security.ts +54 -22
  189. package/integrations/pi/skills/agentmemory-agents/REFERENCE.md +32 -0
  190. package/integrations/pi/skills/agentmemory-agents/SKILL.md +48 -0
  191. package/integrations/pi/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  192. package/integrations/pi/skills/agentmemory-architecture/SKILL.md +33 -0
  193. package/integrations/pi/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  194. package/integrations/pi/skills/agentmemory-config/REFERENCE.md +90 -0
  195. package/integrations/pi/skills/agentmemory-config/SKILL.md +78 -0
  196. package/integrations/pi/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  197. package/integrations/pi/skills/agentmemory-hooks/REFERENCE.md +20 -0
  198. package/integrations/pi/skills/agentmemory-hooks/SKILL.md +49 -0
  199. package/integrations/pi/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  200. package/integrations/pi/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
  201. package/integrations/pi/skills/agentmemory-mcp-tools/SKILL.md +42 -0
  202. package/integrations/pi/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  203. package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +158 -0
  204. package/integrations/pi/skills/agentmemory-rest-api/SKILL.md +49 -0
  205. package/integrations/pi/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  206. package/integrations/pi/skills/commit-context/EXAMPLES.md +71 -0
  207. package/integrations/pi/skills/commit-context/SKILL.md +64 -0
  208. package/integrations/pi/skills/commit-context/TROUBLESHOOTING.md +70 -0
  209. package/integrations/pi/skills/commit-history/EXAMPLES.md +71 -0
  210. package/integrations/pi/skills/commit-history/SKILL.md +62 -0
  211. package/integrations/pi/skills/commit-history/TROUBLESHOOTING.md +70 -0
  212. package/integrations/pi/skills/forget/EXAMPLES.md +40 -0
  213. package/integrations/pi/skills/forget/SKILL.md +73 -0
  214. package/integrations/pi/skills/forget/TROUBLESHOOTING.md +70 -0
  215. package/integrations/pi/skills/handoff/EXAMPLES.md +57 -0
  216. package/integrations/pi/skills/handoff/SKILL.md +75 -0
  217. package/integrations/pi/skills/handoff/TROUBLESHOOTING.md +70 -0
  218. package/integrations/pi/skills/lesson/SKILL.md +64 -0
  219. package/integrations/pi/skills/lesson/TROUBLESHOOTING.md +70 -0
  220. package/integrations/pi/skills/memory-discipline/SKILL.md +65 -0
  221. package/integrations/pi/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  222. package/integrations/pi/skills/recall/EXAMPLES.md +74 -0
  223. package/integrations/pi/skills/recall/SKILL.md +74 -0
  224. package/integrations/pi/skills/recall/TROUBLESHOOTING.md +70 -0
  225. package/integrations/pi/skills/recap/EXAMPLES.md +62 -0
  226. package/integrations/pi/skills/recap/SKILL.md +68 -0
  227. package/integrations/pi/skills/recap/TROUBLESHOOTING.md +70 -0
  228. package/integrations/pi/skills/remember/EXAMPLES.md +67 -0
  229. package/integrations/pi/skills/remember/SKILL.md +66 -0
  230. package/integrations/pi/skills/remember/TROUBLESHOOTING.md +70 -0
  231. package/integrations/pi/skills/session-history/EXAMPLES.md +59 -0
  232. package/integrations/pi/skills/session-history/SKILL.md +67 -0
  233. package/integrations/pi/skills/session-history/TROUBLESHOOTING.md +70 -0
  234. package/integrations/pi/skills/write-agentmemory-skill/SKILL.md +41 -0
  235. package/integrations/pi/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  236. package/package.json +42 -12
  237. package/plugin/.claude-plugin/plugin.json +2 -2
  238. package/plugin/.codex-plugin/plugin.json +27 -4
  239. package/plugin/.devin-plugin/plugin.json +5 -3
  240. package/plugin/.mcp.codex.json +12 -0
  241. package/plugin/.mcp.copilot.json +2 -7
  242. package/plugin/.mcp.json +2 -7
  243. package/plugin/assets/icon.svg +35 -0
  244. package/plugin/cursor/hooks.json +23 -8
  245. package/plugin/hooks/hooks.copilot.json +11 -11
  246. package/plugin/hooks.json +67 -0
  247. package/plugin/opencode/README.md +267 -31
  248. package/plugin/opencode/agentmemory-capture.ts +594 -11
  249. package/plugin/opencode/plugin.json +1 -1
  250. package/plugin/plugin.json +1 -1
  251. package/plugin/scripts/_capture.d.mts +25 -0
  252. package/plugin/scripts/_capture.d.mts.map +1 -0
  253. package/plugin/scripts/_capture.mjs +944 -0
  254. package/plugin/scripts/_capture.mjs.map +1 -0
  255. package/plugin/scripts/antigravity-bridge.d.mts +1 -1
  256. package/plugin/scripts/antigravity-bridge.d.mts.map +1 -1
  257. package/plugin/scripts/antigravity-bridge.mjs +24 -15
  258. package/plugin/scripts/antigravity-bridge.mjs.map +1 -1
  259. package/plugin/scripts/cursor/run-detached.d.mts +1 -0
  260. package/plugin/scripts/cursor/run-detached.mjs +786 -0
  261. package/plugin/scripts/cursor/run-detached.mjs.map +1 -0
  262. package/plugin/scripts/cursor/run-hook.d.mts +1 -0
  263. package/plugin/scripts/cursor/run-hook.mjs +704 -0
  264. package/plugin/scripts/cursor/run-hook.mjs.map +1 -0
  265. package/plugin/scripts/notification.mjs +35 -29
  266. package/plugin/scripts/notification.mjs.map +1 -1
  267. package/plugin/scripts/plugin-bridge.mjs +409 -0
  268. package/plugin/scripts/post-commit.mjs +91 -1
  269. package/plugin/scripts/post-commit.mjs.map +1 -1
  270. package/plugin/scripts/post-tool-failure.mjs +71 -29
  271. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  272. package/plugin/scripts/post-tool-use.mjs +99 -39
  273. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  274. package/plugin/scripts/pre-compact.mjs +113 -6
  275. package/plugin/scripts/pre-compact.mjs.map +1 -1
  276. package/plugin/scripts/pre-tool-use.mjs +91 -1
  277. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  278. package/plugin/scripts/prompt-submit.mjs +25 -25
  279. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  280. package/plugin/scripts/session-end.mjs +67 -31
  281. package/plugin/scripts/session-end.mjs.map +1 -1
  282. package/plugin/scripts/session-start.mjs +110 -6
  283. package/plugin/scripts/session-start.mjs.map +1 -1
  284. package/plugin/scripts/stop.mjs +80 -8
  285. package/plugin/scripts/stop.mjs.map +1 -1
  286. package/plugin/scripts/subagent-start.mjs +28 -29
  287. package/plugin/scripts/subagent-start.mjs.map +1 -1
  288. package/plugin/scripts/subagent-stop.mjs +33 -29
  289. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  290. package/plugin/scripts/task-completed.mjs +31 -31
  291. package/plugin/scripts/task-completed.mjs.map +1 -1
  292. package/plugin/skills/_shared/TROUBLESHOOTING.md +42 -12
  293. package/plugin/skills/agentmemory-agents/REFERENCE.md +10 -9
  294. package/plugin/skills/agentmemory-agents/SKILL.md +19 -5
  295. package/plugin/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
  296. package/plugin/skills/agentmemory-architecture/SKILL.md +4 -4
  297. package/plugin/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
  298. package/plugin/skills/agentmemory-config/REFERENCE.md +48 -3
  299. package/plugin/skills/agentmemory-config/SKILL.md +51 -10
  300. package/plugin/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
  301. package/plugin/skills/agentmemory-hooks/SKILL.md +15 -5
  302. package/plugin/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
  303. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +5 -5
  304. package/plugin/skills/agentmemory-mcp-tools/SKILL.md +6 -3
  305. package/plugin/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
  306. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +18 -2
  307. package/plugin/skills/agentmemory-rest-api/SKILL.md +12 -6
  308. package/plugin/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
  309. package/plugin/skills/commit-context/SKILL.md +1 -1
  310. package/plugin/skills/commit-context/TROUBLESHOOTING.md +70 -0
  311. package/plugin/skills/commit-history/SKILL.md +1 -1
  312. package/plugin/skills/commit-history/TROUBLESHOOTING.md +70 -0
  313. package/plugin/skills/forget/EXAMPLES.md +19 -54
  314. package/plugin/skills/forget/SKILL.md +16 -12
  315. package/plugin/skills/forget/TROUBLESHOOTING.md +70 -0
  316. package/plugin/skills/handoff/EXAMPLES.md +5 -4
  317. package/plugin/skills/handoff/SKILL.md +13 -6
  318. package/plugin/skills/handoff/TROUBLESHOOTING.md +70 -0
  319. package/plugin/skills/lesson/SKILL.md +1 -1
  320. package/plugin/skills/lesson/TROUBLESHOOTING.md +70 -0
  321. package/plugin/skills/memory-discipline/SKILL.md +14 -6
  322. package/plugin/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
  323. package/plugin/skills/recall/SKILL.md +18 -7
  324. package/plugin/skills/recall/TROUBLESHOOTING.md +70 -0
  325. package/plugin/skills/recap/EXAMPLES.md +5 -3
  326. package/plugin/skills/recap/SKILL.md +11 -6
  327. package/plugin/skills/recap/TROUBLESHOOTING.md +70 -0
  328. package/plugin/skills/remember/EXAMPLES.md +3 -3
  329. package/plugin/skills/remember/SKILL.md +1 -1
  330. package/plugin/skills/remember/TROUBLESHOOTING.md +70 -0
  331. package/plugin/skills/session-history/EXAMPLES.md +2 -2
  332. package/plugin/skills/session-history/SKILL.md +10 -4
  333. package/plugin/skills/session-history/TROUBLESHOOTING.md +70 -0
  334. package/plugin/skills/write-agentmemory-skill/SKILL.md +2 -1
  335. package/plugin/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
  336. package/dist/connect-Qaeq15TQ.mjs.map +0 -1
  337. package/dist/docker-compose.yml +0 -47
  338. package/dist/iii-config.docker.yaml +0 -53
  339. package/dist/iii-config.yaml +0 -61
  340. package/dist/image-refs-BKLmp_sD.mjs.map +0 -1
  341. package/dist/image-refs-CJgZmb9X.mjs.map +0 -1
  342. package/dist/keyed-mutex-DTHtcZrt.mjs.map +0 -1
  343. package/dist/logger-BEB7pCI9.mjs +0 -43
  344. package/dist/logger-BEB7pCI9.mjs.map +0 -1
  345. package/dist/schema-BFZhNTF7.mjs.map +0 -1
  346. package/dist/src-G7yt8gGm.mjs.map +0 -1
  347. package/dist/standalone-BqNJDJ04.mjs.map +0 -1
  348. package/dist/tools-registry-BV_2bu7z.mjs +0 -1405
  349. package/dist/tools-registry-BV_2bu7z.mjs.map +0 -1
  350. package/docker-compose.yml +0 -47
  351. package/iii-config.docker.yaml +0 -53
  352. package/iii-config.yaml +0 -61
package/dist/cli.mjs CHANGED
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { a as runAdapter, i as resolveAdapter, r as knownAgents, t as ADAPTERS } from "./connect-Qaeq15TQ.mjs";
3
- import { i as generateId } from "./schema-BFZhNTF7.mjs";
4
- import { r as setBootVerbose } from "./logger-BEB7pCI9.mjs";
5
- import { i as VERSION, n as getAllTools, p as hydrateProcessEnvFromFile, t as ESSENTIAL_TOOLS } from "./tools-registry-BV_2bu7z.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, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
8
- import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } 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
+ import { createHash } from "node:crypto";
13
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";
14
18
  //#region src/cli/doctor-diagnostics.ts
15
19
  /** Common placeholder values shipped in .env.example. */
16
20
  const PLACEHOLDER_VALUES = new Set([
@@ -30,20 +34,7 @@ const PROVIDER_KEY_NAMES = [
30
34
  "OPENROUTER_API_KEY",
31
35
  "MINIMAX_API_KEY"
32
36
  ];
33
- function parseEnvFile(content) {
34
- const out = {};
35
- for (const rawLine of content.split(/\r?\n/)) {
36
- const line = rawLine.trim();
37
- if (!line || line.startsWith("#")) continue;
38
- const eq = line.indexOf("=");
39
- if (eq < 0) continue;
40
- const key = line.slice(0, eq).trim();
41
- let value = line.slice(eq + 1).trim();
42
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
43
- out[key] = value;
44
- }
45
- return out;
46
- }
37
+ const parseEnvFile = parseEnvContent;
47
38
  /** Returns the list of provider keys that look real (non-placeholder). */
48
39
  function realProviderKeys(env) {
49
40
  return PROVIDER_KEY_NAMES.filter((k) => {
@@ -70,7 +61,7 @@ function buildDiagnostics(effects) {
70
61
  id: "env-missing",
71
62
  message: "~/.agentmemory/.env is missing.",
72
63
  fixPreview: "Copy .env.example into ~/.agentmemory/.env (your keys file).",
73
- 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.",
74
65
  check: async () => ({
75
66
  ok: effects.envFileExists(),
76
67
  detail: effects.envFileExists() ? void 0 : "no env file"
@@ -79,18 +70,32 @@ function buildDiagnostics(effects) {
79
70
  },
80
71
  {
81
72
  id: "no-llm-provider-key",
82
- message: "No LLM provider API key found in ~/.agentmemory/.env.",
73
+ message: "No LLM provider API key found in ~/.agentmemory/.env or the environment.",
83
74
  fixPreview: "Open ~/.agentmemory/.env in $EDITOR and paste your key, then re-check.",
84
- 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.",
85
76
  check: async () => {
86
- 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 {
87
82
  ok: false,
88
- 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"
89
94
  };
90
- const real = realProviderKeys(effects.readEnvFile());
95
+ const caveat = fromEnv.length > 0 ? "; a daemon started elsewhere will not see it" : "";
91
96
  return {
92
- ok: real.length > 0,
93
- detail: real.length > 0 ? `found: ${real.join(", ")}` : "no provider key set"
97
+ ok: true,
98
+ detail: `found: ${named.join(", ")}${caveat}`
94
99
  };
95
100
  },
96
101
  fix: (ctx) => effects.openEditor(ctx.envPath)
@@ -98,8 +103,8 @@ function buildDiagnostics(effects) {
98
103
  {
99
104
  id: "engine-version-mismatch",
100
105
  message: "iii binary on PATH doesn't match the version agentmemory pins to.",
101
- fixPreview: "Re-run the iii installer for the pinned version and restart the engine.",
102
- 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.",
103
108
  check: async (ctx) => {
104
109
  const bin = effects.findIiiBinary();
105
110
  if (!bin) return {
@@ -141,8 +146,8 @@ function buildDiagnostics(effects) {
141
146
  {
142
147
  id: "stale-pidfile",
143
148
  message: "Stale pidfile: pid recorded but the process is gone.",
144
- fixPreview: "Clear ~/.agentmemory/iii.pid + engine-state.json, then restart.",
145
- 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.",
146
151
  check: async () => {
147
152
  if (!effects.pidfileExists()) return {
148
153
  ok: true,
@@ -225,11 +230,14 @@ function dryRunPlan(ctx, results) {
225
230
  }
226
231
  //#endregion
227
232
  //#region src/cli/remove-plan.ts
228
- function pidfilePath(home) {
229
- return join(home, ".agentmemory", "iii.pid");
233
+ function pidfilePath(runtimeDir) {
234
+ return join(runtimeDir, "iii.pid");
235
+ }
236
+ function workerPidfilePath$1(runtimeDir) {
237
+ return join(runtimeDir, "worker.pid");
230
238
  }
231
- function enginePath(home) {
232
- return join(home, ".agentmemory", "engine-state.json");
239
+ function enginePath(runtimeDir) {
240
+ return join(runtimeDir, "engine-state.json");
233
241
  }
234
242
  function envPath(home) {
235
243
  return join(home, ".agentmemory", ".env");
@@ -240,9 +248,6 @@ function preferencesPath(home) {
240
248
  function backupsDir(home) {
241
249
  return join(home, ".agentmemory", "backups");
242
250
  }
243
- function dataDir(home) {
244
- return join(home, ".agentmemory", "data");
245
- }
246
251
  function iiiBinFile() {
247
252
  return process.platform === "win32" ? "iii.exe" : "iii";
248
253
  }
@@ -274,31 +279,39 @@ function pathExists(path) {
274
279
  * the structure stable for tests.
275
280
  */
276
281
  function buildRemovePlan(ctx, options) {
277
- const { home, pinnedVersion, localBinIiiVersion, connectManifest } = ctx;
282
+ const { home, runtimeDir, dataDir, pinnedVersion, localBinIiiVersion, connectManifest } = ctx;
278
283
  const plan = [];
279
284
  plan.push({
280
285
  id: "stop-engine",
281
286
  description: "Stop running iii-engine (if any) cleanly",
282
287
  path: null,
283
288
  alwaysAsk: false,
284
- applicable: pathExists(pidfilePath(home)) || pathExists(enginePath(home)),
289
+ applicable: pathExists(pidfilePath(runtimeDir)) || pathExists(workerPidfilePath$1(runtimeDir)) || pathExists(enginePath(runtimeDir)),
285
290
  sizeBytes: -1
286
291
  });
287
292
  plan.push({
288
293
  id: "pidfile",
289
294
  description: "Delete pidfile",
290
- path: pidfilePath(home),
295
+ path: pidfilePath(runtimeDir),
291
296
  alwaysAsk: false,
292
- applicable: pathExists(pidfilePath(home)),
293
- sizeBytes: safeSize(pidfilePath(home))
297
+ applicable: pathExists(pidfilePath(runtimeDir)),
298
+ sizeBytes: safeSize(pidfilePath(runtimeDir))
299
+ });
300
+ plan.push({
301
+ id: "worker-pidfile",
302
+ description: "Delete worker pidfile",
303
+ path: workerPidfilePath$1(runtimeDir),
304
+ alwaysAsk: false,
305
+ applicable: pathExists(workerPidfilePath$1(runtimeDir)),
306
+ sizeBytes: safeSize(workerPidfilePath$1(runtimeDir))
294
307
  });
295
308
  plan.push({
296
309
  id: "engine-state",
297
310
  description: "Delete engine-state.json",
298
- path: enginePath(home),
311
+ path: enginePath(runtimeDir),
299
312
  alwaysAsk: false,
300
- applicable: pathExists(enginePath(home)),
301
- sizeBytes: safeSize(enginePath(home))
313
+ applicable: !options.preserveRuntimeState && pathExists(enginePath(runtimeDir)),
314
+ sizeBytes: safeSize(enginePath(runtimeDir))
302
315
  });
303
316
  plan.push({
304
317
  id: "env",
@@ -324,6 +337,14 @@ function buildRemovePlan(ctx, options) {
324
337
  applicable: !options.keepData && pathExists(backupsDir(home)),
325
338
  sizeBytes: -1
326
339
  });
340
+ plan.push({
341
+ id: "runtime-config",
342
+ description: "Delete generated iii-config.runtime.yaml",
343
+ path: runtimeConfigPath(dataDir),
344
+ alwaysAsk: false,
345
+ applicable: pathExists(runtimeConfigPath(dataDir)),
346
+ sizeBytes: safeSize(runtimeConfigPath(dataDir))
347
+ });
327
348
  if (connectManifest?.installed?.length) for (const entry of connectManifest.installed) plan.push({
328
349
  id: `connect:${entry.target}`,
329
350
  description: `Remove agent connection (${entry.agent ?? "unknown"})`,
@@ -355,10 +376,10 @@ function buildRemovePlan(ctx, options) {
355
376
  }
356
377
  plan.push({
357
378
  id: "data-dir",
358
- description: "Delete memory data directory (~/.agentmemory/data/) — will ask separately",
359
- path: dataDir(home),
379
+ description: `Delete memory data directory (${dataDir}) — will ask separately`,
380
+ path: dataDir,
360
381
  alwaysAsk: true,
361
- applicable: !options.keepData && pathExists(dataDir(home)),
382
+ applicable: !options.keepData && pathExists(dataDir),
362
383
  sizeBytes: -1
363
384
  });
364
385
  return plan;
@@ -377,6 +398,343 @@ function humanBytes(n) {
377
398
  return `${(n / (1024 * 1024)).toFixed(1)} MB`;
378
399
  }
379
400
  //#endregion
401
+ //#region src/cli/ready-hint.ts
402
+ const DEFAULT_REST_PORT = 3111;
403
+ function portFlagSuffix(restPort, instance) {
404
+ if (instance > 0 && restPort === DEFAULT_REST_PORT + instance * 100) return ` --instance ${instance}`;
405
+ return restPort === DEFAULT_REST_PORT ? "" : ` --port ${restPort}`;
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
+ }
414
+ const III_INSTALL_SPAWN_TIMEOUT_MS = 345 * 1e3;
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"
423
+ } };
424
+ function expectedIiiSha256(version, asset, checksums = III_RELEASE_SHA256) {
425
+ return checksums[version]?.[asset] ?? null;
426
+ }
427
+ function sha256File(path) {
428
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
429
+ }
430
+ function curlTimeoutFlags() {
431
+ return `--connect-timeout 15 --max-time 300`;
432
+ }
433
+ function iiiArchivePath(binDir) {
434
+ return join(binDir, ".iii-download.tar.gz");
435
+ }
436
+ function iiiDownloadShellCommand() {
437
+ return `mkdir -p "$2" && rm -f "$3" && if curl -fsSL ${curlTimeoutFlags()} -o "$3" "$1"; then :; else code=$?; rm -f "$3"; exit $code; fi`;
438
+ }
439
+ function iiiExtractShellCommand() {
440
+ return "if tar -xzf \"$1\" -C \"$2\"; then rm -f \"$1\"; else code=$?; rm -f \"$1\"; exit $code; fi && chmod +x \"$3\"";
441
+ }
442
+ function iiiManualInstallCommand(releaseUrl, binDir, binPath, sha256) {
443
+ return `mkdir -p "${binDir}" && archive="$(mktemp "${binDir}/.iii-download.XXXXXX")" && { curl -fL --connect-timeout 15 -o "$archive" "${releaseUrl}"${sha256 ? ` && printf '%s %s\\n' "${sha256}" "$archive" | { sha256sum -c - 2>/dev/null || shasum -a 256 -c -; }` : ""} && tar -xzf "$archive" -C "${binDir}"; code=$?; rm -f "$archive"; (exit $code); } && chmod +x "${binPath}"`;
444
+ }
445
+ function describeInstallFailure(result) {
446
+ if (result.error?.code === "ETIMEDOUT") return `timed out after ${Math.round(III_INSTALL_SPAWN_TIMEOUT_MS / 1e3)} s`;
447
+ const output = (result.stderr || result.stdout || "").toString().trim();
448
+ if (output) return output;
449
+ if (result.status === 28) return `download timed out (no connection within 15 s or not finished within 300 s)`;
450
+ if (result.signal) return `installer stopped by ${result.signal}`;
451
+ if (result.error) return result.error.message;
452
+ return `installer exited with status ${result.status ?? "unknown"}`;
453
+ }
454
+ function installIiiArchive(opts) {
455
+ const expected = expectedIiiSha256(opts.version, opts.asset, opts.checksums);
456
+ if (!expected) return {
457
+ ok: false,
458
+ reason: "no-checksum",
459
+ detail: `no pinned SHA-256 for ${opts.asset} in iii v${opts.version}, so the download cannot be verified`
460
+ };
461
+ const run = (script, args) => spawnSync(opts.sh, [
462
+ "-c",
463
+ script,
464
+ "sh",
465
+ ...args
466
+ ], {
467
+ stdio: "pipe",
468
+ encoding: "utf-8",
469
+ timeout: III_INSTALL_SPAWN_TIMEOUT_MS,
470
+ env: opts.env ?? process.env
471
+ });
472
+ const archive = iiiArchivePath(opts.binDir);
473
+ const download = run(iiiDownloadShellCommand(), [
474
+ opts.releaseUrl,
475
+ opts.binDir,
476
+ archive
477
+ ]);
478
+ if (download.status !== 0) return {
479
+ ok: false,
480
+ reason: "download",
481
+ detail: describeInstallFailure(download)
482
+ };
483
+ let actual;
484
+ try {
485
+ actual = sha256File(archive);
486
+ } catch (err) {
487
+ rmSync(archive, { force: true });
488
+ return {
489
+ ok: false,
490
+ reason: "download",
491
+ detail: err instanceof Error ? err.message : String(err)
492
+ };
493
+ }
494
+ if (actual !== expected) {
495
+ rmSync(archive, { force: true });
496
+ return {
497
+ ok: false,
498
+ reason: "checksum",
499
+ detail: `SHA-256 mismatch for ${opts.asset}: expected ${expected}, got ${actual}. The download was deleted and nothing was installed`
500
+ };
501
+ }
502
+ const extract = run(iiiExtractShellCommand(), [
503
+ archive,
504
+ opts.binDir,
505
+ opts.binPath
506
+ ]);
507
+ if (extract.status !== 0) return {
508
+ ok: false,
509
+ reason: "extract",
510
+ detail: describeInstallFailure(extract)
511
+ };
512
+ return { ok: true };
513
+ }
514
+ //#endregion
515
+ //#region src/cli/startup-stderr.ts
516
+ function createStartupStderrCapture(maxBytes = 16 * 1024) {
517
+ const chunks = [];
518
+ let capturedBytes = 0;
519
+ return {
520
+ append(chunk) {
521
+ if (capturedBytes >= maxBytes) return;
522
+ const slice = chunk.subarray(0, maxBytes - capturedBytes);
523
+ chunks.push(slice);
524
+ capturedBytes += slice.length;
525
+ },
526
+ text() {
527
+ return Buffer.concat(chunks).toString("utf8");
528
+ }
529
+ };
530
+ }
531
+ //#endregion
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
+ };
551
+ }
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;
562
+ }
563
+ if (/^\s*-\s+name:\s*(?:iii-exec|"iii-exec"|'iii-exec')(?:\s+#.*)?\s*$/.test(line)) {
564
+ workerIndent = indent;
565
+ continue;
566
+ }
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;
571
+ continue;
572
+ }
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
+ }
592
+ }
593
+ return false;
594
+ }
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 });
673
+ }
674
+ //#endregion
675
+ //#region src/cli/process-state.ts
676
+ function processStatIsRunning(stat) {
677
+ return !stat.trimStart().startsWith("Z");
678
+ }
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
380
738
  //#region src/cli/splash.ts
381
739
  const IS_COLOR_TTY = !!process.stdout.isTTY && !process.env["NO_COLOR"];
382
740
  function accent(s) {
@@ -436,7 +794,6 @@ const DEFAULTS = {
436
794
  skipSplash: false,
437
795
  skipNpxHint: false,
438
796
  skipGlobalInstall: false,
439
- skipConsoleInstall: false,
440
797
  firstRunAt: null,
441
798
  injectContextChosen: false
442
799
  };
@@ -765,86 +1122,811 @@ async function wireSelectedAgents(agents) {
765
1122
  p.note(summary.join("\n"), "wire summary");
766
1123
  }
767
1124
  //#endregion
768
- //#region src/cli-data-dir.ts
769
- function argValue(args, name) {
770
- const equals = `${name}=`;
771
- const inline = args.find((arg) => arg.startsWith(equals));
772
- if (inline) return inline.slice(equals.length);
773
- const idx = args.indexOf(name);
774
- if (idx !== -1) return args[idx + 1];
775
- }
776
- function expandHome(pathValue, home) {
777
- if (pathValue === "~") return home;
778
- if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) return join(home, pathValue.slice(2));
779
- return pathValue;
780
- }
781
- function absoluteDataDir(pathValue, cwd, home) {
782
- const expanded = expandHome(pathValue, home);
783
- return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
784
- }
785
- function platformDefaultDataDir(env, home, nodePlatform, useXdg = true) {
786
- if (nodePlatform === "darwin") return join(home, "Library", "Application Support", "agentmemory");
787
- if (nodePlatform === "win32") {
788
- const appData = env["APPDATA"];
789
- return appData ? join(appData, "agentmemory") : join(home, ".agentmemory");
790
- }
791
- const xdgDataHome = env["XDG_DATA_HOME"];
792
- if (useXdg && xdgDataHome && isAbsolute(xdgDataHome)) return join(xdgDataHome, "agentmemory");
793
- return join(home, ".local", "share", "agentmemory");
794
- }
795
- function nearestGitParent(pathValue) {
796
- let current = pathValue;
797
- while (true) {
798
- if (existsSync(join(current, ".git"))) return current;
799
- const parent = dirname(current);
800
- if (parent === current) return void 0;
801
- current = parent;
802
- }
803
- }
804
- function isWithin(parent, child) {
805
- const pathFromParent = relative(parent, child);
806
- return pathFromParent === "" || pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent);
807
- }
808
- function resolveDataDir(options = {}) {
809
- const args = options.args ?? process.argv.slice(2);
810
- const env = options.env ?? process.env;
811
- const cwd = options.cwd ?? process.cwd();
812
- const home = options.home ?? homedir();
813
- const nodePlatform = options.platform ?? platform();
814
- const parsedInstance = parseInt(argValue(args, "--instance") ?? "0", 10);
815
- const instance = parsedInstance > 0 && parsedInstance <= 50 ? parsedInstance : 0;
816
- const forInstance = (dataDir) => instance ? join(dataDir, `instance-${instance}`) : dataDir;
817
- const flagValue = argValue(args, "--data-dir");
818
- if (flagValue) return {
819
- dataDir: forInstance(absoluteDataDir(flagValue, cwd, home)),
820
- source: "flag"
1125
+ //#region src/cli/capture.ts
1126
+ function headers(secret) {
1127
+ const h = { "Content-Type": "application/json" };
1128
+ if (secret) h["Authorization"] = `Bearer ${secret}`;
1129
+ return h;
1130
+ }
1131
+ function classify(status) {
1132
+ if (status === 200) return "duplicate";
1133
+ if (status >= 200 && status < 300) return "delivered";
1134
+ if (status === 408 || status === 429 || status >= 500) return "retry";
1135
+ return "rejected";
1136
+ }
1137
+ async function serverCapture(base, secret) {
1138
+ try {
1139
+ const res = await fetch(`${base}/agentmemory/capture?limit=20`, {
1140
+ headers: headers(secret),
1141
+ signal: AbortSignal.timeout(5e3)
1142
+ });
1143
+ if (!res.ok) return { error: `HTTP ${res.status}` };
1144
+ return await res.json();
1145
+ } catch (err) {
1146
+ return { error: err instanceof Error ? err.message : String(err) };
1147
+ }
1148
+ }
1149
+ async function runCaptureCommand(options) {
1150
+ const log = options.log ?? ((line) => console.log(line));
1151
+ const json = options.args.includes("--json");
1152
+ const drain = options.args.includes("--drain");
1153
+ const output = {};
1154
+ if (drain) output["drain"] = await drainSpool(options.base, async (record) => {
1155
+ const res = await fetch(`${options.base}/agentmemory/observe`, {
1156
+ method: "POST",
1157
+ headers: headers(options.secret),
1158
+ body: JSON.stringify({
1159
+ ...record.body,
1160
+ eventId: record.eventId
1161
+ }),
1162
+ signal: AbortSignal.timeout(1e4)
1163
+ });
1164
+ const text = await res.text().catch(() => "");
1165
+ const outcome = classify(res.status);
1166
+ if (outcome === "delivered" || outcome === "duplicate") {
1167
+ let mark = null;
1168
+ try {
1169
+ mark = parseSentMark(JSON.parse(text));
1170
+ } catch {}
1171
+ if (mark) retainSent(options.base, record.eventId, record.body, mark);
1172
+ }
1173
+ return outcome;
1174
+ });
1175
+ output["spool"] = spoolSummary(options.base);
1176
+ output["server"] = await serverCapture(options.base, options.secret);
1177
+ if (json) {
1178
+ log(JSON.stringify(output, null, 2));
1179
+ return 0;
1180
+ }
1181
+ const spool = output["spool"];
1182
+ if (drain) {
1183
+ const d = output["drain"];
1184
+ log(d.skipped ? `Drain skipped: ${d.skipped === "locked" ? "another drain is running" : "nothing to send"}` : `Drained ${d.claimed}: ${d.delivered} stored, ${d.duplicates} already stored, ${d.rejected} rejected, ${d.expired} expired, ${d.remaining} still waiting${d.error ? ` (${d.error})` : ""}`);
1185
+ }
1186
+ log(`Local spool: ${spool.enabled ? `${spool.records} waiting, ${spool.bytes} bytes` : "off (AGENTMEMORY_CAPTURE_SPOOL=false)"}`);
1187
+ log(` file: ${spool.path}`);
1188
+ log(` limits: ${spool.maxBytes} bytes, ${spool.maxAgeHours} h`);
1189
+ log(` totals: ${spool.stats.spooled} spooled, ${spool.stats.delivered} delivered, ${spool.stats.duplicates} duplicates, ${spool.stats.dropped} dropped, ${spool.stats.expired} expired`);
1190
+ if (spool.stats.lastDrainAt) log(` last drain: ${spool.stats.lastDrainAt}`);
1191
+ if (spool.stats.lastDropAt) log(` last drop: ${spool.stats.lastDropAt} (${spool.stats.lastDropReason ?? "unknown"})`);
1192
+ const server = output["server"];
1193
+ if (server.error) {
1194
+ log(`Server: unreachable at ${options.base} (${server.error})`);
1195
+ return 0;
1196
+ }
1197
+ const inbox = server.capture?.inbox;
1198
+ log(`Server inbox: ${inbox ? `${inbox.pending} pending, ${inbox.retrying} retrying, ${inbox.dead} dead letters` : "unknown"}`);
1199
+ if (inbox?.lastError) log(` last error: ${inbox.lastError}`);
1200
+ for (const item of server.items ?? []) log(` ${item.status.padEnd(8)} ${item.eventId} attempts=${item.attempts} ${item.preview ?? ""}`);
1201
+ if (inbox && inbox.dead > 0) log(`Retry dead letters: curl -X POST ${options.base}/agentmemory/capture/retry ${CURL_AUTH_HEADER} -H "Content-Type: application/json" -d '{"all":true}'`);
1202
+ return 0;
1203
+ }
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));
821
1301
  };
822
- const envValue = env["AGENTMEMORY_DATA_DIR"];
823
- if (envValue) return {
824
- dataDir: forInstance(absoluteDataDir(envValue, cwd, home)),
825
- source: "env"
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")
826
1375
  };
827
- const legacyDir = resolve(cwd, "data");
828
- const hasLegacyStore = existsSync(join(legacyDir, "state_store.db")) || existsSync(join(legacyDir, "iii-config.yaml"));
829
- if (instance === 0 && hasLegacyStore) return {
830
- dataDir: legacyDir,
831
- source: "default"
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
+ }
832
1564
  };
833
- const defaultDir = platformDefaultDataDir(env, home, nodePlatform);
834
- const gitParent = nearestGitParent(cwd);
835
- if (gitParent && isWithin(gitParent, defaultDir)) {
836
- const relocated = platformDefaultDataDir(env, home, nodePlatform, false);
837
- if (relocated !== defaultDir) return {
838
- dataDir: forInstance(relocated),
839
- source: "default",
840
- relocatedFrom: defaultDir
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);
841
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(() => {});
842
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();
843
1871
  return {
844
- dataDir: forInstance(defaultDir),
845
- source: "default"
1872
+ child,
1873
+ file: project.file,
1874
+ namespace: composeNamespace(runtime.ports.restPort),
1875
+ stateDir: project.stateDir,
1876
+ logFile,
1877
+ logStartOffset
846
1878
  };
847
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
+ }
848
1930
  //#endregion
849
1931
  //#region src/cli.ts
850
1932
  const c = {
@@ -865,12 +1947,14 @@ const IS_WINDOWS = platform() === "win32";
865
1947
  const IS_VERBOSE = args.includes("--verbose") || args.includes("-v") || process.env["AGENTMEMORY_VERBOSE"] === "1" || process.env["AGENTMEMORY_VERBOSE"] === "true";
866
1948
  setBootVerbose(IS_VERBOSE);
867
1949
  const IS_RESET = args.includes("--reset");
868
- hydrateProcessEnvFromFile();
1950
+ const shellEnv = hydrateProcessEnvFromFile();
869
1951
  if (args.includes("--version") || args.includes("-V")) {
870
1952
  process.stdout.write(`${VERSION}\n`);
871
1953
  process.exit(0);
872
1954
  }
873
- const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || "0.11.2";
1955
+ const IIPINNED_DEFAULT_VERSION = III_PINNED_VERSION;
1956
+ const IIPINNED_VERSION = process.env["AGENTMEMORY_III_VERSION"] || IIPINNED_DEFAULT_VERSION;
1957
+ const IIIENGINE_INSTALL_CMD = `curl -fsSL https://install.iii.dev/iii/main/install.sh | VERSION=${IIPINNED_VERSION} sh`;
874
1958
  function iiiReleaseAsset() {
875
1959
  const p = platform();
876
1960
  const a = process.arch;
@@ -911,13 +1995,14 @@ agentmemory — persistent memory for AI coding agents
911
1995
  Usage: agentmemory [command] [options]
912
1996
 
913
1997
  Commands:
914
- (default) Start agentmemory worker
1998
+ (default) Start agentmemory via a per-instance Worker Compose project
915
1999
  init Copy bundled .env.example to ~/.agentmemory/.env if absent
916
2000
  connect [agent] Wire agentmemory into an installed agent
917
2001
  (${wrapList(knownAgents(), 21)}).
918
2002
  No arg = interactive picker. --all wires every detected agent.
919
2003
  --dry-run shows what would change. --force re-installs.
920
2004
  status Show connection status, memory count, flags, and health
2005
+ console Show migration guidance for the retired iii console
921
2006
  doctor Interactive diagnostic + fixer. [F]ix · [S]kip · [?]more · [Q]uit
922
2007
  --all: apply every fix without prompting (CI)
923
2008
  --dry-run: show what each fix would do, don't execute
@@ -928,11 +2013,20 @@ Commands:
928
2013
  in one command (no second terminal).
929
2014
  upgrade Upgrade local deps + iii runtime (best effort)
930
2015
  stop [--force] Stop the running iii-engine started by this CLI.
931
- --force bypasses the Docker-heuristic guard and signals
932
- whatever pidfile+lsof report on the REST port (use when
933
- 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).
934
2018
  mcp Start standalone MCP shim — opt-in surface for MCP-only clients
935
2019
  (Cursor, Gemini CLI, etc). REST always available at :3111.
2020
+ capture [--drain] Show capture health: the local offline spool and the server
2021
+ inbox (pending, retrying, dead letters). --drain sends
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.
936
2030
  import-jsonl [p] Import Claude Code JSONL transcripts (default: ~/.claude/projects)
937
2031
  --max-files <N> | --max-files=<N>: override scan cap (default 200, max 1000;
938
2032
  out-of-range is rejected; for trees >1000 files, batch by subdirectory)
@@ -943,27 +2037,32 @@ Options:
943
2037
  --reset Wipe ~/.agentmemory/preferences.json and re-run onboarding
944
2038
  --tools all|core Tool visibility (default: all = ${ALL_TOOLS_COUNT} tools; core = ${CORE_TOOLS_COUNT} essentials)
945
2039
  --no-engine Skip auto-starting iii-engine
946
- --port <N> Override REST port (default: 3111). Streams (N+1), viewer
947
- (N+2), and iii engine (N+46023) auto-derive from N so a
948
- 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.
949
2042
  --instance <N> Shortcut for --port (3111 + N*100) to run multiple
950
2043
  daemons side-by-side without env gymnastics.
951
- --instance 1 -> 3211/3212/3213/49234, etc. (max N=50)
2044
+ --instance 1 -> 3211/3213/49234, etc. (max N=50)
952
2045
  --data-dir <path> Store iii-engine state outside the current repo
953
2046
 
954
2047
  Environment:
955
2048
  AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
956
2049
  Honored by status, doctor, and MCP shim commands.
957
2050
  AGENTMEMORY_DATA_DIR State directory fallback when --data-dir is not set.
958
- AGENTMEMORY_USE_DOCKER=1 Prefer the bundled docker-compose path over the
959
- native iii-engine binary on first run.
960
2051
  AGENTMEMORY_III_VERSION Override pinned iii-engine version (default ${IIPINNED_VERSION}).
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.
961
2060
  AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS
962
2061
  Window (seconds) for the smart-search follow-up diagnostic
963
2062
  (default 30). Long values overcount, short values undercount.
964
2063
 
965
2064
  Quick start:
966
- npx @agentmemory/agentmemory # start with local iii-engine or Docker
2065
+ npx @agentmemory/agentmemory # start pinned iii Worker Compose runtime
967
2066
  npx @agentmemory/agentmemory demo # see semantic recall in 30 seconds
968
2067
  npx @agentmemory/agentmemory doctor # diagnose config + feature flags
969
2068
  npx @agentmemory/agentmemory status # health + memory count + flags
@@ -979,21 +2078,63 @@ if (toolsIdx !== -1 && args[toolsIdx + 1]) {
979
2078
  if (toolsMode !== "all" && toolsMode !== "core") p.log.warn(`Unknown --tools value "${toolsMode}" (valid: all, core); falling back to all.`);
980
2079
  process.env["AGENTMEMORY_TOOLS"] = toolsMode;
981
2080
  }
982
- const portIdx = args.indexOf("--port");
983
- if (portIdx !== -1 && args[portIdx + 1]) process.env["III_REST_PORT"] = args[portIdx + 1];
984
- const instanceIdx = args.indexOf("--instance");
985
- if (instanceIdx !== -1 && args[instanceIdx + 1]) {
986
- const n = parseInt(args[instanceIdx + 1] || "", 10);
987
- if (Number.isFinite(n) && n >= 0 && n <= 50) {
988
- const base = 3111 + n * 100;
989
- if (!process.env["III_REST_PORT"]) process.env["III_REST_PORT"] = String(base);
2081
+ const URL_CLIENT_COMMANDS = new Set([
2082
+ "status",
2083
+ "doctor",
2084
+ "mcp",
2085
+ "capture",
2086
+ "compression",
2087
+ "graph-index"
2088
+ ]);
2089
+ let hasExplicitLocalPortOverride = false;
2090
+ let selectedInstance = 0;
2091
+ function cliArgValue(name) {
2092
+ const inline = args.find((arg) => arg.startsWith(`${name}=`));
2093
+ if (inline) return inline.slice(name.length + 1);
2094
+ const index = args.indexOf(name);
2095
+ return index === -1 ? void 0 : args[index + 1];
2096
+ }
2097
+ const portValue = cliArgValue("--port");
2098
+ if (portValue) {
2099
+ process.env["III_REST_PORT"] = portValue;
2100
+ hasExplicitLocalPortOverride = true;
2101
+ }
2102
+ const instanceValue = cliArgValue("--instance");
2103
+ if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance="))) {
2104
+ if (instanceValue === void 0 || !/^\d+$/.test(instanceValue)) {
2105
+ p.log.error("--instance must be an integer between 0 and 50.");
2106
+ process.exit(1);
990
2107
  }
2108
+ const n = Number(instanceValue);
2109
+ if (!Number.isInteger(n) || n > 50) {
2110
+ p.log.error("--instance must be an integer between 0 and 50.");
2111
+ process.exit(1);
2112
+ }
2113
+ selectedInstance = n;
2114
+ const base = 3111 + n * 100;
2115
+ if (!hasExplicitLocalPortOverride) process.env["III_REST_PORT"] = String(base);
2116
+ hasExplicitLocalPortOverride = true;
2117
+ }
2118
+ const restPort = parseInt(process.env["III_REST_PORT"] || "3111", 10);
2119
+ if (Number.isFinite(restPort) && restPort > 0) {
2120
+ process.env["III_VIEWER_PORT"] ??= String(restPort + 2);
2121
+ const configuredEngineUrl = process.env["III_ENGINE_URL"];
2122
+ if (configuredEngineUrl) try {
2123
+ const parsedEnginePort = new URL(configuredEngineUrl).port;
2124
+ if (parsedEnginePort) process.env["III_ENGINE_PORT"] = parsedEnginePort;
2125
+ } catch {}
2126
+ else process.env["III_ENGINE_PORT"] ??= String(restPort + 46023);
2127
+ process.env["AGENTMEMORY_METRICS_PORT"] ??= String(restPort + 6353);
991
2128
  }
992
2129
  const dataDirResolution = resolveDataDir({ args });
993
- if (dataDirResolution.source !== "default") process.env["AGENTMEMORY_DATA_DIR"] = dataDirResolution.dataDir;
2130
+ process.env["AGENTMEMORY_DATA_DIR"] = dataDirResolution.dataDir;
2131
+ process.env["AGENTMEMORY_RUNTIME_DIR"] = selectedInstance > 0 ? dataDirResolution.dataDir : join(homedir(), ".agentmemory");
994
2132
  const skipEngine = args.includes("--no-engine");
2133
+ function shouldUseAgentmemoryUrl() {
2134
+ return !hasExplicitLocalPortOverride && URL_CLIENT_COMMANDS.has(args[0] ?? "");
2135
+ }
995
2136
  function getRestPort() {
996
- const url = process.env["AGENTMEMORY_URL"];
2137
+ const url = shouldUseAgentmemoryUrl() ? process.env["AGENTMEMORY_URL"] : void 0;
997
2138
  if (url) try {
998
2139
  const parsed = new URL(url).port;
999
2140
  if (parsed) return parseInt(parsed, 10);
@@ -1001,10 +2142,13 @@ function getRestPort() {
1001
2142
  return parseInt(process.env["III_REST_PORT"] || "3111", 10) || 3111;
1002
2143
  }
1003
2144
  function getBaseUrl() {
1004
- const url = process.env["AGENTMEMORY_URL"];
2145
+ const url = shouldUseAgentmemoryUrl() ? process.env["AGENTMEMORY_URL"] : void 0;
1005
2146
  if (url) return url.replace(/\/+$/, "");
1006
2147
  return `http://localhost:${getRestPort()}`;
1007
2148
  }
2149
+ function getConfiguredViewerPort() {
2150
+ return parseInt(process.env["III_VIEWER_PORT"] || "", 10) || getRestPort() + 2;
2151
+ }
1008
2152
  let discoveredViewerPort = null;
1009
2153
  async function discoverViewerPort() {
1010
2154
  if (discoveredViewerPort !== null) return;
@@ -1027,75 +2171,36 @@ function getViewerUrl() {
1027
2171
  }
1028
2172
  try {
1029
2173
  const u = new URL(getBaseUrl());
1030
- const vPort = parseInt(process.env["III_VIEWER_PORT"] || "", 10) || (parseInt(u.port || "3111", 10) || 3111) + 2;
2174
+ const vPort = getConfiguredViewerPort();
1031
2175
  return `${u.protocol}//${u.hostname}:${vPort}`;
1032
2176
  } catch {
1033
- return `http://localhost:${parseInt(process.env["III_VIEWER_PORT"] || "", 10) || getRestPort() + 2}`;
2177
+ return `http://localhost:${getConfiguredViewerPort()}`;
1034
2178
  }
1035
2179
  }
1036
- function getStreamPort() {
1037
- return parseInt(process.env["III_STREAM_PORT"] || "", 10) || parseInt(process.env["III_STREAMS_PORT"] || "", 10) || getRestPort() + 1;
1038
- }
1039
2180
  function getEnginePort() {
1040
- const explicit = parseInt(process.env["III_ENGINE_PORT"] || "", 10);
1041
- if (explicit) return explicit;
1042
2181
  const url = process.env["III_ENGINE_URL"];
1043
2182
  if (url) try {
1044
2183
  const parsed = new URL(url).port;
1045
2184
  if (parsed) return parseInt(parsed, 10);
1046
2185
  } catch {}
2186
+ const explicit = parseInt(process.env["III_ENGINE_PORT"] || "", 10);
2187
+ if (explicit) return explicit;
1047
2188
  return getRestPort() + 46023;
1048
2189
  }
1049
2190
  async function isEngineRunning() {
1050
- try {
1051
- await fetch(`${getBaseUrl()}/`, { signal: AbortSignal.timeout(2e3) });
1052
- return true;
1053
- } catch {
1054
- return false;
1055
- }
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;
1056
2195
  }
1057
2196
  async function isAgentmemoryReady() {
1058
- try {
1059
- const res = await fetch(`${getBaseUrl()}/agentmemory/livez`, { signal: AbortSignal.timeout(2e3) });
1060
- if (!res.ok) return false;
1061
- try {
1062
- const data = await res.json();
1063
- if (typeof data.viewerPort === "number") {
1064
- discoveredViewerPort = data.viewerPort;
1065
- return true;
1066
- }
1067
- if (data.viewerSkipped) return true;
1068
- return false;
1069
- } catch {
1070
- return false;
1071
- }
1072
- } catch {
1073
- return false;
1074
- }
2197
+ const result = await withinStartupDeadline(startupDeadline(2e3), workerReadinessProbe).catch(() => null);
2198
+ return result?.completed === true && result.value;
1075
2199
  }
1076
- function findIiiConfig() {
1077
- const envPath = process.env["AGENTMEMORY_III_CONFIG"];
1078
- const candidates = [
1079
- ...envPath ? [envPath] : [],
1080
- join(process.cwd(), "iii-config.yaml"),
1081
- join(homedir(), ".agentmemory", "iii-config.yaml"),
1082
- join(__dirname, "iii-config.yaml"),
1083
- join(__dirname, "..", "iii-config.yaml")
1084
- ];
1085
- for (const c of candidates) if (existsSync(c)) return c;
1086
- return "";
1087
- }
1088
- function yamlSingleQuoted(value) {
1089
- return `'${value.replace(/'/g, "''")}'`;
1090
- }
1091
- function renderIiiConfig(template, dataDir) {
1092
- return template.replace("file_path: ./data/state_store.db", `file_path: ${yamlSingleQuoted(join(dataDir, "state_store.db"))}`).replace("file_path: ./data/stream_store", `file_path: ${yamlSingleQuoted(join(dataDir, "stream_store"))}`);
1093
- }
1094
- function writeRuntimeIiiConfig(configPath, dataDir) {
1095
- mkdirSync(dataDir, { recursive: true });
1096
- const runtimeConfigPath = join(dataDir, "iii-config.yaml");
1097
- writeFileSync(runtimeConfigPath, renderIiiConfig(readFileSync(configPath, "utf8"), dataDir));
1098
- return runtimeConfigPath;
2200
+ function workerReadinessProbe(signal) {
2201
+ return probeWorker(getBaseUrl(), signal, (port) => {
2202
+ discoveredViewerPort = port;
2203
+ });
1099
2204
  }
1100
2205
  function warnIfRelocatedDataDir() {
1101
2206
  if (!dataDirResolution.relocatedFrom) return;
@@ -1178,19 +2283,10 @@ function resolveCompatibleIii(iiiBinPath) {
1178
2283
  return null;
1179
2284
  }
1180
2285
  function enginePidfilePath() {
1181
- return join(homedir(), ".agentmemory", "iii.pid");
2286
+ return runtimeMetadataPath("iii.pid");
1182
2287
  }
1183
2288
  function engineStatePath() {
1184
- return join(homedir(), ".agentmemory", "engine-state.json");
1185
- }
1186
- function writeEnginePidfile(pid) {
1187
- try {
1188
- const pidPath = enginePidfilePath();
1189
- mkdirSync(dirname(pidPath), { recursive: true });
1190
- writeFileSync(pidPath, `${pid}\n`, { encoding: "utf-8" });
1191
- } catch (err) {
1192
- vlog(`writeEnginePidfile: ${err instanceof Error ? err.message : String(err)}`);
1193
- }
2289
+ return runtimeMetadataPath("engine-state.json");
1194
2290
  }
1195
2291
  function readEnginePidfile() {
1196
2292
  try {
@@ -1207,7 +2303,7 @@ function clearEnginePidfile() {
1207
2303
  } catch {}
1208
2304
  }
1209
2305
  function workerPidfilePath() {
1210
- return join(homedir(), ".agentmemory", "worker.pid");
2306
+ return runtimeMetadataPath("worker.pid");
1211
2307
  }
1212
2308
  function readWorkerPidfile() {
1213
2309
  try {
@@ -1223,20 +2319,11 @@ function clearWorkerPidfile() {
1223
2319
  unlinkSync(workerPidfilePath());
1224
2320
  } catch {}
1225
2321
  }
1226
- function writeEngineState(state) {
1227
- try {
1228
- const statePath = engineStatePath();
1229
- mkdirSync(dirname(statePath), { recursive: true });
1230
- writeFileSync(statePath, `${JSON.stringify(state)}\n`, { encoding: "utf-8" });
1231
- } catch (err) {
1232
- vlog(`writeEngineState: ${err instanceof Error ? err.message : String(err)}`);
1233
- }
1234
- }
1235
2322
  function readEngineState() {
1236
2323
  try {
1237
2324
  const raw = readFileSync(engineStatePath(), "utf-8");
1238
2325
  const parsed = JSON.parse(raw);
1239
- if (parsed && (parsed.kind === "native" || parsed.kind === "docker")) return parsed;
2326
+ if (parsed && (parsed.kind === "native" || parsed.kind === "docker" || parsed.kind === "compose")) return parsed;
1240
2327
  return null;
1241
2328
  } catch {
1242
2329
  return null;
@@ -1247,12 +2334,154 @@ function clearEngineState() {
1247
2334
  unlinkSync(engineStatePath());
1248
2335
  } catch {}
1249
2336
  }
1250
- function discoverComposeFile() {
1251
- return [
1252
- join(__dirname, "..", "docker-compose.yml"),
1253
- join(__dirname, "docker-compose.yml"),
1254
- join(process.cwd(), "docker-compose.yml")
1255
- ].find((c) => existsSync(c)) ?? null;
2337
+ function engineStateRestPort(state) {
2338
+ if (state.restPort && Number.isFinite(state.restPort)) return state.restPort;
2339
+ if (state.kind === "compose") return 3111;
2340
+ try {
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];
2342
+ if (configuredPort) return parseInt(configuredPort, 10);
2343
+ } catch {}
2344
+ return 3111;
2345
+ }
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();
2448
+ }
2449
+ function configuredEngineMayStartWorker() {
2450
+ const explicit = startupTimeouts().engineManagesWorker;
2451
+ if (explicit !== void 0) return explicit;
2452
+ const state = readEngineState();
2453
+ if (state?.kind !== "native" || state.attached) return false;
2454
+ try {
2455
+ return configStartsAgentmemoryWorker(readFileSync(state.configPath, "utf-8"), join(__dirname, "index.mjs"), state.cwd);
2456
+ } catch {
2457
+ return false;
2458
+ }
2459
+ }
2460
+ async function assertRuntimePortOwnership() {
2461
+ const state = readEngineState();
2462
+ if (!state) return;
2463
+ if (state.kind === "docker") {
2464
+ p.log.error(legacyDockerStateMessage(state));
2465
+ process.exit(1);
2466
+ }
2467
+ const ownerPort = engineStateRestPort(state);
2468
+ if (state.kind === "compose" && ownerPort !== getRestPort()) return;
2469
+ const requestedPort = getRestPort();
2470
+ if (ownerPort === requestedPort) return;
2471
+ const enginePid = readEnginePidfile();
2472
+ const workerPid = readWorkerPidfile();
2473
+ let active = Boolean(enginePid && pidAlive(enginePid) && !isForeignPortHolder(pidCommand(enginePid)) || workerPid && pidAlive(workerPid));
2474
+ if (!active) try {
2475
+ await fetch(`http://127.0.0.1:${ownerPort}/`, { signal: AbortSignal.timeout(750) });
2476
+ active = true;
2477
+ } catch {}
2478
+ if (active) {
2479
+ p.log.error(`The canonical lifecycle state is owned by a live agentmemory engine on REST port ${ownerPort}. Refusing to reuse it for port ${requestedPort}. Use the original port, or use --instance <N> for an isolated daemon.`);
2480
+ process.exit(1);
2481
+ }
2482
+ clearEnginePidfile();
2483
+ clearWorkerPidfile();
2484
+ clearEngineState();
1256
2485
  }
1257
2486
  function isInvokedViaNpx() {
1258
2487
  if (process.env["npm_lifecycle_event"] === "npx") return true;
@@ -1285,93 +2514,25 @@ async function maybeOfferGlobalInstall() {
1285
2514
  if (runCommand(npmBin, [
1286
2515
  "install",
1287
2516
  "-g",
1288
- `@agentmemory/agentmemory@0.9.29`
1289
- ], { label: `Installing @agentmemory/agentmemory@0.9.29 globally` })) {
2517
+ `@agentmemory/agentmemory@0.9.31`
2518
+ ], { label: `Installing @agentmemory/agentmemory@0.9.31 globally` })) {
1290
2519
  p.log.success("Installed globally. `agentmemory stop` etc. will now work in new shells.");
1291
2520
  writePrefs({ skipGlobalInstall: true });
1292
2521
  } else p.log.warn("Global install failed. Try manually: npm install -g @agentmemory/agentmemory");
1293
2522
  }
1294
- function detectIiiConsole() {
1295
- const onPath = whichBinary("iii-console");
1296
- if (onPath) return {
1297
- kind: "installed",
1298
- binPath: onPath
1299
- };
1300
- const fallback = IS_WINDOWS ? join(process.env["USERPROFILE"] ?? "", ".local", "bin", "iii-console.exe") : join(homedir(), ".local", "bin", "iii-console");
1301
- if (fallback && existsSync(fallback)) return {
1302
- kind: "installed",
1303
- binPath: fallback
1304
- };
1305
- return { kind: "missing" };
1306
- }
1307
- const III_CONSOLE_INSTALL_CMD = `curl -fsSL https://install.iii.dev/iii/main/install.sh | VERSION=${IIPINNED_VERSION} sh`;
1308
- function iiiConsoleInstallHint() {
1309
- if (!IS_WINDOWS) return III_CONSOLE_INSTALL_CMD;
1310
- return `# PowerShell:\n $env:VERSION = "${IIPINNED_VERSION}"\n iwr -useb https://install.iii.dev/iii/main/install.sh -OutFile install.sh\n bash install.sh # WSL or Git Bash required\n# Or grab the pinned release directly:\n https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
1311
- }
1312
- async function ensureIiiConsole() {
1313
- const state = detectIiiConsole();
1314
- if (state.kind === "installed") return state;
1315
- if (!process.stdin.isTTY || process.env["CI"]) return state;
1316
- if (readPrefs().skipConsoleInstall) return state;
1317
- const answer = await p.confirm({
1318
- message: "iii console gives engine-level visibility (workers, functions, queues, traces). Install now?",
1319
- initialValue: true
1320
- });
1321
- if (p.isCancel(answer)) return state;
1322
- if (answer === false) {
1323
- writePrefs({ skipConsoleInstall: true });
1324
- return state;
1325
- }
1326
- const shBin = whichBinary("sh");
1327
- const curlBin = whichBinary("curl");
1328
- if (!shBin || !curlBin) {
1329
- p.log.warn(`curl or sh not found. Install manually:\n ${iiiConsoleInstallHint()}`);
1330
- return state;
1331
- }
1332
- if (!runCommand(shBin, ["-c", III_CONSOLE_INSTALL_CMD], { label: "Installing iii console" })) {
1333
- p.log.warn(`iii console install failed. Re-run manually:\n ${iiiConsoleInstallHint()}`);
1334
- return state;
1335
- }
1336
- return detectIiiConsole();
1337
- }
1338
- function adoptRunningEngine() {
1339
- try {
1340
- const existingState = readEngineState();
1341
- const existingPid = readEnginePidfile();
1342
- if (existingState && existingPid) return;
1343
- const enginePid = findEnginePidsByPort(getRestPort())[0];
1344
- if (enginePid) {
1345
- const comm = pidCommand(enginePid);
1346
- if (isForeignPortHolder(comm)) {
1347
- vlog(`adoptRunningEngine: refusing to adopt pid ${enginePid} (${comm}) — not the iii engine binary`);
1348
- return;
1349
- }
1350
- }
1351
- if (enginePid && !existingPid) writeEnginePidfile(enginePid);
1352
- if (!existingState) writeEngineState({
1353
- kind: "native",
1354
- configPath: findIiiConfig() || "",
1355
- attached: true
1356
- });
1357
- if (enginePid && !existingPid) p.log.info(c.ok(`Attached to existing iii-engine (pid ${enginePid})`));
1358
- } catch (err) {
1359
- vlog(`adoptRunningEngine: ${err instanceof Error ? err.message : String(err)}`);
1360
- }
1361
- }
1362
2523
  async function runIiiInstaller() {
1363
2524
  const releaseUrl = iiiReleaseUrl();
1364
2525
  const asset = iiiReleaseAsset();
1365
2526
  const isZipAsset = asset?.endsWith(".zip") === true;
1366
2527
  if (!releaseUrl) {
1367
- 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}.`);
1368
2529
  return {
1369
2530
  ok: false,
1370
2531
  binPath: null
1371
2532
  };
1372
2533
  }
1373
2534
  if (IS_WINDOWS || isZipAsset) {
1374
- p.log.info(`Auto-install unavailable on ${platform()} — ${asset} isn't tar-compatible. Install manually:\n 1. Download ${releaseUrl}\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}`);
1375
2536
  return {
1376
2537
  ok: false,
1377
2538
  binPath: null
@@ -1379,8 +2540,14 @@ async function runIiiInstaller() {
1379
2540
  }
1380
2541
  const shBin = whichBinary("sh");
1381
2542
  const curlBin = whichBinary("curl");
1382
- if (!shBin || !curlBin) {
1383
- p.log.warn("curl or sh not found. Cannot auto-install iii-engine.");
2543
+ const tarBin = whichBinary("tar");
2544
+ if (!shBin || !curlBin || !tarBin) {
2545
+ const missing = [
2546
+ !shBin && "sh",
2547
+ !curlBin && "curl",
2548
+ !tarBin && "tar"
2549
+ ].filter(Boolean).join(", ");
2550
+ p.log.warn(`${missing} not found. Cannot auto-install iii-engine.`);
1384
2551
  return {
1385
2552
  ok: false,
1386
2553
  binPath: null
@@ -1388,75 +2555,335 @@ async function runIiiInstaller() {
1388
2555
  }
1389
2556
  const binDir = agentmemoryBinDir();
1390
2557
  const binPath = privateIiiPath();
1391
- if (!runCommand(shBin, ["-c", [
1392
- `mkdir -p "${binDir}"`,
1393
- `curl -fsSL "${releaseUrl}" | tar -xz -C "${binDir}"`,
1394
- `chmod +x "${binPath}"`
1395
- ].join(" && ")], {
1396
- label: `Installing iii-engine v${IIPINNED_VERSION} (pinned)`,
1397
- optional: true
1398
- })) {
1399
- p.log.warn(`iii-engine installer failed. Fallbacks: Docker (\`docker pull iiidev/iii:${IIPINNED_VERSION}\`) or download manually from https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}.`);
2558
+ const label = `Installing iii-engine v${IIPINNED_VERSION} (pinned)`;
2559
+ const spinner = p.spinner();
2560
+ spinner.start(label);
2561
+ const outcome = installIiiArchive({
2562
+ sh: shBin,
2563
+ releaseUrl,
2564
+ version: IIPINNED_VERSION,
2565
+ asset: asset ?? "",
2566
+ binDir,
2567
+ binPath
2568
+ });
2569
+ if (!outcome.ok) {
2570
+ spinner.stop(`${label} ${pc.red("✗")}`);
2571
+ const sha256 = expectedIiiSha256(IIPINNED_VERSION, asset ?? "");
2572
+ const heading = outcome.reason === "checksum" ? "iii-engine download failed verification" : outcome.reason === "no-checksum" ? "iii-engine auto-install skipped" : "iii-engine download failed";
2573
+ const checksumHint = sha256 ? "" : `Compare the archive against ${releaseUrl.replace(/\.(tar\.gz|zip)$/, ".sha256")} before extracting.\n`;
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);
1400
2575
  return {
1401
2576
  ok: false,
1402
2577
  binPath: null
1403
2578
  };
1404
2579
  }
2580
+ spinner.stop(`${label} ${pc.green("✓")}`);
1405
2581
  return {
1406
2582
  ok: true,
1407
2583
  binPath
1408
2584
  };
1409
- }
1410
- let startupFailure = null;
1411
- function spawnEngineBackground(bin, spawnArgs, label) {
1412
- vlog(`spawn: ${bin} ${spawnArgs.join(" ")}`);
1413
- const child = spawn(bin, spawnArgs, {
1414
- detached: true,
1415
- stdio: [
1416
- "ignore",
1417
- "ignore",
1418
- "pipe"
1419
- ],
1420
- windowsHide: true
2585
+ }
2586
+ let startupFailure = null;
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
+ }
2656
+ function printCapturedStartupStderr() {
2657
+ const stderr = redactCredentialUrls(activeStartupStderr.text().trim());
2658
+ if (IS_VERBOSE && stderr) p.note(stderr, "engine stderr");
2659
+ }
2660
+ function readComposeStartupLogs() {
2661
+ const state = readEngineState();
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
+ }
2675
+ }
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");
2732
+ }
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
+ }
2739
+ }
2740
+ function composeChildProcesses(state) {
2741
+ if (IS_WINDOWS || !state.composePid) return [];
2742
+ try {
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
+ }
2767
+ }
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 [];
2775
+ }
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) {
2788
+ try {
2789
+ process.kill(-pgid, "SIGTERM");
2790
+ } catch (err) {
2791
+ if (err.code !== "ESRCH") stopped = false;
2792
+ continue;
2793
+ }
2794
+ const deadline = Date.now() + 5e3;
2795
+ while (Date.now() < deadline) {
2796
+ try {
2797
+ process.kill(-pgid, 0);
2798
+ } catch {
2799
+ break;
2800
+ }
2801
+ await new Promise((resolve) => setTimeout(resolve, 100));
2802
+ }
2803
+ try {
2804
+ process.kill(-pgid, 0);
2805
+ process.kill(-pgid, "SIGKILL");
2806
+ } catch {}
2807
+ }
2808
+ for (const pid of composeChildPids(state)) if (pidAlive(pid) && !await signalAndWait(pid, "SIGTERM", 1e3)) stopped = false;
2809
+ return stopped;
2810
+ }
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
1421
2868
  });
1422
- const isDocker = label.includes("Docker");
1423
- if (!isDocker && typeof child.pid === "number") writeEnginePidfile(child.pid);
1424
- const stderrChunks = [];
1425
- let stderrBytes = 0;
1426
- const MAX_STDERR_CAPTURE = 16 * 1024;
1427
- child.stderr?.on("data", (chunk) => {
1428
- if (stderrBytes >= MAX_STDERR_CAPTURE) return;
1429
- const slice = chunk.subarray(0, MAX_STDERR_CAPTURE - stderrBytes);
1430
- stderrChunks.push(slice);
1431
- stderrBytes += slice.length;
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
+ };
1432
2876
  });
1433
- child.on("exit", (code, signal) => {
1434
- if (code !== null && code !== 0 || code === null && signal !== null) {
1435
- const stderr = Buffer.concat(stderrChunks).toString("utf-8");
1436
- startupFailure = {
1437
- kind: isDocker ? "docker-crashed" : "engine-crashed",
1438
- stderr: stderr.trim() || (signal ? `process killed by signal ${signal}` : `process exited with code ${code}`),
1439
- binary: bin
1440
- };
1441
- vlog(`engine exited early: code=${code} signal=${signal}`);
1442
- if (IS_VERBOSE && stderr.trim()) p.log.error(`engine stderr:\n${stderr}`);
1443
- if (!isDocker) clearEnginePidfile();
1444
- clearEngineState();
1445
- }
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
+ };
1446
2883
  });
1447
- child.unref();
1448
- return child;
1449
- }
1450
- function startIiiBin(iiiBin, configPath) {
1451
2884
  const s = p.spinner();
1452
- s.start(`Starting iii-engine: ${iiiBin}`);
1453
- writeEngineState({
1454
- kind: "native",
1455
- configPath,
1456
- binPath: iiiBin
1457
- });
1458
- spawnEngineBackground(iiiBin, ["--config", configPath], "iii-engine");
1459
- s.stop(c.ok("iii-engine process started"));
2885
+ s.start(`Starting iii Worker Compose: ${iiiBin}`);
2886
+ s.stop(c.ok(`Compose daemon started (pid ${compose.child.pid})`));
1460
2887
  return true;
1461
2888
  }
1462
2889
  function pickCompatibleIii(candidates) {
@@ -1468,41 +2895,56 @@ function pickCompatibleIii(candidates) {
1468
2895
  return null;
1469
2896
  }
1470
2897
  async function startEngine() {
1471
- const configPath = findIiiConfig();
2898
+ startupFailure = null;
2899
+ engineReadinessDeadline = null;
2900
+ let stateBackendKind;
2901
+ try {
2902
+ stateBackendKind = getStateBackend();
2903
+ } catch (err) {
2904
+ p.log.error(err instanceof Error ? err.message : String(err));
2905
+ process.exit(1);
2906
+ }
2907
+ if (stateBackendKind === "redis" && !getRedisUrl()) {
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.");
2909
+ process.exit(1);
2910
+ }
2911
+ const persistedState = readEngineState();
2912
+ if (persistedState?.kind === "docker") {
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();
2928
+ }
2929
+ }
1472
2930
  warnIfRelocatedDataDir();
1473
- const runtimeConfigPath = configPath ? writeRuntimeIiiConfig(configPath, dataDirResolution.dataDir) : "";
1474
2931
  const pathIii = whichBinary("iii");
1475
- vlog(`iii binary: ${pathIii ?? "(not on PATH)"}, config: ${runtimeConfigPath || "(not found)"}`);
2932
+ vlog(`iii binary: ${pathIii ?? "(not on PATH)"}`);
1476
2933
  const fallbacks = fallbackIiiPaths().filter((p) => existsSync(p));
1477
2934
  for (const f of fallbacks) vlog(`fallback iii at ${f} reports version: ${iiiBinVersion(f) ?? "unknown"}`);
1478
2935
  let iiiBin = pickCompatibleIii([pathIii, ...fallbacks]);
1479
- if (iiiBin && runtimeConfigPath) {
2936
+ if (iiiBin) {
1480
2937
  if (iiiBin !== pathIii) {
1481
2938
  p.log.info(`Using iii at: ${c.dim(iiiBin)} (v${c.accent(IIPINNED_VERSION)})`);
1482
2939
  process.env["PATH"] = `${dirname(iiiBin)}${delimiter}${process.env["PATH"] ?? ""}`;
1483
2940
  }
1484
- return startIiiBin(iiiBin, runtimeConfigPath);
2941
+ return startIiiBin(iiiBin);
1485
2942
  }
1486
2943
  if (pathIii && !iiiBin) vlog(`iii on PATH is v${iiiBinVersion(pathIii) ?? "unknown"}, pin is v${IIPINNED_VERSION}. Will install pinned engine to ${privateIiiPath()}.`);
1487
- if (!configPath) {
1488
- startupFailure = { kind: "no-engine" };
1489
- return false;
1490
- }
1491
- const dockerBin = whichBinary("docker");
1492
- vlog(`docker binary: ${dockerBin ?? "(not on PATH)"}`);
1493
2944
  mkdirSync(dataDirResolution.dataDir, { recursive: true });
1494
- const composeFile = [
1495
- join(__dirname, "..", "docker-compose.yml"),
1496
- join(__dirname, "docker-compose.yml"),
1497
- join(process.cwd(), "docker-compose.yml")
1498
- ].find((c) => existsSync(c));
1499
- vlog(`docker-compose.yml: ${composeFile ?? "(not found)"}`);
1500
- const dockerOptIn = process.env["AGENTMEMORY_USE_DOCKER"] === "1" || process.env["AGENTMEMORY_USE_DOCKER"] === "true";
1501
2945
  const interactive = !!process.stdin.isTTY && !process.env["CI"];
1502
2946
  let choice;
1503
- const pathIiiMismatch = pathIii !== null && resolveCompatibleIii(pathIii) === null;
1504
- if (dockerOptIn && dockerBin && composeFile) choice = "docker";
1505
- else if (pathIiiMismatch) {
2947
+ if (pathIii !== null && resolveCompatibleIii(pathIii) === null) {
1506
2948
  choice = "install";
1507
2949
  const detected = iiiBinVersion(pathIii);
1508
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).`);
@@ -1516,11 +2958,6 @@ async function startEngine() {
1516
2958
  label: `Install iii v${IIPINNED_VERSION} to ~/.agentmemory/bin (~6MB, ~5s)`,
1517
2959
  hint: "recommended"
1518
2960
  }];
1519
- if (dockerBin && composeFile) options.push({
1520
- value: "docker",
1521
- label: "Use Docker compose",
1522
- hint: "advanced"
1523
- });
1524
2961
  options.push({
1525
2962
  value: "manual",
1526
2963
  label: "Show manual install steps and exit"
@@ -1545,80 +2982,59 @@ async function startEngine() {
1545
2982
  if (result.ok && result.binPath) {
1546
2983
  process.env["PATH"] = `${dirname(result.binPath)}${delimiter}${process.env["PATH"] ?? ""}`;
1547
2984
  iiiBin = result.binPath;
1548
- return startIiiBin(iiiBin, configPath);
1549
- }
1550
- if (dockerBin && composeFile && interactive) {
1551
- const fallback = await p.confirm({
1552
- message: "Auto-install failed. Try Docker compose instead?",
1553
- initialValue: true
1554
- });
1555
- if (p.isCancel(fallback) || fallback !== true) {
1556
- startupFailure = { kind: "no-engine" };
1557
- return false;
1558
- }
1559
- choice = "docker";
1560
- } else {
1561
- startupFailure = { kind: "no-engine" };
1562
- return false;
2985
+ return startIiiBin(iiiBin);
1563
2986
  }
2987
+ startupFailure = { kind: "no-engine" };
2988
+ return false;
1564
2989
  }
1565
- if (choice === "docker" && dockerBin && composeFile) {
1566
- const s = p.spinner();
1567
- s.start("Starting iii-engine via Docker...");
1568
- writeEngineState({
1569
- kind: "docker",
1570
- composeFile
1571
- });
1572
- spawnEngineBackground(dockerBin, [
1573
- "compose",
1574
- "-f",
1575
- composeFile,
1576
- "up",
1577
- "-d"
1578
- ], "iii-engine via Docker");
1579
- s.stop("Docker compose started");
1580
- return true;
1581
- }
1582
- if (!composeFile && dockerBin) startupFailure = { kind: "no-docker-compose" };
1583
- else startupFailure = { kind: "no-engine" };
2990
+ startupFailure = { kind: "no-engine" };
1584
2991
  return false;
1585
2992
  }
1586
- async function waitForEngine(timeoutMs) {
1587
- const start = Date.now();
1588
- while (Date.now() - start < timeoutMs) {
1589
- if (await isEngineRunning()) return true;
1590
- await new Promise((r) => setTimeout(r, 500));
1591
- }
1592
- 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
+ });
3002
+ }
3003
+ async function waitForPersistedNativeEngine() {
3004
+ const state = readEngineState();
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));
3008
+ return true;
1593
3009
  }
1594
3010
  function installInstructions() {
1595
3011
  const releaseUrl = iiiReleaseUrl();
1596
- if (IS_WINDOWS) return [
1597
- `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
1598
- "",
1599
- " A) Download the prebuilt Windows binary:",
1600
- ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
1601
- ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
1602
- " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
1603
- " 4. Re-run: npx @agentmemory/agentmemory",
1604
- "",
1605
- ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
1606
- " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
1607
- "",
1608
- "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
1609
- "",
1610
- "Docs: https://iii.dev/docs"
1611
- ];
1612
- const linuxInstall = releaseUrl ? ` A) mkdir -p ~/.agentmemory/bin && curl -fsSL "${releaseUrl}" | tar -xz -C ~/.agentmemory/bin && chmod +x ~/.agentmemory/bin/iii` : ` A) Manual download: https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
3012
+ if (IS_WINDOWS) {
3013
+ const asset = iiiReleaseAsset() ?? "";
3014
+ const sha256 = expectedIiiSha256(IIPINNED_VERSION, asset);
3015
+ const verify = sha256 ? ` Verify before extracting: (Get-FileHash .\\${asset}).Hash must be ${sha256.toUpperCase()}` : ` Verify before extracting against the matching .sha256 file on the release page`;
3016
+ return [
3017
+ `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
3018
+ "",
3019
+ " A) Download the prebuilt Windows binary:",
3020
+ ` 1. Open https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`,
3021
+ ` 2. Download iii-x86_64-pc-windows-msvc.zip (or iii-aarch64-pc-windows-msvc.zip on ARM)`,
3022
+ verify,
3023
+ " 3. Extract iii.exe to %USERPROFILE%\\.local\\bin\\iii.exe (or add to PATH)",
3024
+ " 4. Re-run: npx @agentmemory/agentmemory",
3025
+ "",
3026
+ "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
3027
+ "",
3028
+ "Docs: https://iii.dev/docs"
3029
+ ];
3030
+ }
3031
+ const linuxInstall = releaseUrl ? ` A) ${iiiManualInstallCommand(releaseUrl, agentmemoryBinDir(), privateIiiPath(), expectedIiiSha256(IIPINNED_VERSION, iiiReleaseAsset() ?? ""))}` : ` A) Manual download: https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`;
1613
3032
  return [
1614
3033
  `agentmemory needs iii-engine v${IIPINNED_VERSION}. Pick one:`,
1615
3034
  "",
1616
3035
  linuxInstall,
1617
3036
  " Then re-run: npx @agentmemory/agentmemory",
1618
3037
  "",
1619
- ` B) Docker: docker pull iiidev/iii:${IIPINNED_VERSION}`,
1620
- " Re-run with AGENTMEMORY_USE_DOCKER=1 npx @agentmemory/agentmemory",
1621
- "",
1622
3038
  "Or skip the engine entirely (standalone MCP): npx @agentmemory/agentmemory mcp",
1623
3039
  "",
1624
3040
  "Docs: https://iii.dev/docs"
@@ -1627,79 +3043,101 @@ function installInstructions() {
1627
3043
  function portInUseDiagnostic(port) {
1628
3044
  return IS_WINDOWS ? ` netstat -ano | findstr :${port}` : ` lsof -i :${port} # or: ss -tlnp | grep :${port}`;
1629
3045
  }
1630
- async function waitForAgentmemoryReady(timeoutMs) {
1631
- const start = Date.now();
1632
- while (Date.now() - start < timeoutMs) {
1633
- if (await isAgentmemoryReady()) return true;
1634
- await new Promise((r) => setTimeout(r, 250));
1635
- }
1636
- return false;
1637
- }
1638
- function getEngineHost() {
1639
- for (const envKey of ["III_ENGINE_URL", "AGENTMEMORY_URL"]) {
1640
- const raw = process.env[envKey];
1641
- if (!raw) continue;
1642
- try {
1643
- const parsed = new URL(raw);
1644
- if (parsed.hostname) return parsed.hostname;
1645
- } catch {}
1646
- }
1647
- return "localhost";
1648
- }
1649
- function printReadyHint(consoleState) {
3046
+ function printReadyHint() {
1650
3047
  const restUrl = getBaseUrl();
1651
3048
  const viewerUrl = getViewerUrl();
1652
- const engineHost = getEngineHost();
1653
- const streamUrl = `ws://${engineHost}:${getStreamPort()}`;
1654
- const engineUrl = `ws://${engineHost}:${getEnginePort()}`;
1655
- const consoleLine = consoleState.kind === "installed" ? `${c.label("iii console")} ${c.dim(consoleState.binPath)} ${c.dim(`(run: ${consoleState.binPath} -p <port>)`)}` : `${c.label("iii console")} ${c.dim(`(install: ${iiiConsoleInstallHint()})`)}`;
3049
+ const engineUrl = process.env["III_ENGINE_URL"] || `ws://localhost:${getEnginePort()}`;
3050
+ const invocation = isInvokedViaNpx() ? "npx @agentmemory/agentmemory" : "agentmemory";
3051
+ const portFlags = portFlagSuffix(getRestPort(), selectedInstance);
1656
3052
  const lines = [
1657
3053
  `${c.label("REST API")} ${c.url(restUrl)}`,
1658
3054
  `${c.label("Viewer")} ${c.url(viewerUrl)}`,
1659
- `${c.label("Streams")} ${c.url(streamUrl)}`,
1660
- `${c.label("Engine")} ${c.url(engineUrl)}`,
1661
- consoleLine
3055
+ `${c.label("Engine")} ${c.url(engineUrl)}`
1662
3056
  ];
1663
3057
  p.note(lines.join("\n"), `agentmemory v${c.accent(VERSION)}`);
1664
- const demoCommand = isInvokedViaNpx() ? "npx @agentmemory/agentmemory demo" : "agentmemory demo";
3058
+ const demoCommand = `${invocation} demo${portFlags}`;
1665
3059
  process.stdout.write(`\n${c.dim("Try:")} ${c.cmd(demoCommand)}\n`);
1666
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
+ }
1667
3099
  async function main() {
3100
+ startupTimeouts();
3101
+ await assertRuntimePortOwnership();
1668
3102
  try {
1669
- 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") {
1670
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\`.`);
1671
3105
  process.exit(1);
1672
3106
  }
1673
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
+ }
1674
3114
  if (IS_RESET) resetPrefs();
1675
3115
  const firstRun = isFirstRun();
1676
3116
  const prefs = readPrefs();
1677
3117
  if (firstRun || IS_RESET || IS_VERBOSE || !prefs.skipSplash) renderSplash(VERSION);
1678
3118
  if (firstRun || IS_RESET) await runOnboarding();
3119
+ beginStartup();
1679
3120
  if (skipEngine) {
1680
3121
  if (IS_VERBOSE) p.log.info("Skipping engine check (--no-engine)");
1681
- await import("./src-G7yt8gGm.mjs");
1682
- if (await waitForAgentmemoryReady(15e3)) {
1683
- const consoleState = await ensureIiiConsole();
1684
- await maybeOfferGlobalInstall();
1685
- printReadyHint(consoleState);
1686
- }
3122
+ await startWorkerForEngineState();
3123
+ await maybeOfferGlobalInstall();
3124
+ printReadyHint();
3125
+ return;
3126
+ }
3127
+ if (await reconcilePersistedComposeEngine()) {
3128
+ await startWorkerForEngineState();
3129
+ await maybeOfferGlobalInstall();
3130
+ printReadyHint();
1687
3131
  return;
1688
3132
  }
1689
- if (await isEngineRunning()) {
3133
+ if (await isEngineRunning() || await waitForPersistedNativeEngine()) {
1690
3134
  if (IS_VERBOSE) p.log.success("iii-engine is running");
1691
3135
  const persisted = readEngineState();
1692
3136
  const attachedBin = (persisted?.kind === "native" && persisted.binPath && existsSync(persisted.binPath) ? persisted.binPath : null) ?? whichBinary("iii") ?? fallbackIiiPaths().find((p) => existsSync(p)) ?? null;
1693
- const detected = attachedBin ? iiiBinVersion(attachedBin) : null;
3137
+ const detected = persisted?.kind === "compose" ? iiiBinVersion(persisted.binPath) : attachedBin ? iiiBinVersion(attachedBin) : null;
1694
3138
  if (detected === IIPINNED_VERSION) {
1695
- adoptRunningEngine();
1696
- await import("./src-G7yt8gGm.mjs");
1697
- if (await waitForAgentmemoryReady(15e3)) {
1698
- const consoleState = await ensureIiiConsole();
1699
- await maybeOfferGlobalInstall();
1700
- printReadyHint(consoleState);
1701
- }
1702
- 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);
1703
3141
  }
1704
3142
  const detectedLabel = detected ? `v${detected}` : "an unverified version";
1705
3143
  const base = isInvokedViaNpx() ? "npx @agentmemory/agentmemory" : "agentmemory";
@@ -1718,7 +3156,7 @@ async function main() {
1718
3156
  ` ${c.cmd(base)}`,
1719
3157
  "",
1720
3158
  c.dim(`Step 2 needs no manual install. To install iii v${IIPINNED_VERSION} yourself (replaces your global iii), curl:`),
1721
- ` ${c.cmd(III_CONSOLE_INSTALL_CMD)}`,
3159
+ ` ${c.cmd(IIIENGINE_INSTALL_CMD)}`,
1722
3160
  ` ${c.dim("or download the release:")} ${c.url(`https://github.com/iii-hq/iii/releases/tag/iii%2Fv${IIPINNED_VERSION}`)}`
1723
3161
  ].join("\n"), "engine conflict");
1724
3162
  process.exit(1);
@@ -1726,16 +3164,42 @@ async function main() {
1726
3164
  if (!await startEngine()) {
1727
3165
  p.log.error("Could not start iii-engine.");
1728
3166
  const lines = installInstructions();
1729
- 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", "");
1730
3167
  p.note(lines.join("\n"), "Setup required");
1731
- process.exit(1);
3168
+ throw new Error("iii-engine could not be started.");
1732
3169
  }
1733
3170
  const s = p.spinner();
1734
3171
  s.start("Waiting for iii-engine to be ready...");
1735
- 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) {
1736
3184
  const port = getRestPort();
1737
- s.stop("iii-engine did not become ready within 15s");
1738
- 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") {
1739
3203
  p.log.error("The iii-engine process crashed on startup.");
1740
3204
  if (startupFailure.binary) p.log.info(`Binary: ${startupFailure.binary}`);
1741
3205
  if (startupFailure.stderr) p.note(startupFailure.stderr, "engine stderr");
@@ -1744,13 +3208,13 @@ async function main() {
1744
3208
  "Common causes:",
1745
3209
  ` - iii-engine version mismatch — reinstall the pinned v${IIPINNED_VERSION} binary`,
1746
3210
  " (sh script on macOS/Linux, GitHub release zip on Windows)",
1747
- " - Docker Desktop not running (if you're using the Docker path)",
1748
3211
  " - Port already in use (see below)",
1749
3212
  "",
1750
3213
  "See https://iii.dev/docs for current install instructions."
1751
3214
  ].join("\n"), "Troubleshooting");
1752
3215
  } else {
1753
3216
  p.log.error("The engine process started but the REST API never responded.");
3217
+ printCapturedStartupStderr();
1754
3218
  p.note([
1755
3219
  `Check whether port ${port} is already bound by another process:`,
1756
3220
  portInUseDiagnostic(port),
@@ -1761,22 +3225,30 @@ async function main() {
1761
3225
  "Re-run with --verbose to see engine stderr."
1762
3226
  ].join("\n"), "Troubleshooting");
1763
3227
  }
1764
- process.exit(1);
3228
+ if (composeLog?.text) p.log.error(`Compose log: ${composeLog.path}`);
3229
+ throw new Error(shortFailure);
1765
3230
  }
1766
3231
  s.stop(c.ok("iii-engine is ready"));
1767
- await import("./src-G7yt8gGm.mjs");
1768
- if (await waitForAgentmemoryReady(15e3)) {
1769
- const consoleState = await ensureIiiConsole();
1770
- await maybeOfferGlobalInstall();
1771
- printReadyHint(consoleState);
3232
+ await startWorkerForEngineState();
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
+ });
1772
3240
  }
3241
+ await maybeOfferGlobalInstall();
3242
+ printReadyHint();
1773
3243
  writePrefs({ skipSplash: true });
3244
+ if (managedCompose?.kind === "compose") {
3245
+ await new Promise((resolve) => process.stdout.write("", () => resolve()));
3246
+ process.exit(0);
3247
+ }
1774
3248
  }
1775
3249
  async function apiFetch(base, path, timeoutMs = 5e3) {
1776
3250
  try {
1777
- const headers = {};
1778
- const secret = process.env["AGENTMEMORY_SECRET"];
1779
- if (secret) headers["Authorization"] = `Bearer ${secret}`;
3251
+ const headers = bearerHeaders(base);
1780
3252
  return await (await fetch(`${base}/agentmemory/${path}`, {
1781
3253
  signal: AbortSignal.timeout(timeoutMs),
1782
3254
  headers
@@ -1786,10 +3258,29 @@ async function apiFetch(base, path, timeoutMs = 5e3) {
1786
3258
  }
1787
3259
  }
1788
3260
  async function runStatus() {
1789
- getRestPort();
1790
3261
  const base = getBaseUrl();
1791
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));
1792
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
+ }
1793
3284
  p.log.error(`Not running — no response at ${base}`);
1794
3285
  p.log.info("Start with: npx @agentmemory/agentmemory");
1795
3286
  process.exit(1);
@@ -1809,8 +3300,8 @@ async function runStatus() {
1809
3300
  const version = healthRes?.version || "?";
1810
3301
  const sessionList = Array.isArray(sessionsRes?.sessions) ? sessionsRes.sessions : [];
1811
3302
  const sessions = sessionList.length;
1812
- const nodes = Number(graphRes?.totalNodes ?? graphRes?.nodes ?? graphRes?.nodeCount ?? 0);
1813
- 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";
1814
3305
  const cb = healthRes?.circuitBreaker?.state || "closed";
1815
3306
  const heapMB = h?.memory ? Math.round(h.memory.heapUsed / 1048576) : 0;
1816
3307
  const uptime = h?.uptimeSeconds ? Math.round(h.uptimeSeconds) : 0;
@@ -1840,7 +3331,8 @@ async function runStatus() {
1840
3331
  }
1841
3332
  if (flagsRes) {
1842
3333
  const provider = flagsRes.provider === "llm" ? pc.green("✓ llm") : pc.yellow("✗ noop (no key)");
1843
- 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");
1844
3336
  const flagRows = (flagsRes.flags || []).map((f) => ` ${f.enabled ? pc.green("✓") : pc.dim("✗")} ${pc.bold(f.key.padEnd(32))} ${f.label}`);
1845
3337
  lines.push("");
1846
3338
  lines.push(`Provider: ${provider}`);
@@ -1855,6 +3347,21 @@ async function runStatus() {
1855
3347
  lines.push("");
1856
3348
  lines.push(`Followup rate: ${hits}/${total} (${pct}%) within ${followupRes.windowSeconds}s — directional, may overcount on refinement`);
1857
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
+ }
1858
3365
  p.note(lines.join("\n"), "agentmemory");
1859
3366
  } catch (err) {
1860
3367
  p.log.error(err instanceof Error ? err.message : String(err));
@@ -1900,34 +3407,50 @@ function checkClaudeCodeHooks() {
1900
3407
  if (content.includes("Loading hooks from plugin: agentmemory")) return { state: "loaded" };
1901
3408
  return { state: "not-loaded" };
1902
3409
  }
3410
+ function doctorEnvPath() {
3411
+ return process.env["AGENTMEMORY_ENV_FILE"] || join(homedir(), ".agentmemory", ".env");
3412
+ }
1903
3413
  function buildDoctorContext() {
1904
3414
  return {
1905
3415
  baseUrl: getBaseUrl(),
1906
3416
  viewerUrl: getViewerUrl(),
1907
- envPath: join(homedir(), ".agentmemory", ".env"),
1908
- pidfilePath: enginePidfilePath(),
3417
+ envPath: doctorEnvPath(),
3418
+ pidfilePath: readEngineState()?.kind === "compose" ? engineStatePath() : enginePidfilePath(),
1909
3419
  enginePath: engineStatePath(),
1910
3420
  pinnedVersion: IIPINNED_VERSION
1911
3421
  };
1912
3422
  }
1913
3423
  function buildDoctorEffects() {
1914
3424
  return {
1915
- envFileExists: () => existsSync(join(homedir(), ".agentmemory", ".env")),
3425
+ envFileExists: () => existsSync(doctorEnvPath()),
1916
3426
  readEnvFile: () => {
1917
3427
  try {
1918
- return parseEnvFile(readFileSync(join(homedir(), ".agentmemory", ".env"), "utf-8"));
3428
+ return parseEnvFile(readFileSync(doctorEnvPath(), "utf-8"));
1919
3429
  } catch {
1920
3430
  return {};
1921
3431
  }
1922
3432
  },
1923
- pidfileExists: () => existsSync(enginePidfilePath()),
3433
+ processEnv: () => shellEnv,
3434
+ pidfileExists: () => {
3435
+ const state = readEngineState();
3436
+ return state?.kind === "compose" ? Boolean(state.composePid) : existsSync(enginePidfilePath());
3437
+ },
1924
3438
  pidfilePidIsAlive: () => {
3439
+ const state = readEngineState();
3440
+ if (state?.kind === "compose") return state.composePid ? pidAlive(state.composePid) : null;
1925
3441
  const pid = readEnginePidfile();
1926
3442
  if (pid === null) return null;
1927
3443
  return pidAlive(pid);
1928
3444
  },
1929
- findIiiBinary: () => whichBinary("iii"),
1930
- 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
+ },
1931
3454
  iiiBinaryVersion: (binPath) => iiiBinVersion(binPath),
1932
3455
  viewerReachable: async (timeoutMs = 2e3) => {
1933
3456
  try {
@@ -1984,6 +3507,14 @@ function buildDoctorEffects() {
1984
3507
  },
1985
3508
  runStop: async () => {
1986
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
+ }
1987
3518
  const portPids = findEnginePidsByPort(getRestPort());
1988
3519
  const pidfilePid = readEnginePidfile();
1989
3520
  if (portPids.length === 0 && pidfilePid === null) {
@@ -2014,16 +3545,18 @@ function buildDoctorEffects() {
2014
3545
  },
2015
3546
  runStart: async () => {
2016
3547
  try {
2017
- if (!await startEngine()) return {
2018
- ok: false,
2019
- message: "startEngine() returned false"
2020
- };
2021
- 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);
2022
3553
  return {
2023
- ok: ready,
2024
- message: ready ? "Engine ready" : "Engine did not become ready within 15s"
3554
+ ok: true,
3555
+ message: "Engine and agentmemory worker ready"
2025
3556
  };
2026
3557
  } catch (err) {
3558
+ await runStartupCleanup(1);
3559
+ finishStartup();
2027
3560
  return {
2028
3561
  ok: false,
2029
3562
  message: err instanceof Error ? err.message : String(err)
@@ -2052,8 +3585,9 @@ async function passiveServerChecks() {
2052
3585
  apiFetch(base, "graph/stats", 3e3)
2053
3586
  ]);
2054
3587
  const hasLlm = flags?.provider === "llm";
2055
- const hasEmbed = flags?.embeddingProvider === "embeddings";
2056
- 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;
2057
3591
  checks.push({
2058
3592
  name: "Health status",
2059
3593
  ok: health?.status === "healthy",
@@ -2065,7 +3599,7 @@ async function passiveServerChecks() {
2065
3599
  }, {
2066
3600
  name: "Embedding provider",
2067
3601
  ok: hasEmbed,
2068
- hint: hasEmbed ? void 0 : "Running BM25-only. Add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST"
3602
+ hint: hasEmbed ? void 0 : "Running BM25-only. Set EMBEDDING_PROVIDER=local for on-device semantic search, or add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST"
2069
3603
  });
2070
3604
  for (const f of flags?.flags || []) checks.push({
2071
3605
  name: f.label,
@@ -2097,7 +3631,7 @@ async function passiveServerChecks() {
2097
3631
  checks.push({
2098
3632
  name: "Knowledge graph populated",
2099
3633
  ok: graphHas,
2100
- 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."
2101
3635
  });
2102
3636
  return checks;
2103
3637
  }
@@ -2279,7 +3813,10 @@ async function postJson(url, body, timeoutMs = 5e3) {
2279
3813
  try {
2280
3814
  const res = await fetch(url, {
2281
3815
  method: "POST",
2282
- headers: { "Content-Type": "application/json" },
3816
+ headers: {
3817
+ "Content-Type": "application/json",
3818
+ ...bearerHeaders(url)
3819
+ },
2283
3820
  body: JSON.stringify(body),
2284
3821
  signal: AbortSignal.timeout(timeoutMs)
2285
3822
  });
@@ -2292,7 +3829,10 @@ async function postJson(url, body, timeoutMs = 5e3) {
2292
3829
  async function postJsonStrict(url, body, timeoutMs = 5e3) {
2293
3830
  const res = await fetch(url, {
2294
3831
  method: "POST",
2295
- headers: { "Content-Type": "application/json" },
3832
+ headers: {
3833
+ "Content-Type": "application/json",
3834
+ ...bearerHeaders(url)
3835
+ },
2296
3836
  body: JSON.stringify(body),
2297
3837
  signal: AbortSignal.timeout(timeoutMs)
2298
3838
  });
@@ -2327,7 +3867,10 @@ async function seedDemoSession(base, project, session) {
2327
3867
  try {
2328
3868
  const res = await fetch(url, {
2329
3869
  method: "POST",
2330
- headers: { "Content-Type": "application/json" },
3870
+ headers: {
3871
+ "Content-Type": "application/json",
3872
+ ...bearerHeaders(url)
3873
+ },
2331
3874
  body: JSON.stringify(payload),
2332
3875
  signal: AbortSignal.timeout(5e3)
2333
3876
  });
@@ -2340,7 +3883,10 @@ async function seedDemoSession(base, project, session) {
2340
3883
  p.log.warn(`observe request failed for ${obs.toolName}: ${err instanceof Error ? err.message : String(err)}`);
2341
3884
  }
2342
3885
  }
2343
- await postJsonStrict(`${base}/agentmemory/session/end`, { sessionId: session.id });
3886
+ await postJsonStrict(`${base}/agentmemory/session/end`, {
3887
+ sessionId: session.id,
3888
+ final: true
3889
+ });
2344
3890
  return stored;
2345
3891
  }
2346
3892
  async function runDemoSearch(base, query) {
@@ -2399,39 +3945,26 @@ async function runInit() {
2399
3945
  p.outro(`Edit ${target} and you're set.`);
2400
3946
  }
2401
3947
  async function startServerForDemo() {
3948
+ startupTimeouts();
3949
+ await assertRuntimePortOwnership();
2402
3950
  if (await isAgentmemoryReady()) return async () => {};
2403
- const startedEngine = !await isEngineRunning();
3951
+ beginStartup();
3952
+ const startedEngine = !skipEngine && !(await isEngineRunning() || await waitForPersistedNativeEngine());
2404
3953
  if (startedEngine) {
2405
3954
  if (!await startEngine()) {
2406
3955
  p.log.error("Could not start iii-engine for the demo.");
2407
3956
  p.note(installInstructions().join("\n"), "Setup required");
2408
- process.exit(1);
3957
+ throw new Error("iii-engine could not be started for the demo.");
2409
3958
  }
2410
- if (!await waitForEngine(15e3)) {
2411
- p.log.error("iii-engine did not become ready within 15s.");
2412
- process.exit(1);
3959
+ if (!await waitForEngine()) {
3960
+ printCapturedStartupStderr();
3961
+ throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
2413
3962
  }
2414
3963
  }
2415
- await import("./src-G7yt8gGm.mjs");
2416
- if (!await waitForAgentmemoryReady(15e3)) {
2417
- p.log.error("agentmemory worker did not become ready within 15s.");
2418
- process.exit(1);
2419
- }
3964
+ await startWorkerForEngineState();
2420
3965
  return async () => {
2421
- if (!startedEngine) return;
2422
- const port = getRestPort();
2423
- const state = readEngineState();
2424
- if (state?.kind === "docker") {
2425
- await stopDockerEngine(state.composeFile, port).catch(() => {});
2426
- return;
2427
- }
2428
- const pids = new Set(findEnginePidsByPort(port));
2429
- const pidfilePid = readEnginePidfile();
2430
- if (pidfilePid) pids.add(pidfilePid);
2431
- for (const pid of pids) await signalAndWait(pid, "SIGTERM", 3e3).catch(() => {});
2432
- clearEnginePidfile();
2433
- clearEngineState();
2434
- clearWorkerPidfile();
3966
+ if (importedStartupWorker && readWorkerPidfile() === process.pid) clearWorkerPidfile();
3967
+ if (startedEngine) await cleanupFailedStartup();
2435
3968
  };
2436
3969
  }
2437
3970
  async function runDemo() {
@@ -2482,8 +4015,8 @@ async function runDemoBody(base) {
2482
4015
  "",
2483
4016
  ...semanticHits > 0 ? [c.accent(`Notice: searching "database performance optimization"`), c.accent(`found the N+1 query fix — keyword matching can't do that.`)] : [
2484
4017
  c.dim(`Note: "database performance optimization" found nothing —`),
2485
- c.dim(`semantic recall needs an embedding provider key (e.g.`),
2486
- c.dim(`OPENAI_API_KEY or GEMINI_API_KEY in ~/.agentmemory/.env).`)
4018
+ c.dim(`set EMBEDDING_PROVIDER=local for on-device semantic recall,`),
4019
+ c.dim(`or add an embedding API key in ~/.agentmemory/.env.`)
2487
4020
  ],
2488
4021
  "",
2489
4022
  `Viewer: ${c.url(getViewerUrl())}`,
@@ -2498,7 +4031,11 @@ function runCommand(command, commandArgs, options = { label: "command" }) {
2498
4031
  const result = spawnSync(command, commandArgs, {
2499
4032
  cwd: options.cwd || process.cwd(),
2500
4033
  stdio: "pipe",
2501
- encoding: "utf-8"
4034
+ encoding: "utf-8",
4035
+ ...options.timeoutMs !== void 0 ? {
4036
+ timeout: options.timeoutMs,
4037
+ killSignal: "SIGKILL"
4038
+ } : {}
2502
4039
  });
2503
4040
  if (result.status === 0) {
2504
4041
  spinner.stop(`${options.label} ${pc.green("✓")}`);
@@ -2523,7 +4060,6 @@ async function runUpgrade() {
2523
4060
  const hasPnpmLock = existsSync(join(cwd, "pnpm-lock.yaml"));
2524
4061
  const pnpmBin = whichBinary("pnpm");
2525
4062
  const npmBin = whichBinary("npm");
2526
- const dockerBin = whichBinary("docker");
2527
4063
  p.log.info(`Working directory: ${cwd}`);
2528
4064
  const requireSuccess = (ok, label) => {
2529
4065
  if (!ok) {
@@ -2533,14 +4069,22 @@ async function runUpgrade() {
2533
4069
  };
2534
4070
  if (hasPackageJson) if (!!pnpmBin && hasPnpmLock && pnpmBin) {
2535
4071
  requireSuccess(runCommand(pnpmBin, ["install"], { label: "Refreshing dependencies (pnpm install)" }), "pnpm install");
2536
- runCommand(pnpmBin, ["up", "iii-sdk@0.11.2"], {
2537
- label: "Pinning iii-sdk@0.11.2",
4072
+ runCommand(pnpmBin, [
4073
+ "up",
4074
+ `iii-sdk@${IIPINNED_DEFAULT_VERSION}`,
4075
+ `@iii-dev/helpers@${IIPINNED_DEFAULT_VERSION}`
4076
+ ], {
4077
+ label: `Pinning iii-sdk and @iii-dev/helpers at ${IIPINNED_DEFAULT_VERSION}`,
2538
4078
  optional: true
2539
4079
  });
2540
4080
  } else if (npmBin) {
2541
4081
  requireSuccess(runCommand(npmBin, ["install"], { label: "Refreshing dependencies (npm install)" }), "npm install");
2542
- runCommand(npmBin, ["install", "iii-sdk@0.11.2"], {
2543
- label: "Pinning iii-sdk@0.11.2",
4082
+ runCommand(npmBin, [
4083
+ "install",
4084
+ `iii-sdk@${IIPINNED_DEFAULT_VERSION}`,
4085
+ `@iii-dev/helpers@${IIPINNED_DEFAULT_VERSION}`
4086
+ ], {
4087
+ label: `Pinning iii-sdk and @iii-dev/helpers at ${IIPINNED_DEFAULT_VERSION}`,
2544
4088
  optional: true
2545
4089
  });
2546
4090
  } else p.log.warn("No package manager found (pnpm/npm). Skipping JS dependency upgrade.");
@@ -2555,11 +4099,6 @@ async function runUpgrade() {
2555
4099
  }
2556
4100
  if (upgradeEngine === true) await runIiiInstaller();
2557
4101
  else p.log.info("Skipped iii-engine installer.");
2558
- if (dockerBin) runCommand(dockerBin, ["pull", `iiidev/iii:${IIPINNED_VERSION}`], {
2559
- label: `Pulling iii Docker image v${IIPINNED_VERSION} (pinned)`,
2560
- optional: true
2561
- });
2562
- else p.log.info("Docker not found. Skipping Docker image refresh.");
2563
4102
  p.note([
2564
4103
  "Upgrade flow completed.",
2565
4104
  "",
@@ -2572,6 +4111,22 @@ async function runUpgrade() {
2572
4111
  function pidAlive(pid) {
2573
4112
  try {
2574
4113
  process.kill(pid, 0);
4114
+ if (!IS_WINDOWS) try {
4115
+ const stat = execFileSync("ps", [
4116
+ "-p",
4117
+ String(pid),
4118
+ "-o",
4119
+ "stat="
4120
+ ], {
4121
+ encoding: "utf-8",
4122
+ stdio: [
4123
+ "ignore",
4124
+ "pipe",
4125
+ "ignore"
4126
+ ]
4127
+ });
4128
+ if (stat.trim() && !processStatIsRunning(stat)) return false;
4129
+ } catch {}
2575
4130
  return true;
2576
4131
  } catch (err) {
2577
4132
  return err?.code === "EPERM";
@@ -2606,6 +4161,28 @@ async function signalAndWait(pid, initialSignal, timeoutMs) {
2606
4161
  await new Promise((r) => setTimeout(r, 200));
2607
4162
  return !pidAlive(pid);
2608
4163
  }
4164
+ async function waitForEngineFlush() {
4165
+ let stateBackend = "file";
4166
+ try {
4167
+ stateBackend = getStateBackend();
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
+ }
4174
+ const runtimePath = runtimeConfigPath(dataDirResolution.dataDir);
4175
+ const configTexts = [];
4176
+ for (const path of engineStateConfigPaths(dataDirResolution.dataDir, runtimePath)) try {
4177
+ configTexts.push(readFileSync(path, "utf-8"));
4178
+ } catch {}
4179
+ const waitMs = engineFlushWaitMs(stateBackend, configTexts);
4180
+ if (waitMs <= 0) return;
4181
+ const s = p.spinner();
4182
+ s.start(`Waiting ${(waitMs / 1e3).toFixed(1)}s for iii-engine to write state to disk...`);
4183
+ await new Promise((r) => setTimeout(r, waitMs));
4184
+ s.stop("iii-engine state flush window passed");
4185
+ }
2609
4186
  async function stopWorkerPid(pid, graceMs) {
2610
4187
  const s = p.spinner();
2611
4188
  s.start(`Stopping agentmemory worker (pid ${pid})... [flushing state]`);
@@ -2613,6 +4190,46 @@ async function stopWorkerPid(pid, graceMs) {
2613
4190
  s.stop(ok ? `Stopped worker pid ${pid}` : `Failed to stop worker pid ${pid}`);
2614
4191
  return ok;
2615
4192
  }
4193
+ async function stopNativeEngineForRemoval() {
4194
+ const port = getRestPort();
4195
+ const workerPid = readWorkerPidfile();
4196
+ const enginePids = new Set(findEnginePidsByPort(port));
4197
+ const pidfilePid = readEnginePidfile();
4198
+ if (pidfilePid) enginePids.add(pidfilePid);
4199
+ if (workerPid) enginePids.delete(workerPid);
4200
+ const foreign = [...enginePids].map((pid) => ({
4201
+ pid,
4202
+ comm: pidCommand(pid)
4203
+ })).filter(({ comm }) => isForeignPortHolder(comm));
4204
+ if (foreign.length > 0) return {
4205
+ ok: false,
4206
+ reason: `refusing to signal non-iii process(es): ${foreign.map(({ pid, comm }) => `${pid} (${comm})`).join(", ")}`
4207
+ };
4208
+ if (workerPid) {
4209
+ if (!await stopWorkerPid(workerPid, 5e3)) return {
4210
+ ok: false,
4211
+ reason: `worker pid ${workerPid} could not be stopped; no engine or files were removed`
4212
+ };
4213
+ clearWorkerPidfile();
4214
+ }
4215
+ for (const pid of enginePids) {
4216
+ const s = p.spinner();
4217
+ s.start(`Stopping iii-engine (pid ${pid})...`);
4218
+ const ok = await signalAndWait(pid, "SIGTERM", 3e3);
4219
+ s.stop(ok ? `Stopped pid ${pid}` : `Failed to stop pid ${pid}`);
4220
+ if (!ok) return {
4221
+ ok: false,
4222
+ reason: `engine pid ${pid} survived shutdown; lifecycle state and files were preserved`
4223
+ };
4224
+ }
4225
+ clearEnginePidfile();
4226
+ clearEngineState();
4227
+ clearWorkerPidfile();
4228
+ return {
4229
+ ok: true,
4230
+ stoppedEnginePids: enginePids.size
4231
+ };
4232
+ }
2616
4233
  function pidCommand(pid) {
2617
4234
  if (IS_WINDOWS) return "";
2618
4235
  try {
@@ -2662,65 +4279,64 @@ function findEnginePidsByPort(port) {
2662
4279
  return [];
2663
4280
  }
2664
4281
  }
2665
- async function stopDockerEngine(composeFile, port) {
2666
- const dockerBin = whichBinary("docker");
2667
- if (!dockerBin) {
2668
- p.log.error(`Engine was started via Docker compose, but \`docker\` is no longer on PATH. Stop it manually:\n docker compose -f ${composeFile} down`);
2669
- process.exit(1);
2670
- }
2671
- if (!existsSync(composeFile)) {
2672
- p.log.error(`Engine state references ${composeFile}, but the file is gone. Stop it manually:\n docker compose down (from the dir holding the original docker-compose.yml)`);
2673
- process.exit(1);
2674
- }
2675
- const workerPid = readWorkerPidfile();
2676
- let workerStopped = true;
2677
- if (workerPid) workerStopped = await stopWorkerPid(workerPid, 5e3);
2678
- let composeText = "";
2679
- try {
2680
- composeText = readFileSync(composeFile, "utf-8");
2681
- } catch {
2682
- composeText = "";
2683
- }
2684
- const ownServices = ["iii-engine", "iii-init"].filter((svc) => new RegExp(`^\\s+${svc}:`, "m").test(composeText));
2685
- if (ownServices.length === 0) {
2686
- p.log.error(`${composeFile} does not define the agentmemory services (iii-engine/iii-init). Refusing to run an unscoped \`docker compose down\` against it — that would tear down every service in the file.\n\nStop the engine service manually:\n docker compose -f ${composeFile} stop <service>`);
4282
+ async function runStop() {
4283
+ const savedState = readEngineState();
4284
+ if (savedState?.kind === "docker") {
4285
+ p.intro("agentmemory stop");
4286
+ p.log.error(legacyDockerStateMessage(savedState));
2687
4287
  process.exit(1);
2688
4288
  }
2689
- const ok = runCommand(dockerBin, [
2690
- "compose",
2691
- "-f",
2692
- composeFile,
2693
- "rm",
2694
- "-s",
2695
- "-f",
2696
- ...ownServices
2697
- ], { label: `docker compose -f ${composeFile} rm -s -f ${ownServices.join(" ")}` });
2698
- if (workerStopped) clearWorkerPidfile();
2699
- if (ok) {
2700
- clearEnginePidfile();
2701
- clearEngineState();
2702
- } else {
2703
- p.log.error(`docker compose rm failed. The engine may still be running on :${port}. Inspect with:\n docker compose -f ${composeFile} 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.`);
2704
4292
  process.exit(1);
2705
4293
  }
2706
- p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
2707
- }
2708
- async function runStop() {
4294
+ await assertRuntimePortOwnership();
2709
4295
  p.intro("agentmemory stop");
2710
4296
  const port = getRestPort();
2711
4297
  const state = readEngineState();
2712
4298
  const running = await isEngineRunning();
2713
4299
  const force = args.includes("--force");
2714
- if (state?.kind === "docker") {
2715
- if (!running) {
2716
- p.log.info(`No engine responding on port ${port}.`);
2717
- clearEnginePidfile();
2718
- clearEngineState();
2719
- clearWorkerPidfile();
2720
- p.outro("Nothing to stop.");
2721
- return;
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);
2722
4309
  }
2723
- await stopDockerEngine(state.composeFile, port);
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.");
2724
4340
  return;
2725
4341
  }
2726
4342
  const portPids = findEnginePidsByPort(port);
@@ -2737,7 +4353,7 @@ async function runStop() {
2737
4353
  if (workerPid !== null && portPids.length === 0 && pidfilePid === null) {
2738
4354
  const s = p.spinner();
2739
4355
  s.start(`Stopping orphaned agentmemory worker (pid ${workerPid})...`);
2740
- const ok = await signalAndWait(workerPid, "SIGTERM", 3e3);
4356
+ const ok = await signalAndWait(workerPid, "SIGTERM", SHUTDOWN_HARD_EXIT_MS + 1e3);
2741
4357
  s.stop(ok ? `Stopped worker pid ${workerPid}` : `Failed to stop worker pid ${workerPid}`);
2742
4358
  clearEnginePidfile();
2743
4359
  clearEngineState();
@@ -2756,14 +4372,6 @@ async function runStop() {
2756
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}`);
2757
4373
  process.exit(1);
2758
4374
  }
2759
- if (!state) {
2760
- const compose = discoverComposeFile();
2761
- if (compose && pidfilePid === null) if (force) p.log.warn(`--force: bypassing Docker-heuristic guard. Falling back to native pidfile + lsof on :${port}.`);
2762
- else {
2763
- 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.`);
2764
- process.exit(1);
2765
- }
2766
- }
2767
4375
  const candidates = /* @__PURE__ */ new Set();
2768
4376
  if (pidfilePid) candidates.add(pidfilePid);
2769
4377
  for (const pid of portPids) candidates.add(pid);
@@ -2774,7 +4382,8 @@ async function runStop() {
2774
4382
  process.exit(1);
2775
4383
  }
2776
4384
  let allStopped = true;
2777
- for (const pid of workerCandidates) if (!await stopWorkerPid(pid, 5e3)) allStopped = false;
4385
+ for (const pid of workerCandidates) if (!await stopWorkerPid(pid, 9e3)) allStopped = false;
4386
+ if (workerCandidates.size > 0 && [...candidates].some((pid) => !workerCandidates.has(pid))) await waitForEngineFlush();
2778
4387
  const skippedForeign = [];
2779
4388
  for (const pid of candidates) {
2780
4389
  if (workerCandidates.has(pid)) continue;
@@ -2797,7 +4406,7 @@ async function runStop() {
2797
4406
  clearWorkerPidfile();
2798
4407
  if (skippedForeign.length > 0) {
2799
4408
  const list = skippedForeign.map((sf) => ` pid ${sf.pid} ${sf.comm}`).join("\n");
2800
- 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.`);
2801
4410
  process.exit(1);
2802
4411
  }
2803
4412
  if (!allStopped) {
@@ -2807,10 +4416,10 @@ async function runStop() {
2807
4416
  p.outro("Stopped. Memories persisted to disk; restart anytime with: npx @agentmemory/agentmemory");
2808
4417
  }
2809
4418
  async function runMcp() {
2810
- await import("./standalone-BqNJDJ04.mjs");
4419
+ await import("./standalone-BlqshyZ5.mjs");
2811
4420
  }
2812
4421
  async function runConnectCmd() {
2813
- const { runConnect } = await import("./connect-Qaeq15TQ.mjs").then((n) => n.n);
4422
+ const { runConnect } = await import("./connect-BSLIAMpw.mjs").then((n) => n.n);
2814
4423
  await runConnect(args.slice(1));
2815
4424
  }
2816
4425
  async function runImportJsonl() {
@@ -2867,11 +4476,12 @@ async function runImportJsonl() {
2867
4476
  process.exit(1);
2868
4477
  }
2869
4478
  const body = {};
2870
- if (pathArg) body["path"] = pathArg;
4479
+ if (pathArg) body["path"] = resolve(pathArg.startsWith("~") ? join(homedir(), pathArg.slice(1)) : pathArg);
2871
4480
  if (maxFiles !== void 0) body["maxFiles"] = maxFiles;
2872
- const headers = { "content-type": "application/json" };
2873
- const secret = process.env["AGENTMEMORY_SECRET"];
2874
- if (secret) headers["authorization"] = `Bearer ${secret}`;
4481
+ const headers = {
4482
+ "content-type": "application/json",
4483
+ ...bearerHeaders(base)
4484
+ };
2875
4485
  p.log.info(`Importing JSONL from ${pathArg || "~/.claude/projects"}…`);
2876
4486
  const spinner = p.spinner();
2877
4487
  spinner.start("scanning files");
@@ -2958,20 +4568,39 @@ function safeDelete(path) {
2958
4568
  }
2959
4569
  }
2960
4570
  async function runRemove() {
4571
+ if (selectedInstance !== 0) {
4572
+ p.intro("agentmemory remove");
4573
+ p.log.error(`remove is a global uninstall and cannot target instance ${selectedInstance}. Stop that daemon with \`agentmemory stop --instance ${selectedInstance}\`, then run \`agentmemory remove\` without --instance when you intend to uninstall shared assets.`);
4574
+ process.exit(1);
4575
+ }
4576
+ await assertRuntimePortOwnership();
2961
4577
  p.intro("agentmemory remove");
2962
4578
  const force = args.includes("--force");
2963
4579
  const keepData = args.includes("--keep-data");
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
+ }
4585
+ if (ownedState?.kind === "docker") {
4586
+ p.log.error(legacyDockerStateMessage(ownedState));
4587
+ process.exit(1);
4588
+ }
2964
4589
  const home = homedir();
2965
4590
  const connectManifest = loadConnectManifest(home);
4591
+ const localBinIiiVersion = probeLocalBinIiiVersion(home);
4592
+ const options = {
4593
+ force,
4594
+ keepData
4595
+ };
2966
4596
  const plan = buildRemovePlan({
2967
4597
  home,
4598
+ runtimeDir: dirname(engineStatePath()),
4599
+ dataDir: dataDirResolution.dataDir,
2968
4600
  pinnedVersion: IIPINNED_VERSION,
2969
- localBinIiiVersion: probeLocalBinIiiVersion(home),
4601
+ localBinIiiVersion,
2970
4602
  connectManifest
2971
- }, {
2972
- force,
2973
- keepData
2974
- });
4603
+ }, options);
2975
4604
  if (plan.filter((it) => it.applicable).length === 0) {
2976
4605
  p.outro("Nothing to remove. agentmemory is already gone.");
2977
4606
  return;
@@ -3008,19 +4637,12 @@ async function runRemove() {
3008
4637
  }
3009
4638
  }
3010
4639
  if (item.id === "stop-engine") {
3011
- try {
3012
- const portPids = findEnginePidsByPort(getRestPort());
3013
- const pidfilePid = readEnginePidfile();
3014
- const cands = /* @__PURE__ */ new Set();
3015
- if (pidfilePid) cands.add(pidfilePid);
3016
- for (const pid of portPids) cands.add(pid);
3017
- for (const pid of cands) await signalAndWait(pid, "SIGTERM", 3e3);
3018
- clearEnginePidfile();
3019
- clearEngineState();
3020
- p.log.success(cands.size > 0 ? `stopped engine (${cands.size} pid${cands.size === 1 ? "" : "s"})` : "no engine running");
3021
- } catch (err) {
3022
- p.log.warn(`engine stop best-effort: ${err instanceof Error ? err.message : String(err)}`);
4640
+ const stopped = await stopNativeEngineForRemoval();
4641
+ if (!stopped.ok) {
4642
+ p.log.error(`Removal aborted: ${stopped.reason}.`);
4643
+ process.exit(1);
3023
4644
  }
4645
+ p.log.success(stopped.stoppedEnginePids > 0 ? `stopped engine (${stopped.stoppedEnginePids} pid${stopped.stoppedEnginePids === 1 ? "" : "s"})` : "no engine running");
3024
4646
  continue;
3025
4647
  }
3026
4648
  if (!item.path) continue;
@@ -3030,26 +4652,65 @@ async function runRemove() {
3030
4652
  }
3031
4653
  p.outro("Done. agentmemory cleanly removed. The npm package itself: npm uninstall -g @agentmemory/agentmemory");
3032
4654
  }
4655
+ async function runConsole() {
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");
4663
+ }
4664
+ async function runCapture() {
4665
+ const code = await runCaptureCommand({
4666
+ base: getBaseUrl(),
4667
+ args: args.slice(1),
4668
+ secret: resolveClientSecret(getBaseUrl())
4669
+ });
4670
+ process.exit(code);
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
+ }
3033
4689
  const commands = {
3034
4690
  init: runInit,
3035
4691
  connect: runConnectCmd,
3036
4692
  status: runStatus,
4693
+ console: runConsole,
3037
4694
  doctor: runDoctor,
3038
4695
  demo: runDemo,
3039
4696
  upgrade: runUpgrade,
3040
4697
  stop: runStop,
3041
4698
  remove: runRemove,
3042
4699
  mcp: runMcp,
3043
- "import-jsonl": runImportJsonl
4700
+ "import-jsonl": runImportJsonl,
4701
+ capture: runCapture,
4702
+ compression: runCompression,
4703
+ "graph-index": runGraphIndex
3044
4704
  };
3045
4705
  const first = args[0] ?? "";
3046
4706
  async function unknownCommand() {
3047
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.`);
3048
4708
  process.exit(1);
3049
4709
  }
3050
- (commands[first] ?? (first && !first.startsWith("-") ? unknownCommand : main))().catch((err) => {
4710
+ (commands[first] ?? (first && !first.startsWith("-") ? unknownCommand : main))().catch(async (err) => {
3051
4711
  p.log.error(err instanceof Error ? err.message : String(err));
3052
- process.exit(1);
4712
+ const exitCode = await runStartupCleanup(1).catch(() => 1);
4713
+ process.exit(exitCode);
3053
4714
  });
3054
4715
  //#endregion
3055
4716
  export { discoverViewerPort };