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