@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.
- package/.env.example +102 -35
- package/AGENTS.md +19 -26
- package/README.md +417 -525
- package/dist/.env.example +102 -35
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +2316 -655
- package/dist/cli.mjs.map +1 -1
- package/dist/compression-recovery-CnhLQC6N.mjs +5912 -0
- package/dist/compression-recovery-CnhLQC6N.mjs.map +1 -0
- package/dist/{connect-Qaeq15TQ.mjs → connect-BSLIAMpw.mjs} +900 -302
- package/dist/connect-BSLIAMpw.mjs.map +1 -0
- package/dist/hooks/_capture.d.mts +25 -0
- package/dist/hooks/_capture.d.mts.map +1 -0
- package/dist/hooks/_capture.mjs +944 -0
- package/dist/hooks/_capture.mjs.map +1 -0
- package/dist/hooks/antigravity-bridge.d.mts +1 -1
- package/dist/hooks/antigravity-bridge.d.mts.map +1 -1
- package/dist/hooks/antigravity-bridge.mjs +24 -15
- package/dist/hooks/antigravity-bridge.mjs.map +1 -1
- package/dist/hooks/notification.mjs +35 -29
- package/dist/hooks/notification.mjs.map +1 -1
- package/dist/hooks/post-commit.mjs +91 -1
- package/dist/hooks/post-commit.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +71 -29
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +99 -39
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +113 -6
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +91 -1
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/prompt-submit.mjs +25 -25
- package/dist/hooks/prompt-submit.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +67 -31
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +110 -6
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +80 -8
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-start.mjs +28 -29
- package/dist/hooks/subagent-start.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +33 -29
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +31 -31
- package/dist/hooks/task-completed.mjs.map +1 -1
- package/dist/{image-refs-BKLmp_sD.mjs → image-refs-BaT-9oMU.mjs} +2 -2
- package/dist/image-refs-BaT-9oMU.mjs.map +1 -0
- package/dist/{image-refs-CJgZmb9X.mjs → image-refs-Cyta4Lf9.mjs} +3 -3
- package/dist/image-refs-Cyta4Lf9.mjs.map +1 -0
- package/dist/{keyed-mutex-DTHtcZrt.mjs → image-store-BsN4vyjr.mjs} +2 -14
- package/dist/image-store-BsN4vyjr.mjs.map +1 -0
- package/dist/index.mjs +15180 -4119
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/dsh.d.mts +55 -0
- package/dist/integrations/dsh.d.mts.map +1 -0
- package/dist/integrations/dsh.mjs +1145 -0
- package/dist/integrations/dsh.mjs.map +1 -0
- package/dist/{schema-BFZhNTF7.mjs → keyed-mutex-DCUh7PG-.mjs} +81 -8
- package/dist/keyed-mutex-DCUh7PG-.mjs.map +1 -0
- package/dist/secret-store-D9GwO0xp.mjs +2449 -0
- package/dist/secret-store-D9GwO0xp.mjs.map +1 -0
- package/dist/session-arguments-zxsbbd8y.mjs +113 -0
- package/dist/session-arguments-zxsbbd8y.mjs.map +1 -0
- package/dist/{src-G7yt8gGm.mjs → src-DaMtDzt_.mjs} +8320 -4260
- package/dist/src-DaMtDzt_.mjs.map +1 -0
- package/dist/{standalone-BqNJDJ04.mjs → standalone-BlqshyZ5.mjs} +246 -62
- package/dist/standalone-BlqshyZ5.mjs.map +1 -0
- package/dist/standalone.d.mts +1 -1
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +866 -78
- package/dist/standalone.mjs.map +1 -1
- package/dist/viewer/index.html +7394 -2816
- package/integrations/hermes/README.md +84 -0
- package/integrations/hermes/__init__.py +669 -0
- package/integrations/hermes/plugin.yaml +27 -0
- package/integrations/omp/LICENSE +190 -0
- package/integrations/omp/README.md +76 -0
- package/integrations/omp/capture.ts +1 -0
- package/integrations/omp/dist/index.mjs +1450 -0
- package/integrations/omp/index.ts +488 -0
- package/integrations/omp/package.json +13 -0
- package/integrations/omp/security.ts +67 -0
- package/integrations/omp/skills/agentmemory-agents/REFERENCE.md +32 -0
- package/integrations/omp/skills/agentmemory-agents/SKILL.md +48 -0
- package/integrations/omp/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-architecture/SKILL.md +33 -0
- package/integrations/omp/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-config/REFERENCE.md +90 -0
- package/integrations/omp/skills/agentmemory-config/SKILL.md +78 -0
- package/integrations/omp/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-hooks/REFERENCE.md +20 -0
- package/integrations/omp/skills/agentmemory-hooks/SKILL.md +49 -0
- package/integrations/omp/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
- package/integrations/omp/skills/agentmemory-mcp-tools/SKILL.md +42 -0
- package/integrations/omp/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +158 -0
- package/integrations/omp/skills/agentmemory-rest-api/SKILL.md +49 -0
- package/integrations/omp/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/commit-context/EXAMPLES.md +71 -0
- package/integrations/omp/skills/commit-context/SKILL.md +64 -0
- package/integrations/omp/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/commit-history/EXAMPLES.md +71 -0
- package/integrations/omp/skills/commit-history/SKILL.md +62 -0
- package/integrations/omp/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/forget/EXAMPLES.md +40 -0
- package/integrations/omp/skills/forget/SKILL.md +73 -0
- package/integrations/omp/skills/forget/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/handoff/EXAMPLES.md +57 -0
- package/integrations/omp/skills/handoff/SKILL.md +75 -0
- package/integrations/omp/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/lesson/SKILL.md +64 -0
- package/integrations/omp/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/memory-discipline/SKILL.md +65 -0
- package/integrations/omp/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/recall/EXAMPLES.md +74 -0
- package/integrations/omp/skills/recall/SKILL.md +74 -0
- package/integrations/omp/skills/recall/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/recap/EXAMPLES.md +62 -0
- package/integrations/omp/skills/recap/SKILL.md +68 -0
- package/integrations/omp/skills/recap/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/remember/EXAMPLES.md +67 -0
- package/integrations/omp/skills/remember/SKILL.md +66 -0
- package/integrations/omp/skills/remember/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/session-history/EXAMPLES.md +59 -0
- package/integrations/omp/skills/session-history/SKILL.md +67 -0
- package/integrations/omp/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/write-agentmemory-skill/SKILL.md +41 -0
- package/integrations/omp/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/LICENSE +190 -0
- package/integrations/openclaw/README.md +88 -0
- package/integrations/openclaw/openclaw.plugin.json +71 -0
- package/integrations/openclaw/package.json +38 -0
- package/integrations/openclaw/plugin.mjs +425 -0
- package/integrations/openclaw/plugin.yaml +26 -0
- package/integrations/openclaw/skills/agentmemory-agents/REFERENCE.md +32 -0
- package/integrations/openclaw/skills/agentmemory-agents/SKILL.md +48 -0
- package/integrations/openclaw/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-architecture/SKILL.md +33 -0
- package/integrations/openclaw/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +90 -0
- package/integrations/openclaw/skills/agentmemory-config/SKILL.md +78 -0
- package/integrations/openclaw/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-hooks/REFERENCE.md +20 -0
- package/integrations/openclaw/skills/agentmemory-hooks/SKILL.md +49 -0
- package/integrations/openclaw/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
- package/integrations/openclaw/skills/agentmemory-mcp-tools/SKILL.md +42 -0
- package/integrations/openclaw/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +158 -0
- package/integrations/openclaw/skills/agentmemory-rest-api/SKILL.md +49 -0
- package/integrations/openclaw/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/commit-context/EXAMPLES.md +71 -0
- package/integrations/openclaw/skills/commit-context/SKILL.md +64 -0
- package/integrations/openclaw/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/commit-history/EXAMPLES.md +71 -0
- package/integrations/openclaw/skills/commit-history/SKILL.md +62 -0
- package/integrations/openclaw/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/forget/EXAMPLES.md +40 -0
- package/integrations/openclaw/skills/forget/SKILL.md +73 -0
- package/integrations/openclaw/skills/forget/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/handoff/EXAMPLES.md +57 -0
- package/integrations/openclaw/skills/handoff/SKILL.md +75 -0
- package/integrations/openclaw/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/lesson/SKILL.md +64 -0
- package/integrations/openclaw/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/memory-discipline/SKILL.md +65 -0
- package/integrations/openclaw/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/recall/EXAMPLES.md +74 -0
- package/integrations/openclaw/skills/recall/SKILL.md +74 -0
- package/integrations/openclaw/skills/recall/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/recap/EXAMPLES.md +62 -0
- package/integrations/openclaw/skills/recap/SKILL.md +68 -0
- package/integrations/openclaw/skills/recap/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/remember/EXAMPLES.md +67 -0
- package/integrations/openclaw/skills/remember/SKILL.md +66 -0
- package/integrations/openclaw/skills/remember/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/session-history/EXAMPLES.md +59 -0
- package/integrations/openclaw/skills/session-history/SKILL.md +67 -0
- package/integrations/openclaw/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/write-agentmemory-skill/SKILL.md +41 -0
- package/integrations/openclaw/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/LICENSE +190 -0
- package/integrations/pi/README.md +45 -51
- package/integrations/pi/dist/index.mjs +1379 -0
- package/integrations/pi/index.ts +139 -173
- package/integrations/pi/package.json +16 -6
- package/integrations/pi/security.ts +54 -22
- package/integrations/pi/skills/agentmemory-agents/REFERENCE.md +32 -0
- package/integrations/pi/skills/agentmemory-agents/SKILL.md +48 -0
- package/integrations/pi/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-architecture/SKILL.md +33 -0
- package/integrations/pi/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-config/REFERENCE.md +90 -0
- package/integrations/pi/skills/agentmemory-config/SKILL.md +78 -0
- package/integrations/pi/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-hooks/REFERENCE.md +20 -0
- package/integrations/pi/skills/agentmemory-hooks/SKILL.md +49 -0
- package/integrations/pi/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
- package/integrations/pi/skills/agentmemory-mcp-tools/SKILL.md +42 -0
- package/integrations/pi/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +158 -0
- package/integrations/pi/skills/agentmemory-rest-api/SKILL.md +49 -0
- package/integrations/pi/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/commit-context/EXAMPLES.md +71 -0
- package/integrations/pi/skills/commit-context/SKILL.md +64 -0
- package/integrations/pi/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/commit-history/EXAMPLES.md +71 -0
- package/integrations/pi/skills/commit-history/SKILL.md +62 -0
- package/integrations/pi/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/forget/EXAMPLES.md +40 -0
- package/integrations/pi/skills/forget/SKILL.md +73 -0
- package/integrations/pi/skills/forget/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/handoff/EXAMPLES.md +57 -0
- package/integrations/pi/skills/handoff/SKILL.md +75 -0
- package/integrations/pi/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/lesson/SKILL.md +64 -0
- package/integrations/pi/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/memory-discipline/SKILL.md +65 -0
- package/integrations/pi/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/recall/EXAMPLES.md +74 -0
- package/integrations/pi/skills/recall/SKILL.md +74 -0
- package/integrations/pi/skills/recall/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/recap/EXAMPLES.md +62 -0
- package/integrations/pi/skills/recap/SKILL.md +68 -0
- package/integrations/pi/skills/recap/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/remember/EXAMPLES.md +67 -0
- package/integrations/pi/skills/remember/SKILL.md +66 -0
- package/integrations/pi/skills/remember/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/session-history/EXAMPLES.md +59 -0
- package/integrations/pi/skills/session-history/SKILL.md +67 -0
- package/integrations/pi/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/write-agentmemory-skill/SKILL.md +41 -0
- package/integrations/pi/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/package.json +42 -12
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +27 -4
- package/plugin/.devin-plugin/plugin.json +5 -3
- package/plugin/.mcp.codex.json +12 -0
- package/plugin/.mcp.copilot.json +2 -7
- package/plugin/.mcp.json +2 -7
- package/plugin/assets/icon.svg +35 -0
- package/plugin/cursor/hooks.json +23 -8
- package/plugin/hooks/hooks.copilot.json +11 -11
- package/plugin/hooks.json +67 -0
- package/plugin/opencode/README.md +267 -31
- package/plugin/opencode/agentmemory-capture.ts +594 -11
- package/plugin/opencode/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
- package/plugin/scripts/_capture.d.mts +25 -0
- package/plugin/scripts/_capture.d.mts.map +1 -0
- package/plugin/scripts/_capture.mjs +944 -0
- package/plugin/scripts/_capture.mjs.map +1 -0
- package/plugin/scripts/antigravity-bridge.d.mts +1 -1
- package/plugin/scripts/antigravity-bridge.d.mts.map +1 -1
- package/plugin/scripts/antigravity-bridge.mjs +24 -15
- package/plugin/scripts/antigravity-bridge.mjs.map +1 -1
- package/plugin/scripts/cursor/run-detached.d.mts +1 -0
- package/plugin/scripts/cursor/run-detached.mjs +786 -0
- package/plugin/scripts/cursor/run-detached.mjs.map +1 -0
- package/plugin/scripts/cursor/run-hook.d.mts +1 -0
- package/plugin/scripts/cursor/run-hook.mjs +704 -0
- package/plugin/scripts/cursor/run-hook.mjs.map +1 -0
- package/plugin/scripts/notification.mjs +35 -29
- package/plugin/scripts/notification.mjs.map +1 -1
- package/plugin/scripts/plugin-bridge.mjs +409 -0
- package/plugin/scripts/post-commit.mjs +91 -1
- package/plugin/scripts/post-commit.mjs.map +1 -1
- package/plugin/scripts/post-tool-failure.mjs +71 -29
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +99 -39
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +113 -6
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +91 -1
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/prompt-submit.mjs +25 -25
- package/plugin/scripts/prompt-submit.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +67 -31
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +110 -6
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +80 -8
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-start.mjs +28 -29
- package/plugin/scripts/subagent-start.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +33 -29
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +31 -31
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/plugin/skills/_shared/TROUBLESHOOTING.md +42 -12
- package/plugin/skills/agentmemory-agents/REFERENCE.md +10 -9
- package/plugin/skills/agentmemory-agents/SKILL.md +19 -5
- package/plugin/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-architecture/SKILL.md +4 -4
- package/plugin/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-config/REFERENCE.md +48 -3
- package/plugin/skills/agentmemory-config/SKILL.md +51 -10
- package/plugin/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-hooks/SKILL.md +15 -5
- package/plugin/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +5 -5
- package/plugin/skills/agentmemory-mcp-tools/SKILL.md +6 -3
- package/plugin/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-rest-api/REFERENCE.md +18 -2
- package/plugin/skills/agentmemory-rest-api/SKILL.md +12 -6
- package/plugin/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/commit-context/SKILL.md +1 -1
- package/plugin/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/commit-history/SKILL.md +1 -1
- package/plugin/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/forget/EXAMPLES.md +19 -54
- package/plugin/skills/forget/SKILL.md +16 -12
- package/plugin/skills/forget/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/handoff/EXAMPLES.md +5 -4
- package/plugin/skills/handoff/SKILL.md +13 -6
- package/plugin/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/lesson/SKILL.md +1 -1
- package/plugin/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/memory-discipline/SKILL.md +14 -6
- package/plugin/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/recall/SKILL.md +18 -7
- package/plugin/skills/recall/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/recap/EXAMPLES.md +5 -3
- package/plugin/skills/recap/SKILL.md +11 -6
- package/plugin/skills/recap/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/remember/EXAMPLES.md +3 -3
- package/plugin/skills/remember/SKILL.md +1 -1
- package/plugin/skills/remember/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/session-history/EXAMPLES.md +2 -2
- package/plugin/skills/session-history/SKILL.md +10 -4
- package/plugin/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/write-agentmemory-skill/SKILL.md +2 -1
- package/plugin/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/dist/connect-Qaeq15TQ.mjs.map +0 -1
- package/dist/docker-compose.yml +0 -47
- package/dist/iii-config.docker.yaml +0 -53
- package/dist/iii-config.yaml +0 -61
- package/dist/image-refs-BKLmp_sD.mjs.map +0 -1
- package/dist/image-refs-CJgZmb9X.mjs.map +0 -1
- package/dist/keyed-mutex-DTHtcZrt.mjs.map +0 -1
- package/dist/logger-BEB7pCI9.mjs +0 -43
- package/dist/logger-BEB7pCI9.mjs.map +0 -1
- package/dist/schema-BFZhNTF7.mjs.map +0 -1
- package/dist/src-G7yt8gGm.mjs.map +0 -1
- package/dist/standalone-BqNJDJ04.mjs.map +0 -1
- package/dist/tools-registry-BV_2bu7z.mjs +0 -1405
- package/dist/tools-registry-BV_2bu7z.mjs.map +0 -1
- package/docker-compose.yml +0 -47
- package/iii-config.docker.yaml +0 -53
- 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-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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: "
|
|
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
|
|
95
|
+
const caveat = fromEnv.length > 0 ? "; a daemon started elsewhere will not see it" : "";
|
|
91
96
|
return {
|
|
92
|
-
ok:
|
|
93
|
-
detail:
|
|
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: "
|
|
102
|
-
moreInfo: "agentmemory
|
|
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: "
|
|
145
|
-
moreInfo: "When
|
|
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(
|
|
229
|
-
return join(
|
|
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(
|
|
232
|
-
return join(
|
|
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(
|
|
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(
|
|
295
|
+
path: pidfilePath(runtimeDir),
|
|
291
296
|
alwaysAsk: false,
|
|
292
|
-
applicable: pathExists(pidfilePath(
|
|
293
|
-
sizeBytes: safeSize(pidfilePath(
|
|
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(
|
|
311
|
+
path: enginePath(runtimeDir),
|
|
299
312
|
alwaysAsk: false,
|
|
300
|
-
applicable: pathExists(enginePath(
|
|
301
|
-
sizeBytes: safeSize(enginePath(
|
|
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:
|
|
359
|
-
path: dataDir
|
|
379
|
+
description: `Delete memory data directory (${dataDir}) — will ask separately`,
|
|
380
|
+
path: dataDir,
|
|
360
381
|
alwaysAsk: true,
|
|
361
|
-
applicable: !options.keepData && pathExists(dataDir
|
|
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
|
|
769
|
-
function
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
return
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
|
834
|
-
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
845
|
-
|
|
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
|
|
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
|
|
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
|
|
932
|
-
|
|
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).
|
|
947
|
-
|
|
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/
|
|
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
|
|
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
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
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 =
|
|
2174
|
+
const vPort = getConfiguredViewerPort();
|
|
1031
2175
|
return `${u.protocol}//${u.hostname}:${vPort}`;
|
|
1032
2176
|
} catch {
|
|
1033
|
-
return `http://localhost:${
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
|
2286
|
+
return runtimeMetadataPath("iii.pid");
|
|
1182
2287
|
}
|
|
1183
2288
|
function engineStatePath() {
|
|
1184
|
-
return
|
|
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
|
|
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
|
|
1251
|
-
return
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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.
|
|
1289
|
-
], { label: `Installing @agentmemory/agentmemory@0.9.
|
|
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}.
|
|
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)\
|
|
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
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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 (
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
|
1453
|
-
|
|
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
|
-
|
|
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)"}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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)
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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
|
-
|
|
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
|
|
1653
|
-
const
|
|
1654
|
-
const
|
|
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("
|
|
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 =
|
|
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
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1738
|
-
|
|
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
|
-
|
|
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
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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 =
|
|
1813
|
-
const edges =
|
|
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
|
|
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:
|
|
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(
|
|
3425
|
+
envFileExists: () => existsSync(doctorEnvPath()),
|
|
1916
3426
|
readEnvFile: () => {
|
|
1917
3427
|
try {
|
|
1918
|
-
return parseEnvFile(readFileSync(
|
|
3428
|
+
return parseEnvFile(readFileSync(doctorEnvPath(), "utf-8"));
|
|
1919
3429
|
} catch {
|
|
1920
3430
|
return {};
|
|
1921
3431
|
}
|
|
1922
3432
|
},
|
|
1923
|
-
|
|
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: () =>
|
|
1930
|
-
|
|
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
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
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:
|
|
2024
|
-
message:
|
|
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
|
|
2056
|
-
const
|
|
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.
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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`, {
|
|
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
|
-
|
|
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
|
-
|
|
3957
|
+
throw new Error("iii-engine could not be started for the demo.");
|
|
2409
3958
|
}
|
|
2410
|
-
if (!await waitForEngine(
|
|
2411
|
-
|
|
2412
|
-
|
|
3959
|
+
if (!await waitForEngine()) {
|
|
3960
|
+
printCapturedStartupStderr();
|
|
3961
|
+
throw new Error(startupTimeoutMessage("engine", startupTimeouts().engineMs));
|
|
2413
3962
|
}
|
|
2414
3963
|
}
|
|
2415
|
-
await
|
|
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 (
|
|
2422
|
-
|
|
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(`
|
|
2486
|
-
c.dim(`
|
|
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, [
|
|
2537
|
-
|
|
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, [
|
|
2543
|
-
|
|
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
|
|
2666
|
-
const
|
|
2667
|
-
if (
|
|
2668
|
-
p.
|
|
2669
|
-
|
|
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
|
-
|
|
2690
|
-
"
|
|
2691
|
-
|
|
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
|
-
|
|
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 === "
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
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
|
-
|
|
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",
|
|
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,
|
|
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\
|
|
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-
|
|
4419
|
+
await import("./standalone-BlqshyZ5.mjs");
|
|
2811
4420
|
}
|
|
2812
4421
|
async function runConnectCmd() {
|
|
2813
|
-
const { runConnect } = await import("./connect-
|
|
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 = {
|
|
2873
|
-
|
|
2874
|
-
|
|
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
|
|
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
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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
|
-
|
|
4712
|
+
const exitCode = await runStartupCleanup(1).catch(() => 1);
|
|
4713
|
+
process.exit(exitCode);
|
|
3053
4714
|
});
|
|
3054
4715
|
//#endregion
|
|
3055
4716
|
export { discoverViewerPort };
|