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