@agentmemory/agentmemory 0.9.30 → 0.9.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +39 -33
- package/AGENTS.md +8 -8
- package/README.md +173 -558
- package/dist/.env.example +39 -33
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +1741 -966
- package/dist/cli.mjs.map +1 -1
- package/dist/compression-recovery-CnhLQC6N.mjs +5912 -0
- package/dist/compression-recovery-CnhLQC6N.mjs.map +1 -0
- package/dist/{connect-B4_ca6BD.mjs → connect-BSLIAMpw.mjs} +874 -267
- package/dist/connect-BSLIAMpw.mjs.map +1 -0
- package/dist/hooks/_capture.d.mts +5 -2
- package/dist/hooks/_capture.d.mts.map +1 -1
- package/dist/hooks/_capture.mjs +55 -19
- package/dist/hooks/_capture.mjs.map +1 -1
- package/dist/hooks/notification.mjs +20 -6
- package/dist/hooks/notification.mjs.map +1 -1
- package/dist/hooks/post-commit.mjs +25 -5
- package/dist/hooks/post-commit.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +12 -4
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +12 -4
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +37 -10
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +25 -5
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/prompt-submit.mjs +12 -4
- package/dist/hooks/prompt-submit.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +47 -16
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +37 -10
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +74 -72
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-start.mjs +13 -5
- package/dist/hooks/subagent-start.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +18 -6
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +13 -5
- package/dist/hooks/task-completed.mjs.map +1 -1
- package/dist/{image-refs-T_urbwHM.mjs → image-refs-Cyta4Lf9.mjs} +2 -2
- package/dist/{image-refs-T_urbwHM.mjs.map → image-refs-Cyta4Lf9.mjs.map} +1 -1
- package/dist/index.mjs +8539 -4203
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/dsh.d.mts +55 -0
- package/dist/integrations/dsh.d.mts.map +1 -0
- package/dist/integrations/dsh.mjs +1145 -0
- package/dist/integrations/dsh.mjs.map +1 -0
- package/dist/{keyed-mutex-DchXsyfP.mjs → keyed-mutex-DCUh7PG-.mjs} +58 -9
- package/dist/keyed-mutex-DCUh7PG-.mjs.map +1 -0
- package/dist/{secret-store-BnY3A50C.mjs → secret-store-D9GwO0xp.mjs} +535 -434
- package/dist/secret-store-D9GwO0xp.mjs.map +1 -0
- package/dist/session-arguments-zxsbbd8y.mjs +113 -0
- package/dist/session-arguments-zxsbbd8y.mjs.map +1 -0
- package/dist/{src-Jsq9LCeH.mjs → src-DaMtDzt_.mjs} +4087 -3788
- package/dist/src-DaMtDzt_.mjs.map +1 -0
- package/dist/{standalone-DKCdrf1d.mjs → standalone-BlqshyZ5.mjs} +213 -56
- package/dist/standalone-BlqshyZ5.mjs.map +1 -0
- package/dist/standalone.d.mts +1 -1
- package/dist/standalone.d.mts.map +1 -1
- package/dist/standalone.mjs +626 -73
- package/dist/standalone.mjs.map +1 -1
- package/dist/viewer/index.html +288 -140
- package/integrations/hermes/README.md +84 -0
- package/integrations/hermes/__init__.py +669 -0
- package/integrations/hermes/plugin.yaml +27 -0
- package/integrations/omp/LICENSE +190 -0
- package/integrations/omp/README.md +76 -0
- package/integrations/omp/capture.ts +1 -0
- package/integrations/omp/dist/index.mjs +1450 -0
- package/integrations/omp/index.ts +488 -0
- package/integrations/omp/package.json +13 -0
- package/integrations/omp/security.ts +67 -0
- package/integrations/omp/skills/agentmemory-agents/REFERENCE.md +32 -0
- package/integrations/omp/skills/agentmemory-agents/SKILL.md +48 -0
- package/integrations/omp/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-architecture/SKILL.md +33 -0
- package/integrations/omp/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-config/REFERENCE.md +90 -0
- package/integrations/omp/skills/agentmemory-config/SKILL.md +78 -0
- package/integrations/omp/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-hooks/REFERENCE.md +20 -0
- package/integrations/omp/skills/agentmemory-hooks/SKILL.md +49 -0
- package/integrations/omp/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
- package/integrations/omp/skills/agentmemory-mcp-tools/SKILL.md +42 -0
- package/integrations/omp/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/agentmemory-rest-api/REFERENCE.md +158 -0
- package/integrations/omp/skills/agentmemory-rest-api/SKILL.md +49 -0
- package/integrations/omp/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/commit-context/EXAMPLES.md +71 -0
- package/integrations/omp/skills/commit-context/SKILL.md +64 -0
- package/integrations/omp/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/commit-history/EXAMPLES.md +71 -0
- package/integrations/omp/skills/commit-history/SKILL.md +62 -0
- package/integrations/omp/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/forget/EXAMPLES.md +40 -0
- package/integrations/omp/skills/forget/SKILL.md +73 -0
- package/integrations/omp/skills/forget/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/handoff/EXAMPLES.md +57 -0
- package/integrations/omp/skills/handoff/SKILL.md +75 -0
- package/integrations/omp/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/lesson/SKILL.md +64 -0
- package/integrations/omp/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/memory-discipline/SKILL.md +65 -0
- package/integrations/omp/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/recall/EXAMPLES.md +74 -0
- package/integrations/omp/skills/recall/SKILL.md +74 -0
- package/integrations/omp/skills/recall/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/recap/EXAMPLES.md +62 -0
- package/integrations/omp/skills/recap/SKILL.md +68 -0
- package/integrations/omp/skills/recap/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/remember/EXAMPLES.md +67 -0
- package/integrations/omp/skills/remember/SKILL.md +66 -0
- package/integrations/omp/skills/remember/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/session-history/EXAMPLES.md +59 -0
- package/integrations/omp/skills/session-history/SKILL.md +67 -0
- package/integrations/omp/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/integrations/omp/skills/write-agentmemory-skill/SKILL.md +41 -0
- package/integrations/omp/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/LICENSE +190 -0
- package/integrations/openclaw/README.md +88 -0
- package/integrations/openclaw/openclaw.plugin.json +71 -0
- package/integrations/openclaw/package.json +38 -0
- package/integrations/openclaw/plugin.mjs +425 -0
- package/integrations/openclaw/plugin.yaml +26 -0
- package/integrations/openclaw/skills/agentmemory-agents/REFERENCE.md +32 -0
- package/integrations/openclaw/skills/agentmemory-agents/SKILL.md +48 -0
- package/integrations/openclaw/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-architecture/SKILL.md +33 -0
- package/integrations/openclaw/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-config/REFERENCE.md +90 -0
- package/integrations/openclaw/skills/agentmemory-config/SKILL.md +78 -0
- package/integrations/openclaw/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-hooks/REFERENCE.md +20 -0
- package/integrations/openclaw/skills/agentmemory-hooks/SKILL.md +49 -0
- package/integrations/openclaw/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
- package/integrations/openclaw/skills/agentmemory-mcp-tools/SKILL.md +42 -0
- package/integrations/openclaw/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/agentmemory-rest-api/REFERENCE.md +158 -0
- package/integrations/openclaw/skills/agentmemory-rest-api/SKILL.md +49 -0
- package/integrations/openclaw/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/commit-context/EXAMPLES.md +71 -0
- package/integrations/openclaw/skills/commit-context/SKILL.md +64 -0
- package/integrations/openclaw/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/commit-history/EXAMPLES.md +71 -0
- package/integrations/openclaw/skills/commit-history/SKILL.md +62 -0
- package/integrations/openclaw/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/forget/EXAMPLES.md +40 -0
- package/integrations/openclaw/skills/forget/SKILL.md +73 -0
- package/integrations/openclaw/skills/forget/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/handoff/EXAMPLES.md +57 -0
- package/integrations/openclaw/skills/handoff/SKILL.md +75 -0
- package/integrations/openclaw/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/lesson/SKILL.md +64 -0
- package/integrations/openclaw/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/memory-discipline/SKILL.md +65 -0
- package/integrations/openclaw/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/recall/EXAMPLES.md +74 -0
- package/integrations/openclaw/skills/recall/SKILL.md +74 -0
- package/integrations/openclaw/skills/recall/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/recap/EXAMPLES.md +62 -0
- package/integrations/openclaw/skills/recap/SKILL.md +68 -0
- package/integrations/openclaw/skills/recap/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/remember/EXAMPLES.md +67 -0
- package/integrations/openclaw/skills/remember/SKILL.md +66 -0
- package/integrations/openclaw/skills/remember/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/session-history/EXAMPLES.md +59 -0
- package/integrations/openclaw/skills/session-history/SKILL.md +67 -0
- package/integrations/openclaw/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/integrations/openclaw/skills/write-agentmemory-skill/SKILL.md +41 -0
- package/integrations/openclaw/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/LICENSE +190 -0
- package/integrations/pi/README.md +45 -51
- package/integrations/pi/dist/index.mjs +1379 -0
- package/integrations/pi/index.ts +138 -171
- package/integrations/pi/package.json +16 -6
- package/integrations/pi/security.ts +6 -26
- package/integrations/pi/skills/agentmemory-agents/REFERENCE.md +32 -0
- package/integrations/pi/skills/agentmemory-agents/SKILL.md +48 -0
- package/integrations/pi/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-architecture/SKILL.md +33 -0
- package/integrations/pi/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-config/REFERENCE.md +90 -0
- package/integrations/pi/skills/agentmemory-config/SKILL.md +78 -0
- package/integrations/pi/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-hooks/REFERENCE.md +20 -0
- package/integrations/pi/skills/agentmemory-hooks/SKILL.md +49 -0
- package/integrations/pi/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-mcp-tools/REFERENCE.md +66 -0
- package/integrations/pi/skills/agentmemory-mcp-tools/SKILL.md +42 -0
- package/integrations/pi/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/agentmemory-rest-api/REFERENCE.md +158 -0
- package/integrations/pi/skills/agentmemory-rest-api/SKILL.md +49 -0
- package/integrations/pi/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/commit-context/EXAMPLES.md +71 -0
- package/integrations/pi/skills/commit-context/SKILL.md +64 -0
- package/integrations/pi/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/commit-history/EXAMPLES.md +71 -0
- package/integrations/pi/skills/commit-history/SKILL.md +62 -0
- package/integrations/pi/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/forget/EXAMPLES.md +40 -0
- package/integrations/pi/skills/forget/SKILL.md +73 -0
- package/integrations/pi/skills/forget/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/handoff/EXAMPLES.md +57 -0
- package/integrations/pi/skills/handoff/SKILL.md +75 -0
- package/integrations/pi/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/lesson/SKILL.md +64 -0
- package/integrations/pi/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/memory-discipline/SKILL.md +65 -0
- package/integrations/pi/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/recall/EXAMPLES.md +74 -0
- package/integrations/pi/skills/recall/SKILL.md +74 -0
- package/integrations/pi/skills/recall/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/recap/EXAMPLES.md +62 -0
- package/integrations/pi/skills/recap/SKILL.md +68 -0
- package/integrations/pi/skills/recap/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/remember/EXAMPLES.md +67 -0
- package/integrations/pi/skills/remember/SKILL.md +66 -0
- package/integrations/pi/skills/remember/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/session-history/EXAMPLES.md +59 -0
- package/integrations/pi/skills/session-history/SKILL.md +67 -0
- package/integrations/pi/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/integrations/pi/skills/write-agentmemory-skill/SKILL.md +41 -0
- package/integrations/pi/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/package.json +32 -11
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +4 -3
- package/plugin/.devin-plugin/plugin.json +5 -3
- package/plugin/cursor/hooks.json +23 -8
- package/plugin/hooks.json +67 -0
- package/plugin/opencode/README.md +1 -1
- package/plugin/opencode/agentmemory-capture.ts +25 -11
- package/plugin/opencode/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
- package/plugin/scripts/_capture.d.mts +5 -2
- package/plugin/scripts/_capture.d.mts.map +1 -1
- package/plugin/scripts/_capture.mjs +55 -19
- package/plugin/scripts/_capture.mjs.map +1 -1
- package/plugin/scripts/cursor/run-detached.d.mts +1 -0
- package/plugin/scripts/cursor/run-detached.mjs +786 -0
- package/plugin/scripts/cursor/run-detached.mjs.map +1 -0
- package/plugin/scripts/cursor/run-hook.d.mts +1 -0
- package/plugin/scripts/cursor/run-hook.mjs +704 -0
- package/plugin/scripts/cursor/run-hook.mjs.map +1 -0
- package/plugin/scripts/notification.mjs +20 -6
- package/plugin/scripts/notification.mjs.map +1 -1
- package/plugin/scripts/plugin-bridge.mjs +37 -6
- package/plugin/scripts/post-commit.mjs +25 -5
- package/plugin/scripts/post-commit.mjs.map +1 -1
- package/plugin/scripts/post-tool-failure.mjs +12 -4
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +12 -4
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +37 -10
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +25 -5
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/prompt-submit.mjs +12 -4
- package/plugin/scripts/prompt-submit.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +47 -16
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +37 -10
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +74 -72
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-start.mjs +13 -5
- package/plugin/scripts/subagent-start.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +18 -6
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +13 -5
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/plugin/skills/_shared/TROUBLESHOOTING.md +21 -7
- package/plugin/skills/agentmemory-agents/REFERENCE.md +7 -6
- package/plugin/skills/agentmemory-agents/SKILL.md +19 -5
- package/plugin/skills/agentmemory-agents/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-architecture/SKILL.md +4 -4
- package/plugin/skills/agentmemory-architecture/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-config/REFERENCE.md +12 -15
- package/plugin/skills/agentmemory-config/SKILL.md +7 -3
- package/plugin/skills/agentmemory-config/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-hooks/SKILL.md +15 -5
- package/plugin/skills/agentmemory-hooks/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +4 -4
- package/plugin/skills/agentmemory-mcp-tools/SKILL.md +6 -3
- package/plugin/skills/agentmemory-mcp-tools/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/agentmemory-rest-api/REFERENCE.md +9 -1
- package/plugin/skills/agentmemory-rest-api/SKILL.md +12 -6
- package/plugin/skills/agentmemory-rest-api/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/commit-context/SKILL.md +1 -1
- package/plugin/skills/commit-context/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/commit-history/SKILL.md +1 -1
- package/plugin/skills/commit-history/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/forget/EXAMPLES.md +19 -54
- package/plugin/skills/forget/SKILL.md +16 -12
- package/plugin/skills/forget/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/handoff/EXAMPLES.md +5 -4
- package/plugin/skills/handoff/SKILL.md +10 -5
- package/plugin/skills/handoff/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/lesson/SKILL.md +1 -1
- package/plugin/skills/lesson/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/memory-discipline/SKILL.md +1 -1
- package/plugin/skills/memory-discipline/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/recall/SKILL.md +18 -7
- package/plugin/skills/recall/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/recap/EXAMPLES.md +5 -3
- package/plugin/skills/recap/SKILL.md +11 -6
- package/plugin/skills/recap/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/remember/EXAMPLES.md +3 -3
- package/plugin/skills/remember/SKILL.md +1 -1
- package/plugin/skills/remember/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/session-history/EXAMPLES.md +2 -2
- package/plugin/skills/session-history/SKILL.md +10 -4
- package/plugin/skills/session-history/TROUBLESHOOTING.md +70 -0
- package/plugin/skills/write-agentmemory-skill/SKILL.md +2 -1
- package/plugin/skills/write-agentmemory-skill/TROUBLESHOOTING.md +70 -0
- package/dist/connect-B4_ca6BD.mjs.map +0 -1
- package/dist/docker-compose.yml +0 -73
- package/dist/iii-config.docker.yaml +0 -53
- package/dist/iii-config.yaml +0 -63
- package/dist/keyed-mutex-DchXsyfP.mjs.map +0 -1
- package/dist/secret-store-BnY3A50C.mjs.map +0 -1
- package/dist/src-Jsq9LCeH.mjs.map +0 -1
- package/dist/standalone-DKCdrf1d.mjs.map +0 -1
- package/dist/status-IRz7ta8-.mjs +0 -2344
- package/dist/status-IRz7ta8-.mjs.map +0 -1
- package/docker-compose.yml +0 -73
- package/iii-config.docker.yaml +0 -53
- package/iii-config.yaml +0 -63
package/dist/status-IRz7ta8-.mjs
DELETED
|
@@ -1,2344 +0,0 @@
|
|
|
1
|
-
import { o as generateId, r as KV, t as withKeyedLock } from "./keyed-mutex-DchXsyfP.mjs";
|
|
2
|
-
import { U as isGraphExtractionEnabled, c as recordAudit, et as logger, u as safeAudit } from "./secret-store-BnY3A50C.mjs";
|
|
3
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
|
|
4
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { homedir, platform } from "node:os";
|
|
6
|
-
//#region src/runtime-paths.ts
|
|
7
|
-
function runtimeMetadataPath(name, options = {}) {
|
|
8
|
-
const env = options.env ?? process.env;
|
|
9
|
-
const home = options.home ?? homedir();
|
|
10
|
-
return join(env["AGENTMEMORY_RUNTIME_DIR"] ? resolve(env["AGENTMEMORY_RUNTIME_DIR"]) : join(home, ".agentmemory"), name);
|
|
11
|
-
}
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/shutdown.ts
|
|
14
|
-
const SHUTDOWN_FLUSH_TIMEOUT_MS = 4e3;
|
|
15
|
-
const SHUTDOWN_HARD_EXIT_MS = 8e3;
|
|
16
|
-
async function settleWithin(work, ms) {
|
|
17
|
-
let timer;
|
|
18
|
-
const expiry = new Promise((resolve) => {
|
|
19
|
-
timer = setTimeout(() => resolve(false), ms);
|
|
20
|
-
});
|
|
21
|
-
try {
|
|
22
|
-
return await Promise.race([work.then(() => true), expiry]);
|
|
23
|
-
} finally {
|
|
24
|
-
clearTimeout(timer);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/cli-data-dir.ts
|
|
29
|
-
function argValue(args, name) {
|
|
30
|
-
const equals = `${name}=`;
|
|
31
|
-
const inline = args.find((arg) => arg.startsWith(equals));
|
|
32
|
-
if (inline) return inline.slice(equals.length);
|
|
33
|
-
const idx = args.indexOf(name);
|
|
34
|
-
if (idx !== -1) return args[idx + 1];
|
|
35
|
-
}
|
|
36
|
-
function expandHome(pathValue, home) {
|
|
37
|
-
if (pathValue === "~") return home;
|
|
38
|
-
if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) return join(home, pathValue.slice(2));
|
|
39
|
-
return pathValue;
|
|
40
|
-
}
|
|
41
|
-
function absoluteDataDir(pathValue, cwd, home) {
|
|
42
|
-
const expanded = expandHome(pathValue, home);
|
|
43
|
-
return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
|
|
44
|
-
}
|
|
45
|
-
function platformDefaultDataDir(env, home, nodePlatform, useXdg = true) {
|
|
46
|
-
if (nodePlatform === "darwin") return join(home, "Library", "Application Support", "agentmemory");
|
|
47
|
-
if (nodePlatform === "win32") {
|
|
48
|
-
const appData = env["APPDATA"];
|
|
49
|
-
return appData ? join(appData, "agentmemory") : join(home, ".agentmemory");
|
|
50
|
-
}
|
|
51
|
-
const xdgDataHome = env["XDG_DATA_HOME"];
|
|
52
|
-
if (useXdg && xdgDataHome && isAbsolute(xdgDataHome)) return join(xdgDataHome, "agentmemory");
|
|
53
|
-
return join(home, ".local", "share", "agentmemory");
|
|
54
|
-
}
|
|
55
|
-
function nearestGitParent(pathValue) {
|
|
56
|
-
let current = pathValue;
|
|
57
|
-
while (true) {
|
|
58
|
-
if (existsSync(join(current, ".git"))) return current;
|
|
59
|
-
const parent = dirname(current);
|
|
60
|
-
if (parent === current) return void 0;
|
|
61
|
-
current = parent;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function isWithin(parent, child) {
|
|
65
|
-
const pathFromParent = relative(parent, child);
|
|
66
|
-
return pathFromParent === "" || pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent);
|
|
67
|
-
}
|
|
68
|
-
function resolveDataDir(options = {}) {
|
|
69
|
-
const args = options.args ?? process.argv.slice(2);
|
|
70
|
-
const env = options.env ?? process.env;
|
|
71
|
-
const cwd = options.cwd ?? process.cwd();
|
|
72
|
-
const home = options.home ?? homedir();
|
|
73
|
-
const nodePlatform = options.platform ?? platform();
|
|
74
|
-
const parsedInstance = parseInt(argValue(args, "--instance") ?? "0", 10);
|
|
75
|
-
const instance = parsedInstance > 0 && parsedInstance <= 50 ? parsedInstance : 0;
|
|
76
|
-
const forInstance = (dataDir) => instance ? join(dataDir, `instance-${instance}`) : dataDir;
|
|
77
|
-
const flagValue = argValue(args, "--data-dir");
|
|
78
|
-
if (flagValue) return {
|
|
79
|
-
dataDir: forInstance(absoluteDataDir(flagValue, cwd, home)),
|
|
80
|
-
source: "flag"
|
|
81
|
-
};
|
|
82
|
-
const envValue = env["AGENTMEMORY_DATA_DIR"];
|
|
83
|
-
if (envValue) return {
|
|
84
|
-
dataDir: forInstance(absoluteDataDir(envValue, cwd, home)),
|
|
85
|
-
source: "env"
|
|
86
|
-
};
|
|
87
|
-
const legacyDir = resolve(cwd, "data");
|
|
88
|
-
const hasLegacyStore = existsSync(join(legacyDir, "state_store.db")) || existsSync(join(legacyDir, "iii-config.yaml"));
|
|
89
|
-
if (instance === 0 && hasLegacyStore) return {
|
|
90
|
-
dataDir: legacyDir,
|
|
91
|
-
source: "default"
|
|
92
|
-
};
|
|
93
|
-
const defaultDir = platformDefaultDataDir(env, home, nodePlatform);
|
|
94
|
-
const gitParent = nearestGitParent(cwd);
|
|
95
|
-
if (gitParent && isWithin(gitParent, defaultDir)) {
|
|
96
|
-
const relocated = platformDefaultDataDir(env, home, nodePlatform, false);
|
|
97
|
-
if (relocated !== defaultDir) return {
|
|
98
|
-
dataDir: forInstance(relocated),
|
|
99
|
-
source: "default",
|
|
100
|
-
relocatedFrom: defaultDir
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
dataDir: forInstance(defaultDir),
|
|
105
|
-
source: "default"
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/functions/privacy.ts
|
|
110
|
-
const PRIVATE_TAG_RE = /<private>[\s\S]*?<\/private>/gi;
|
|
111
|
-
const SECRET_PATTERN_SOURCES = [
|
|
112
|
-
/(?:api[_-]?key|secret|token|password|credential|auth)[\s]*[=:]\s*["']?[A-Za-z0-9_\-/.+]{20,}["']?/gi,
|
|
113
|
-
/Bearer\s+[A-Za-z0-9._\-+/=]{20,}/gi,
|
|
114
|
-
/sk-proj-[A-Za-z0-9\-_]{20,}/g,
|
|
115
|
-
/(?:sk|pk|rk|ak)-[A-Za-z0-9][A-Za-z0-9\-_]{19,}/g,
|
|
116
|
-
/sk-ant-[A-Za-z0-9\-_]{20,}/g,
|
|
117
|
-
/gh[pus]_[A-Za-z0-9]{36,}/g,
|
|
118
|
-
/github_pat_[A-Za-z0-9_]{22,}/g,
|
|
119
|
-
/xoxb-[A-Za-z0-9\-]+/g,
|
|
120
|
-
/AKIA[0-9A-Z]{16}/g,
|
|
121
|
-
/AIza[A-Za-z0-9\-_]{35}/g,
|
|
122
|
-
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
123
|
-
/npm_[A-Za-z0-9]{36}/g,
|
|
124
|
-
/glpat-[A-Za-z0-9\-_]{20,}/g,
|
|
125
|
-
/dop_v1_[A-Za-z0-9]{64}/g
|
|
126
|
-
];
|
|
127
|
-
const PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/g;
|
|
128
|
-
const URL_CREDENTIALS_RE = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@:"'<>]*:[^\s/?#@"'<>]+@/gi;
|
|
129
|
-
function stripPrivateData(input) {
|
|
130
|
-
let result = input.replace(PRIVATE_TAG_RE, "[REDACTED]");
|
|
131
|
-
result = result.replace(new RegExp(PRIVATE_KEY_BLOCK_RE.source, PRIVATE_KEY_BLOCK_RE.flags), "[REDACTED_SECRET]");
|
|
132
|
-
result = result.replace(new RegExp(URL_CREDENTIALS_RE.source, URL_CREDENTIALS_RE.flags), "$1[REDACTED_SECRET]@");
|
|
133
|
-
for (const source of SECRET_PATTERN_SOURCES) {
|
|
134
|
-
const pattern = new RegExp(source.source, source.flags);
|
|
135
|
-
result = result.replace(pattern, "[REDACTED_SECRET]");
|
|
136
|
-
}
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
function scrubRecord(value) {
|
|
140
|
-
if (typeof value === "string") return stripPrivateData(value);
|
|
141
|
-
if (Array.isArray(value)) return value.map((item) => scrubRecord(item));
|
|
142
|
-
if (value !== null && typeof value === "object") {
|
|
143
|
-
const proto = Object.getPrototypeOf(value);
|
|
144
|
-
if (proto !== Object.prototype && proto !== null) return value;
|
|
145
|
-
const out = {};
|
|
146
|
-
for (const [key, item] of Object.entries(value)) out[key] = scrubRecord(item);
|
|
147
|
-
return out;
|
|
148
|
-
}
|
|
149
|
-
return value;
|
|
150
|
-
}
|
|
151
|
-
const SCRUBBED_WRITE_FUNCTIONS = new Set([
|
|
152
|
-
"mem::remember",
|
|
153
|
-
"mem::evolve",
|
|
154
|
-
"mem::slot-create",
|
|
155
|
-
"mem::slot-append",
|
|
156
|
-
"mem::slot-replace",
|
|
157
|
-
"mem::lesson-save",
|
|
158
|
-
"mem::action-create",
|
|
159
|
-
"mem::action-update",
|
|
160
|
-
"mem::sketch-create",
|
|
161
|
-
"mem::sketch-add",
|
|
162
|
-
"mem::import",
|
|
163
|
-
"mem::mesh-receive",
|
|
164
|
-
"mem::team-share",
|
|
165
|
-
"mem::signal-send",
|
|
166
|
-
"mem::routine-create",
|
|
167
|
-
"mem::checkpoint-create",
|
|
168
|
-
"mem::checkpoint-resolve",
|
|
169
|
-
"mem::sentinel-create",
|
|
170
|
-
"mem::sentinel-trigger",
|
|
171
|
-
"mem::core-add",
|
|
172
|
-
"mem::facet-tag"
|
|
173
|
-
]);
|
|
174
|
-
function withWriteScrubbing(sdk) {
|
|
175
|
-
return new Proxy(sdk, { get(target, prop) {
|
|
176
|
-
const value = Reflect.get(target, prop, target);
|
|
177
|
-
if (typeof value !== "function") return value;
|
|
178
|
-
if (prop !== "registerFunction") return value.bind(target);
|
|
179
|
-
const register = value;
|
|
180
|
-
return (id, handler, ...rest) => {
|
|
181
|
-
if (!SCRUBBED_WRITE_FUNCTIONS.has(id) || typeof handler !== "function") return register.call(target, id, handler, ...rest);
|
|
182
|
-
const original = handler;
|
|
183
|
-
const scrubbed = (data, ...args) => original(scrubRecord(data), ...args);
|
|
184
|
-
return register.call(target, id, scrubbed, ...rest);
|
|
185
|
-
};
|
|
186
|
-
} });
|
|
187
|
-
}
|
|
188
|
-
function registerPrivacyFunction(sdk) {
|
|
189
|
-
sdk.registerFunction("mem::privacy", async (data) => {
|
|
190
|
-
if (!data || typeof data.input !== "string") return {
|
|
191
|
-
output: "",
|
|
192
|
-
error: "invalid input: expected string field 'input'"
|
|
193
|
-
};
|
|
194
|
-
return { output: stripPrivateData(data.input) };
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
//#endregion
|
|
198
|
-
//#region src/capture/spool.ts
|
|
199
|
-
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
200
|
-
const MIN_MAX_BYTES = 64 * 1024;
|
|
201
|
-
const DEFAULT_MAX_AGE_HOURS = 168;
|
|
202
|
-
const LOCK_STALE_MS = 1e4;
|
|
203
|
-
const DRAIN_LOCK_STALE_MS = 12e4;
|
|
204
|
-
const ORPHAN_CLAIM_MS = 12e4;
|
|
205
|
-
const LOCK_WAIT_MS = 500;
|
|
206
|
-
const IMAGE_PLACEHOLDER = "[image dropped from capture spool]";
|
|
207
|
-
const BOOT_ID_RE = /^[A-Za-z0-9]{8,64}$/;
|
|
208
|
-
const SENT_FILE_RE = /^([A-Za-z0-9]{8,64})-(\d+)\.jsonl$/;
|
|
209
|
-
const SENT_BUCKET_MS = 1e3;
|
|
210
|
-
const MAX_DURABLE_AFTER_MS = 10 * 6e4;
|
|
211
|
-
function emptyStats() {
|
|
212
|
-
return {
|
|
213
|
-
spooled: 0,
|
|
214
|
-
dropped: 0,
|
|
215
|
-
droppedBytes: 0,
|
|
216
|
-
expired: 0,
|
|
217
|
-
delivered: 0,
|
|
218
|
-
duplicates: 0,
|
|
219
|
-
rejected: 0
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
function positiveInt(raw, fallback) {
|
|
223
|
-
if (!raw || !/^\d+$/.test(raw.trim())) return fallback;
|
|
224
|
-
const n = parseInt(raw.trim(), 10);
|
|
225
|
-
return n > 0 ? n : fallback;
|
|
226
|
-
}
|
|
227
|
-
function spoolPolicy(env = process.env) {
|
|
228
|
-
const maxBytes = Math.max(MIN_MAX_BYTES, positiveInt(env["AGENTMEMORY_CAPTURE_SPOOL_MAX_BYTES"], DEFAULT_MAX_BYTES));
|
|
229
|
-
const maxAgeHours = positiveInt(env["AGENTMEMORY_CAPTURE_SPOOL_MAX_AGE_HOURS"], DEFAULT_MAX_AGE_HOURS);
|
|
230
|
-
return {
|
|
231
|
-
enabled: env["AGENTMEMORY_CAPTURE_SPOOL"] !== "false",
|
|
232
|
-
maxBytes,
|
|
233
|
-
maxAgeMs: maxAgeHours * 36e5,
|
|
234
|
-
maxRecordBytes: Math.min(256 * 1024, Math.floor(maxBytes / 4))
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
function spoolDir(env = process.env) {
|
|
238
|
-
const explicit = env["AGENTMEMORY_CAPTURE_SPOOL_DIR"];
|
|
239
|
-
if (explicit && explicit.trim()) return resolve(explicit.trim());
|
|
240
|
-
return join(resolveDataDir({
|
|
241
|
-
args: [],
|
|
242
|
-
env
|
|
243
|
-
}).dataDir, "capture-spool");
|
|
244
|
-
}
|
|
245
|
-
const LOOPBACK = new Set([
|
|
246
|
-
"localhost",
|
|
247
|
-
"127.0.0.1",
|
|
248
|
-
"::1",
|
|
249
|
-
"[::1]",
|
|
250
|
-
"0.0.0.0"
|
|
251
|
-
]);
|
|
252
|
-
function spoolTargetName(url) {
|
|
253
|
-
try {
|
|
254
|
-
const u = new URL(url);
|
|
255
|
-
return `${LOOPBACK.has(u.hostname) ? "local" : u.hostname.toLowerCase().replace(/[^a-z0-9.-]/g, "_")}-${u.port || (u.protocol === "https:" ? "443" : "80")}`;
|
|
256
|
-
} catch {
|
|
257
|
-
return "local-3111";
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function spoolPaths(url, dir = spoolDir()) {
|
|
261
|
-
const name = spoolTargetName(url);
|
|
262
|
-
return {
|
|
263
|
-
dir,
|
|
264
|
-
name,
|
|
265
|
-
file: join(dir, `${name}.jsonl`),
|
|
266
|
-
lock: join(dir, `${name}.lock`),
|
|
267
|
-
drainLock: join(dir, `${name}.drain.lock`),
|
|
268
|
-
stats: join(dir, `${name}.stats.json`)
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function ensureDir(dir) {
|
|
272
|
-
mkdirSync(dir, {
|
|
273
|
-
recursive: true,
|
|
274
|
-
mode: 448
|
|
275
|
-
});
|
|
276
|
-
try {
|
|
277
|
-
chmodSync(dir, 448);
|
|
278
|
-
} catch {}
|
|
279
|
-
}
|
|
280
|
-
function sleepSync(ms) {
|
|
281
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
282
|
-
}
|
|
283
|
-
function tryLock(path, staleMs) {
|
|
284
|
-
try {
|
|
285
|
-
const fd = openSync(path, "wx", 384);
|
|
286
|
-
writeSync(fd, `${process.pid}\n`);
|
|
287
|
-
closeSync(fd);
|
|
288
|
-
return true;
|
|
289
|
-
} catch (err) {
|
|
290
|
-
if (err.code !== "EEXIST") return false;
|
|
291
|
-
try {
|
|
292
|
-
if (Date.now() - statSync(path).mtimeMs > staleMs) unlinkSync(path);
|
|
293
|
-
} catch {}
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function releaseLock(path) {
|
|
298
|
-
try {
|
|
299
|
-
unlinkSync(path);
|
|
300
|
-
} catch {}
|
|
301
|
-
}
|
|
302
|
-
function withLock(path, fn, waitMs = LOCK_WAIT_MS) {
|
|
303
|
-
const deadline = Date.now() + waitMs;
|
|
304
|
-
let locked = tryLock(path, LOCK_STALE_MS);
|
|
305
|
-
while (!locked && Date.now() < deadline) {
|
|
306
|
-
sleepSync(5);
|
|
307
|
-
locked = tryLock(path, LOCK_STALE_MS);
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
return fn(locked);
|
|
311
|
-
} finally {
|
|
312
|
-
if (locked) releaseLock(path);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function readSpoolStats(paths) {
|
|
316
|
-
try {
|
|
317
|
-
const parsed = JSON.parse(readFileSync(paths.stats, "utf-8"));
|
|
318
|
-
return {
|
|
319
|
-
...emptyStats(),
|
|
320
|
-
...parsed
|
|
321
|
-
};
|
|
322
|
-
} catch {
|
|
323
|
-
return emptyStats();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function writeStats(paths, patch) {
|
|
327
|
-
try {
|
|
328
|
-
const stats = readSpoolStats(paths);
|
|
329
|
-
patch(stats);
|
|
330
|
-
const tmp = `${paths.stats}.${process.pid}.tmp`;
|
|
331
|
-
writeFileSync(tmp, JSON.stringify(stats), { mode: 384 });
|
|
332
|
-
renameSync(tmp, paths.stats);
|
|
333
|
-
} catch {}
|
|
334
|
-
}
|
|
335
|
-
function fileSize(path) {
|
|
336
|
-
try {
|
|
337
|
-
return statSync(path).size;
|
|
338
|
-
} catch {
|
|
339
|
-
return 0;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function sanitizeBody(body) {
|
|
343
|
-
try {
|
|
344
|
-
return JSON.parse(stripPrivateData(JSON.stringify(body)));
|
|
345
|
-
} catch {
|
|
346
|
-
return body;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
function withoutImage(body) {
|
|
350
|
-
const data = body["data"];
|
|
351
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) return body;
|
|
352
|
-
const d = data;
|
|
353
|
-
if (d["image_data"] === void 0) return body;
|
|
354
|
-
return {
|
|
355
|
-
...body,
|
|
356
|
-
data: {
|
|
357
|
-
...d,
|
|
358
|
-
image_data: IMAGE_PLACEHOLDER
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
function parseLines(text) {
|
|
363
|
-
const records = [];
|
|
364
|
-
let corrupt = 0;
|
|
365
|
-
for (const line of text.split("\n")) {
|
|
366
|
-
if (!line.trim()) continue;
|
|
367
|
-
try {
|
|
368
|
-
const rec = JSON.parse(line);
|
|
369
|
-
if (rec && typeof rec.eventId === "string" && rec.body && typeof rec.body === "object") records.push(rec);
|
|
370
|
-
else corrupt++;
|
|
371
|
-
} catch {
|
|
372
|
-
corrupt++;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return {
|
|
376
|
-
records,
|
|
377
|
-
corrupt
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
function isExpired(rec, policy, now) {
|
|
381
|
-
const at = Date.parse(rec.spooledAt);
|
|
382
|
-
return Number.isFinite(at) && now - at > policy.maxAgeMs;
|
|
383
|
-
}
|
|
384
|
-
function serialize(records) {
|
|
385
|
-
return records.map((r) => JSON.stringify(r)).join("\n") + (records.length ? "\n" : "");
|
|
386
|
-
}
|
|
387
|
-
function rewriteLocked(paths, records) {
|
|
388
|
-
const tmp = `${paths.file}.${process.pid}.tmp`;
|
|
389
|
-
const fd = openSync(tmp, "w", 384);
|
|
390
|
-
try {
|
|
391
|
-
writeSync(fd, serialize(records));
|
|
392
|
-
fsyncSync(fd);
|
|
393
|
-
} finally {
|
|
394
|
-
closeSync(fd);
|
|
395
|
-
}
|
|
396
|
-
renameSync(tmp, paths.file);
|
|
397
|
-
}
|
|
398
|
-
function listSent(paths) {
|
|
399
|
-
const segments = [];
|
|
400
|
-
let entries;
|
|
401
|
-
try {
|
|
402
|
-
entries = readdirSync(paths.dir);
|
|
403
|
-
} catch {
|
|
404
|
-
return segments;
|
|
405
|
-
}
|
|
406
|
-
const prefix = `${paths.name}.sent-`;
|
|
407
|
-
for (const entry of entries) {
|
|
408
|
-
if (!entry.startsWith(prefix)) continue;
|
|
409
|
-
const match = entry.slice(prefix.length).match(SENT_FILE_RE);
|
|
410
|
-
if (!match) continue;
|
|
411
|
-
const file = join(paths.dir, entry);
|
|
412
|
-
segments.push({
|
|
413
|
-
file,
|
|
414
|
-
bootId: match[1],
|
|
415
|
-
dueAt: Number(match[2]),
|
|
416
|
-
bytes: fileSize(file)
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
return segments;
|
|
420
|
-
}
|
|
421
|
-
function sentBytes(paths) {
|
|
422
|
-
return listSent(paths).reduce((n, s) => n + s.bytes, 0);
|
|
423
|
-
}
|
|
424
|
-
function parseSentMark(body) {
|
|
425
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
426
|
-
const bootId = body["bootId"];
|
|
427
|
-
const after = body["durableAfterMs"];
|
|
428
|
-
if (typeof bootId !== "string" || !BOOT_ID_RE.test(bootId)) return null;
|
|
429
|
-
if (typeof after !== "number" || !Number.isFinite(after) || after < 0) return null;
|
|
430
|
-
return {
|
|
431
|
-
bootId,
|
|
432
|
-
durableAfterMs: Math.min(Math.ceil(after), MAX_DURABLE_AFTER_MS)
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
function retainSent(url, eventId, body, mark, options = {}) {
|
|
436
|
-
const policy = options.policy ?? spoolPolicy();
|
|
437
|
-
if (!policy.enabled || !BOOT_ID_RE.test(mark.bootId)) return false;
|
|
438
|
-
const paths = spoolPaths(url, options.dir);
|
|
439
|
-
const now = options.now ?? Date.now();
|
|
440
|
-
try {
|
|
441
|
-
ensureDir(paths.dir);
|
|
442
|
-
const record = {
|
|
443
|
-
v: 1,
|
|
444
|
-
eventId,
|
|
445
|
-
spooledAt: new Date(now).toISOString(),
|
|
446
|
-
reason: "sent",
|
|
447
|
-
attempts: 0,
|
|
448
|
-
body: sanitizeBody(body),
|
|
449
|
-
bootId: mark.bootId
|
|
450
|
-
};
|
|
451
|
-
let line = JSON.stringify(record) + "\n";
|
|
452
|
-
if (Buffer.byteLength(line) > policy.maxRecordBytes) {
|
|
453
|
-
record.body = withoutImage(record.body);
|
|
454
|
-
line = JSON.stringify(record) + "\n";
|
|
455
|
-
}
|
|
456
|
-
const bytes = Buffer.byteLength(line);
|
|
457
|
-
if (bytes > policy.maxRecordBytes) return false;
|
|
458
|
-
const dueAt = Math.ceil((now + mark.durableAfterMs) / SENT_BUCKET_MS) * SENT_BUCKET_MS;
|
|
459
|
-
const file = join(paths.dir, `${paths.name}.sent-${mark.bootId}-${dueAt}.jsonl`);
|
|
460
|
-
return withLock(paths.lock, () => {
|
|
461
|
-
if (fileSize(paths.file) + sentBytes(paths) + bytes > policy.maxBytes) return false;
|
|
462
|
-
const fd = openSync(file, "a", 384);
|
|
463
|
-
try {
|
|
464
|
-
writeSync(fd, line);
|
|
465
|
-
} finally {
|
|
466
|
-
closeSync(fd);
|
|
467
|
-
}
|
|
468
|
-
return true;
|
|
469
|
-
});
|
|
470
|
-
} catch {
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
function reconcileSent(url, bootId, options = {}) {
|
|
475
|
-
const result = {
|
|
476
|
-
released: 0,
|
|
477
|
-
requeued: 0
|
|
478
|
-
};
|
|
479
|
-
if (!(options.policy ?? spoolPolicy()).enabled) return result;
|
|
480
|
-
const paths = spoolPaths(url, options.dir);
|
|
481
|
-
const now = options.now ?? Date.now();
|
|
482
|
-
for (const seg of listSent(paths)) {
|
|
483
|
-
if (seg.bootId === bootId) {
|
|
484
|
-
if (seg.dueAt > now) continue;
|
|
485
|
-
try {
|
|
486
|
-
unlinkSync(seg.file);
|
|
487
|
-
result.released++;
|
|
488
|
-
} catch {}
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
const target = join(paths.dir, `${paths.name}.draining-${process.pid}-${now}-${result.requeued}.jsonl`);
|
|
492
|
-
try {
|
|
493
|
-
renameSync(seg.file, target);
|
|
494
|
-
} catch {
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
try {
|
|
498
|
-
utimesSync(target, 0, 0);
|
|
499
|
-
} catch {}
|
|
500
|
-
result.requeued++;
|
|
501
|
-
}
|
|
502
|
-
return result;
|
|
503
|
-
}
|
|
504
|
-
function claimFiles(paths) {
|
|
505
|
-
const claimed = [];
|
|
506
|
-
try {
|
|
507
|
-
const prefix = `${paths.name}.draining-`;
|
|
508
|
-
for (const entry of readdirSync(paths.dir)) {
|
|
509
|
-
if (!entry.startsWith(prefix) || !entry.endsWith(".jsonl")) continue;
|
|
510
|
-
const full = join(paths.dir, entry);
|
|
511
|
-
try {
|
|
512
|
-
if (Date.now() - statSync(full).mtimeMs > ORPHAN_CLAIM_MS) claimed.push(full);
|
|
513
|
-
} catch {}
|
|
514
|
-
}
|
|
515
|
-
} catch {}
|
|
516
|
-
if (fileSize(paths.file) > 0) {
|
|
517
|
-
const target = join(paths.dir, `${paths.name}.draining-${process.pid}-${Date.now()}.jsonl`);
|
|
518
|
-
withLock(paths.lock, (locked) => {
|
|
519
|
-
if (!locked) return;
|
|
520
|
-
try {
|
|
521
|
-
renameSync(paths.file, target);
|
|
522
|
-
claimed.push(target);
|
|
523
|
-
} catch {}
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
return claimed;
|
|
527
|
-
}
|
|
528
|
-
function requeueLocked(paths, leftover) {
|
|
529
|
-
if (leftover.length === 0) return;
|
|
530
|
-
const existing = existsSync(paths.file) ? parseLines(readFileSync(paths.file, "utf-8")).records : [];
|
|
531
|
-
rewriteLocked(paths, [...leftover, ...existing]);
|
|
532
|
-
}
|
|
533
|
-
function requeueUnlocked(paths, leftover) {
|
|
534
|
-
if (leftover.length === 0) return;
|
|
535
|
-
const target = join(paths.dir, `${paths.name}.draining-${process.pid}-${Date.now()}-requeue.jsonl`);
|
|
536
|
-
const fd = openSync(target, "wx", 384);
|
|
537
|
-
try {
|
|
538
|
-
writeSync(fd, serialize(leftover));
|
|
539
|
-
fsyncSync(fd);
|
|
540
|
-
} finally {
|
|
541
|
-
closeSync(fd);
|
|
542
|
-
}
|
|
543
|
-
try {
|
|
544
|
-
utimesSync(target, 0, 0);
|
|
545
|
-
} catch {}
|
|
546
|
-
}
|
|
547
|
-
async function drainSpool(url, send, options = {}) {
|
|
548
|
-
const policy = options.policy ?? spoolPolicy();
|
|
549
|
-
const result = {
|
|
550
|
-
claimed: 0,
|
|
551
|
-
delivered: 0,
|
|
552
|
-
duplicates: 0,
|
|
553
|
-
rejected: 0,
|
|
554
|
-
expired: 0,
|
|
555
|
-
corrupt: 0,
|
|
556
|
-
remaining: 0
|
|
557
|
-
};
|
|
558
|
-
const paths = spoolPaths(url, options.dir);
|
|
559
|
-
if (!existsSync(paths.dir)) return {
|
|
560
|
-
...result,
|
|
561
|
-
skipped: "empty"
|
|
562
|
-
};
|
|
563
|
-
try {
|
|
564
|
-
ensureDir(paths.dir);
|
|
565
|
-
} catch {}
|
|
566
|
-
if (!tryLock(paths.drainLock, DRAIN_LOCK_STALE_MS)) return {
|
|
567
|
-
...result,
|
|
568
|
-
skipped: "locked"
|
|
569
|
-
};
|
|
570
|
-
try {
|
|
571
|
-
const files = claimFiles(paths);
|
|
572
|
-
if (files.length === 0) return {
|
|
573
|
-
...result,
|
|
574
|
-
skipped: "empty"
|
|
575
|
-
};
|
|
576
|
-
const records = [];
|
|
577
|
-
for (const file of files) try {
|
|
578
|
-
const parsed = parseLines(readFileSync(file, "utf-8"));
|
|
579
|
-
records.push(...parsed.records);
|
|
580
|
-
result.corrupt += parsed.corrupt;
|
|
581
|
-
} catch {}
|
|
582
|
-
result.claimed = records.length;
|
|
583
|
-
const seen = /* @__PURE__ */ new Set();
|
|
584
|
-
const now = Date.now();
|
|
585
|
-
const deadline = options.deadlineMs ? now + options.deadlineMs : Number.POSITIVE_INFINITY;
|
|
586
|
-
const maxRecords = options.maxRecords ?? Number.POSITIVE_INFINITY;
|
|
587
|
-
const leftover = [];
|
|
588
|
-
let stopped = false;
|
|
589
|
-
let processed = 0;
|
|
590
|
-
for (const rec of records) {
|
|
591
|
-
if (seen.has(rec.eventId)) {
|
|
592
|
-
result.duplicates++;
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
seen.add(rec.eventId);
|
|
596
|
-
if (isExpired(rec, policy, now)) {
|
|
597
|
-
result.expired++;
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
if (stopped || processed >= maxRecords || Date.now() > deadline) {
|
|
601
|
-
leftover.push(rec);
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
processed++;
|
|
605
|
-
let outcome;
|
|
606
|
-
try {
|
|
607
|
-
outcome = await send(rec);
|
|
608
|
-
} catch (err) {
|
|
609
|
-
outcome = "retry";
|
|
610
|
-
result.error = err instanceof Error ? err.message : String(err);
|
|
611
|
-
}
|
|
612
|
-
if (outcome === "delivered") result.delivered++;
|
|
613
|
-
else if (outcome === "duplicate") result.duplicates++;
|
|
614
|
-
else if (outcome === "rejected") result.rejected++;
|
|
615
|
-
else {
|
|
616
|
-
stopped = true;
|
|
617
|
-
leftover.push({
|
|
618
|
-
...rec,
|
|
619
|
-
attempts: (rec.attempts ?? 0) + 1
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
result.remaining = leftover.length;
|
|
624
|
-
withLock(paths.lock, (locked) => locked ? requeueLocked(paths, leftover) : requeueUnlocked(paths, leftover), 2e3);
|
|
625
|
-
for (const file of files) try {
|
|
626
|
-
unlinkSync(file);
|
|
627
|
-
} catch {}
|
|
628
|
-
writeStats(paths, (s) => {
|
|
629
|
-
s.delivered += result.delivered;
|
|
630
|
-
s.duplicates += result.duplicates;
|
|
631
|
-
s.rejected += result.rejected;
|
|
632
|
-
s.expired += result.expired;
|
|
633
|
-
s.lastDrainAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
634
|
-
s.lastDrainDelivered = result.delivered;
|
|
635
|
-
s.lastDrainDuplicates = result.duplicates;
|
|
636
|
-
s.lastDrainRejected = result.rejected;
|
|
637
|
-
s.lastDrainRemaining = result.remaining;
|
|
638
|
-
if (result.error) s.lastDrainError = result.error;
|
|
639
|
-
else delete s.lastDrainError;
|
|
640
|
-
});
|
|
641
|
-
return result;
|
|
642
|
-
} finally {
|
|
643
|
-
releaseLock(paths.drainLock);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
function spoolSummary(url, options = {}) {
|
|
647
|
-
const policy = options.policy ?? spoolPolicy();
|
|
648
|
-
const paths = spoolPaths(url, options.dir);
|
|
649
|
-
let records = 0;
|
|
650
|
-
let bytes = 0;
|
|
651
|
-
let oldestAt = null;
|
|
652
|
-
const files = [];
|
|
653
|
-
if (existsSync(paths.file)) files.push(paths.file);
|
|
654
|
-
try {
|
|
655
|
-
for (const entry of readdirSync(paths.dir)) if (entry.startsWith(`${paths.name}.draining-`) && entry.endsWith(".jsonl")) files.push(join(paths.dir, entry));
|
|
656
|
-
} catch {}
|
|
657
|
-
for (const file of files) try {
|
|
658
|
-
const text = readFileSync(file, "utf-8");
|
|
659
|
-
bytes += Buffer.byteLength(text);
|
|
660
|
-
for (const rec of parseLines(text).records) {
|
|
661
|
-
records++;
|
|
662
|
-
if (!oldestAt || rec.spooledAt < oldestAt) oldestAt = rec.spooledAt;
|
|
663
|
-
}
|
|
664
|
-
} catch {}
|
|
665
|
-
let retained = 0;
|
|
666
|
-
let retainedBytes = 0;
|
|
667
|
-
for (const seg of listSent(paths)) try {
|
|
668
|
-
const text = readFileSync(seg.file, "utf-8");
|
|
669
|
-
retainedBytes += Buffer.byteLength(text);
|
|
670
|
-
retained += parseLines(text).records.length;
|
|
671
|
-
} catch {}
|
|
672
|
-
return {
|
|
673
|
-
enabled: policy.enabled,
|
|
674
|
-
path: paths.file,
|
|
675
|
-
records,
|
|
676
|
-
bytes,
|
|
677
|
-
oldestAt,
|
|
678
|
-
retained,
|
|
679
|
-
retainedBytes,
|
|
680
|
-
maxBytes: policy.maxBytes,
|
|
681
|
-
maxAgeHours: Math.round(policy.maxAgeMs / 36e5),
|
|
682
|
-
stats: readSpoolStats(paths)
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
//#endregion
|
|
686
|
-
//#region src/prompts/graph-extraction.ts
|
|
687
|
-
const GRAPH_EXTRACTION_SYSTEM = `You are a knowledge graph extraction engine. Given a compressed observation from a coding session, extract entities and relationships.
|
|
688
|
-
|
|
689
|
-
Output format (XML):
|
|
690
|
-
<entities>
|
|
691
|
-
<entity type="file|function|concept|error|decision|pattern|library|person" name="exact name">
|
|
692
|
-
<property key="key">value</property>
|
|
693
|
-
</entity>
|
|
694
|
-
</entities>
|
|
695
|
-
<relationships>
|
|
696
|
-
<relationship type="uses|imports|modifies|causes|fixes|depends_on|related_to" source="entity name" target="entity name" weight="0.1-1.0"/>
|
|
697
|
-
</relationships>
|
|
698
|
-
|
|
699
|
-
Rules:
|
|
700
|
-
- Extract concrete entities only (real file paths, function names, library names)
|
|
701
|
-
- Use the most specific type available
|
|
702
|
-
- Weight relationships by how strong/direct the connection is
|
|
703
|
-
- If no entities found, output empty tags`;
|
|
704
|
-
function buildGraphExtractionPrompt(observations) {
|
|
705
|
-
return `Extract entities and relationships from these observations:\n\n${observations.map((o, i) => `[${i + 1}] Type: ${o.type}\nTitle: ${o.title}\nNarrative: ${o.narrative}\nConcepts: ${(o.concepts ?? []).join(", ")}\nFiles: ${(o.files ?? []).join(", ")}`).join("\n\n")}${process.env.AGENTMEMORY_LLM_NOTHINK === "1" ? "\n/no_think" : ""}`;
|
|
706
|
-
}
|
|
707
|
-
//#endregion
|
|
708
|
-
//#region src/functions/graph.ts
|
|
709
|
-
const DEFAULT_GRAPH_QUERY_LIMIT = 500;
|
|
710
|
-
const MAX_GRAPH_QUERY_LIMIT = 5e3;
|
|
711
|
-
const SNAPSHOT_TOP_NODES = DEFAULT_GRAPH_QUERY_LIMIT;
|
|
712
|
-
const SNAPSHOT_KEY = "current";
|
|
713
|
-
const LIVE_ENUMERATION_BUDGET_MS = 6e3;
|
|
714
|
-
function withTimeout(p, ms, label) {
|
|
715
|
-
return new Promise((resolve, reject) => {
|
|
716
|
-
const t = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label}: exceeded ${ms}ms budget`)), ms);
|
|
717
|
-
p.then((v) => {
|
|
718
|
-
clearTimeout(t);
|
|
719
|
-
resolve(v);
|
|
720
|
-
}, (err) => {
|
|
721
|
-
clearTimeout(t);
|
|
722
|
-
reject(err);
|
|
723
|
-
});
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
function emptySnapshot() {
|
|
727
|
-
return {
|
|
728
|
-
version: 1,
|
|
729
|
-
topNodes: [],
|
|
730
|
-
topEdges: [],
|
|
731
|
-
topDegrees: {},
|
|
732
|
-
stats: {
|
|
733
|
-
totalNodes: 0,
|
|
734
|
-
totalEdges: 0,
|
|
735
|
-
nodesByType: {},
|
|
736
|
-
edgesByType: {}
|
|
737
|
-
},
|
|
738
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
739
|
-
dirty: true
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
async function readSnapshot(kv) {
|
|
743
|
-
try {
|
|
744
|
-
const snap = await kv.get(KV.graphSnapshot, SNAPSHOT_KEY);
|
|
745
|
-
if (snap && typeof snap === "object" && snap.version === 1) return snap;
|
|
746
|
-
return null;
|
|
747
|
-
} catch (err) {
|
|
748
|
-
logger.warn("Graph snapshot read failed", { error: err instanceof Error ? err.message : String(err) });
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
async function readSnapshotStrict(kv) {
|
|
753
|
-
let raw;
|
|
754
|
-
try {
|
|
755
|
-
raw = await kv.get(KV.graphSnapshot, SNAPSHOT_KEY);
|
|
756
|
-
} catch (err) {
|
|
757
|
-
logger.warn("Graph snapshot read failed, retrying once", { error: err instanceof Error ? err.message : String(err) });
|
|
758
|
-
raw = await kv.get(KV.graphSnapshot, SNAPSHOT_KEY);
|
|
759
|
-
}
|
|
760
|
-
if (raw === null || raw === void 0) return null;
|
|
761
|
-
if (typeof raw === "object" && raw.version === 1) return raw;
|
|
762
|
-
throw new Error(`Graph snapshot has unknown schema version: ${JSON.stringify(raw.version)}`);
|
|
763
|
-
}
|
|
764
|
-
function buildSnapshotFromArrays(nodes, edges) {
|
|
765
|
-
const liveNodes = nodes.filter((n) => !n.stale);
|
|
766
|
-
const liveEdges = edges.filter((e) => !e.stale);
|
|
767
|
-
const degree = /* @__PURE__ */ new Map();
|
|
768
|
-
for (const e of liveEdges) {
|
|
769
|
-
degree.set(e.sourceNodeId, (degree.get(e.sourceNodeId) ?? 0) + 1);
|
|
770
|
-
degree.set(e.targetNodeId, (degree.get(e.targetNodeId) ?? 0) + 1);
|
|
771
|
-
}
|
|
772
|
-
const ranked = [...liveNodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, SNAPSHOT_TOP_NODES);
|
|
773
|
-
const rankedIds = new Set(ranked.map((n) => n.id));
|
|
774
|
-
const topEdges = liveEdges.filter((e) => rankedIds.has(e.sourceNodeId) && rankedIds.has(e.targetNodeId));
|
|
775
|
-
const topDegrees = {};
|
|
776
|
-
for (const n of ranked) topDegrees[n.id] = degree.get(n.id) ?? 0;
|
|
777
|
-
const nodesByType = {};
|
|
778
|
-
for (const n of liveNodes) nodesByType[n.type] = (nodesByType[n.type] || 0) + 1;
|
|
779
|
-
const edgesByType = {};
|
|
780
|
-
for (const e of liveEdges) edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
|
|
781
|
-
return {
|
|
782
|
-
version: 1,
|
|
783
|
-
topNodes: ranked,
|
|
784
|
-
topEdges,
|
|
785
|
-
topDegrees,
|
|
786
|
-
stats: {
|
|
787
|
-
totalNodes: liveNodes.length,
|
|
788
|
-
totalEdges: liveEdges.length,
|
|
789
|
-
nodesByType,
|
|
790
|
-
edgesByType
|
|
791
|
-
},
|
|
792
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
793
|
-
dirty: false
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
function paginateFromSnapshot(snap, filterType, limit, offset) {
|
|
797
|
-
const filteredNodes = filterType ? snap.topNodes.filter((n) => n.type === filterType) : snap.topNodes;
|
|
798
|
-
const totalRaw = filterType ? snap.stats.nodesByType[filterType] ?? 0 : snap.stats.totalNodes;
|
|
799
|
-
const total = Math.max(totalRaw, filteredNodes.length);
|
|
800
|
-
const pageNodes = filteredNodes.slice(offset, offset + limit);
|
|
801
|
-
const pageIds = new Set(pageNodes.map((n) => n.id));
|
|
802
|
-
const pageEdges = snap.topEdges.filter((e) => pageIds.has(e.sourceNodeId) && pageIds.has(e.targetNodeId));
|
|
803
|
-
const degrees = {};
|
|
804
|
-
for (const n of pageNodes) {
|
|
805
|
-
const d = snap.topDegrees?.[n.id];
|
|
806
|
-
if (typeof d === "number") degrees[n.id] = d;
|
|
807
|
-
}
|
|
808
|
-
return {
|
|
809
|
-
nodes: pageNodes,
|
|
810
|
-
edges: pageEdges,
|
|
811
|
-
depth: 0,
|
|
812
|
-
totalNodes: total,
|
|
813
|
-
totalEdges: snap.stats.totalEdges,
|
|
814
|
-
truncated: total > pageNodes.length,
|
|
815
|
-
limit,
|
|
816
|
-
offset,
|
|
817
|
-
fromSnapshot: true,
|
|
818
|
-
degrees
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
const REBUILD_SAFE_NODE_CEILING = 25e3;
|
|
822
|
-
function nameIndexKey(type, name) {
|
|
823
|
-
return `${type}|${name}`;
|
|
824
|
-
}
|
|
825
|
-
function edgeIndexKey(sourceNodeId, targetNodeId, type) {
|
|
826
|
-
return `${sourceNodeId}|${targetNodeId}|${type}`;
|
|
827
|
-
}
|
|
828
|
-
async function applyDegreeDelta(kv, snap, nodeId, delta) {
|
|
829
|
-
const prev = await kv.get(KV.graphNodeDegree, nodeId) ?? 0;
|
|
830
|
-
const next = Math.max(0, prev + delta);
|
|
831
|
-
await kv.set(KV.graphNodeDegree, nodeId, next);
|
|
832
|
-
if (snap.topNodes.findIndex((n) => n.id === nodeId) !== -1) {
|
|
833
|
-
snap.topDegrees[nodeId] = next;
|
|
834
|
-
snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
|
|
835
|
-
return next;
|
|
836
|
-
}
|
|
837
|
-
if (snap.topNodes.length < SNAPSHOT_TOP_NODES) {
|
|
838
|
-
const node = await kv.get(KV.graphNodes, nodeId);
|
|
839
|
-
if (node && !node.stale) {
|
|
840
|
-
snap.topNodes.push(node);
|
|
841
|
-
snap.topDegrees[node.id] = next;
|
|
842
|
-
snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
|
|
843
|
-
}
|
|
844
|
-
return next;
|
|
845
|
-
}
|
|
846
|
-
const tailEntry = snap.topNodes[snap.topNodes.length - 1];
|
|
847
|
-
if (!tailEntry) return next;
|
|
848
|
-
if (next > (snap.topDegrees[tailEntry.id] ?? 0)) {
|
|
849
|
-
const node = await kv.get(KV.graphNodes, nodeId);
|
|
850
|
-
if (node && !node.stale) {
|
|
851
|
-
const evicted = snap.topNodes.pop();
|
|
852
|
-
if (evicted) delete snap.topDegrees[evicted.id];
|
|
853
|
-
snap.topNodes.push(node);
|
|
854
|
-
snap.topDegrees[node.id] = next;
|
|
855
|
-
snap.topNodes.sort((a, b) => (snap.topDegrees[b.id] ?? 0) - (snap.topDegrees[a.id] ?? 0));
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
return next;
|
|
859
|
-
}
|
|
860
|
-
function snapshotPushEdgeIfBothInTop(snap, edge) {
|
|
861
|
-
const topIds = new Set(snap.topNodes.map((n) => n.id));
|
|
862
|
-
if (topIds.has(edge.sourceNodeId) && topIds.has(edge.targetNodeId)) {
|
|
863
|
-
if (!snap.topEdges.find((e) => e.id === edge.id)) snap.topEdges.push(edge);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
function boundSources(existing, incoming) {
|
|
867
|
-
const merged = new Set(existing);
|
|
868
|
-
for (const id of incoming) {
|
|
869
|
-
merged.delete(id);
|
|
870
|
-
merged.add(id);
|
|
871
|
-
}
|
|
872
|
-
return [...merged].slice(-32);
|
|
873
|
-
}
|
|
874
|
-
function boundRecordSources(record) {
|
|
875
|
-
const sources = record?.sourceObservationIds;
|
|
876
|
-
if (!Array.isArray(sources) || sources.length <= 32) return record;
|
|
877
|
-
return {
|
|
878
|
-
...record,
|
|
879
|
-
sourceObservationIds: boundSources([], sources)
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
function mergeNode(existing, incoming, capturedAt) {
|
|
883
|
-
return {
|
|
884
|
-
...existing,
|
|
885
|
-
sourceObservationIds: boundSources(existing.sourceObservationIds ?? [], incoming.sourceObservationIds ?? []),
|
|
886
|
-
properties: {
|
|
887
|
-
...existing.properties,
|
|
888
|
-
...incoming.properties
|
|
889
|
-
},
|
|
890
|
-
updatedAt: capturedAt
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
function mergeEdge(existing, incoming) {
|
|
894
|
-
return {
|
|
895
|
-
...existing,
|
|
896
|
-
sourceObservationIds: boundSources(existing.sourceObservationIds ?? [], incoming.sourceObservationIds ?? [])
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
function resolvePagination(rawLimit, rawOffset) {
|
|
900
|
-
return {
|
|
901
|
-
limit: Math.max(1, Math.min(typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.floor(rawLimit) : DEFAULT_GRAPH_QUERY_LIMIT, MAX_GRAPH_QUERY_LIMIT)),
|
|
902
|
-
offset: Math.max(0, typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.floor(rawOffset) : 0)
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
function paginate(nodes, allEdges, depth, limit, offset) {
|
|
906
|
-
const totalNodes = nodes.length;
|
|
907
|
-
const pageNodes = nodes.slice(offset, offset + limit);
|
|
908
|
-
const pageNodeIds = new Set(pageNodes.map((n) => n.id));
|
|
909
|
-
const pageEdges = allEdges.filter((e) => pageNodeIds.has(e.sourceNodeId) && pageNodeIds.has(e.targetNodeId));
|
|
910
|
-
const universeIds = new Set(nodes.map((n) => n.id));
|
|
911
|
-
return {
|
|
912
|
-
nodes: pageNodes,
|
|
913
|
-
edges: pageEdges,
|
|
914
|
-
depth,
|
|
915
|
-
totalNodes,
|
|
916
|
-
totalEdges: allEdges.reduce((count, e) => universeIds.has(e.sourceNodeId) && universeIds.has(e.targetNodeId) ? count + 1 : count, 0),
|
|
917
|
-
truncated: totalNodes > pageNodes.length,
|
|
918
|
-
limit,
|
|
919
|
-
offset
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
function parseAttrs(raw) {
|
|
923
|
-
const attrs = {};
|
|
924
|
-
const attrRegex = /([A-Za-z_][\w:-]*)="([^"]*)"/g;
|
|
925
|
-
let m;
|
|
926
|
-
while ((m = attrRegex.exec(raw)) !== null) attrs[m[1]] = m[2];
|
|
927
|
-
return attrs;
|
|
928
|
-
}
|
|
929
|
-
function parseGraphXml(xml, observationIds) {
|
|
930
|
-
const nodes = [];
|
|
931
|
-
const edges = [];
|
|
932
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
933
|
-
const entitySelfClose = /<entity\b([^>]*?)\/>/g;
|
|
934
|
-
const entityWithBody = /<entity\b([^>]*[^/])>([\s\S]*?)<\/entity>/g;
|
|
935
|
-
const addEntity = (rawAttrs, propsBlock = "") => {
|
|
936
|
-
const attrs = parseAttrs(rawAttrs);
|
|
937
|
-
const type = attrs["type"];
|
|
938
|
-
const name = attrs["name"];
|
|
939
|
-
if (!type || !name) return;
|
|
940
|
-
const properties = {};
|
|
941
|
-
const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
|
|
942
|
-
let propMatch;
|
|
943
|
-
while ((propMatch = propRegex.exec(propsBlock)) !== null) properties[propMatch[1]] = propMatch[2];
|
|
944
|
-
nodes.push({
|
|
945
|
-
id: generateId("gn"),
|
|
946
|
-
type,
|
|
947
|
-
name,
|
|
948
|
-
properties,
|
|
949
|
-
sourceObservationIds: observationIds,
|
|
950
|
-
createdAt: now
|
|
951
|
-
});
|
|
952
|
-
};
|
|
953
|
-
let match;
|
|
954
|
-
while ((match = entitySelfClose.exec(xml)) !== null) addEntity(match[1]);
|
|
955
|
-
while ((match = entityWithBody.exec(xml)) !== null) addEntity(match[1], match[2]);
|
|
956
|
-
const relRegex = /<relationship\b([^>]*?)\/>/g;
|
|
957
|
-
while ((match = relRegex.exec(xml)) !== null) {
|
|
958
|
-
const attrs = parseAttrs(match[1]);
|
|
959
|
-
const type = attrs["type"];
|
|
960
|
-
const sourceName = attrs["source"];
|
|
961
|
-
const targetName = attrs["target"];
|
|
962
|
-
if (!type || !sourceName || !targetName) continue;
|
|
963
|
-
const parsedWeight = parseFloat(attrs["weight"] ?? "");
|
|
964
|
-
const weight = Number.isFinite(parsedWeight) ? parsedWeight : .5;
|
|
965
|
-
const sourceNode = nodes.find((n) => n.name === sourceName);
|
|
966
|
-
const targetNode = nodes.find((n) => n.name === targetName);
|
|
967
|
-
if (!sourceNode || !targetNode) continue;
|
|
968
|
-
edges.push({
|
|
969
|
-
id: generateId("ge"),
|
|
970
|
-
type,
|
|
971
|
-
sourceNodeId: sourceNode.id,
|
|
972
|
-
targetNodeId: targetNode.id,
|
|
973
|
-
weight: Math.max(0, Math.min(1, weight)),
|
|
974
|
-
sourceObservationIds: observationIds,
|
|
975
|
-
createdAt: now
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
return {
|
|
979
|
-
nodes,
|
|
980
|
-
edges
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
const HEURISTIC_EDGE_WEIGHT = .4;
|
|
984
|
-
const MAX_HEURISTIC_EDGES_PER_OBS = 12;
|
|
985
|
-
function extractGraphHeuristics(observations) {
|
|
986
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
987
|
-
const nodes = [];
|
|
988
|
-
const nodeByKey = /* @__PURE__ */ new Map();
|
|
989
|
-
const edges = [];
|
|
990
|
-
const edgeByPair = /* @__PURE__ */ new Map();
|
|
991
|
-
const nodeFor = (type, name, obsId) => {
|
|
992
|
-
const trimmed = name.trim();
|
|
993
|
-
if (!trimmed) return null;
|
|
994
|
-
const key = `${type}${trimmed.toLowerCase()}`;
|
|
995
|
-
let node = nodeByKey.get(key);
|
|
996
|
-
if (!node) {
|
|
997
|
-
node = {
|
|
998
|
-
id: generateId("gn"),
|
|
999
|
-
type,
|
|
1000
|
-
name: trimmed,
|
|
1001
|
-
properties: {},
|
|
1002
|
-
sourceObservationIds: [obsId],
|
|
1003
|
-
createdAt: now
|
|
1004
|
-
};
|
|
1005
|
-
nodeByKey.set(key, node);
|
|
1006
|
-
nodes.push(node);
|
|
1007
|
-
} else if (!node.sourceObservationIds.includes(obsId)) node.sourceObservationIds.push(obsId);
|
|
1008
|
-
return node;
|
|
1009
|
-
};
|
|
1010
|
-
for (const obs of observations) {
|
|
1011
|
-
let budget = MAX_HEURISTIC_EDGES_PER_OBS;
|
|
1012
|
-
const link = (a, b) => {
|
|
1013
|
-
if (!a || !b || a.id === b.id) return;
|
|
1014
|
-
const pair = a.id < b.id ? `${a.id}|${b.id}` : `${b.id}|${a.id}`;
|
|
1015
|
-
const existing = edgeByPair.get(pair);
|
|
1016
|
-
if (existing) {
|
|
1017
|
-
if (!existing.sourceObservationIds.includes(obs.id)) existing.sourceObservationIds.push(obs.id);
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (budget <= 0) return;
|
|
1021
|
-
budget -= 1;
|
|
1022
|
-
const edge = {
|
|
1023
|
-
id: generateId("ge"),
|
|
1024
|
-
type: "related_to",
|
|
1025
|
-
sourceNodeId: a.id,
|
|
1026
|
-
targetNodeId: b.id,
|
|
1027
|
-
weight: HEURISTIC_EDGE_WEIGHT,
|
|
1028
|
-
sourceObservationIds: [obs.id],
|
|
1029
|
-
createdAt: now
|
|
1030
|
-
};
|
|
1031
|
-
edgeByPair.set(pair, edge);
|
|
1032
|
-
edges.push(edge);
|
|
1033
|
-
};
|
|
1034
|
-
const fileNodes = (obs.files ?? []).map((f) => nodeFor("file", f, obs.id));
|
|
1035
|
-
const conceptNodes = (obs.concepts ?? []).map((c) => nodeFor("concept", c, obs.id));
|
|
1036
|
-
for (const concept of conceptNodes) for (const file of fileNodes) link(concept, file);
|
|
1037
|
-
for (let i = 0; i + 1 < conceptNodes.length; i++) link(conceptNodes[i], conceptNodes[i + 1]);
|
|
1038
|
-
for (let i = 0; i + 1 < fileNodes.length; i++) link(fileNodes[i], fileNodes[i + 1]);
|
|
1039
|
-
}
|
|
1040
|
-
return {
|
|
1041
|
-
nodes,
|
|
1042
|
-
edges
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
async function persistGraphDelta(kv, rawNodes, rawEdges) {
|
|
1046
|
-
const nodes = rawNodes.map((node) => scrubRecord(node));
|
|
1047
|
-
const edges = rawEdges.map((edge) => scrubRecord(edge));
|
|
1048
|
-
return withKeyedLock("graph:persist", async () => {
|
|
1049
|
-
const snap = await readSnapshotStrict(kv) ?? emptySnapshot();
|
|
1050
|
-
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1051
|
-
let newNodeCount = 0;
|
|
1052
|
-
let newEdgeCount = 0;
|
|
1053
|
-
let snapMutated = false;
|
|
1054
|
-
const newEdgesForTopCheck = [];
|
|
1055
|
-
const idRemap = /* @__PURE__ */ new Map();
|
|
1056
|
-
for (const node of nodes) {
|
|
1057
|
-
const indexKey = nameIndexKey(node.type, node.name);
|
|
1058
|
-
const existingId = await kv.get(KV.graphNameIndex, indexKey);
|
|
1059
|
-
let existing = null;
|
|
1060
|
-
if (existingId) {
|
|
1061
|
-
existing = await kv.get(KV.graphNodes, existingId);
|
|
1062
|
-
if (existing && snap.resetAt && typeof existing.createdAt === "string" && existing.createdAt < snap.resetAt) existing = null;
|
|
1063
|
-
}
|
|
1064
|
-
if (existing) {
|
|
1065
|
-
idRemap.set(node.id, existing.id);
|
|
1066
|
-
const merged = mergeNode(existing, node, capturedAt);
|
|
1067
|
-
await kv.set(KV.graphNodes, existing.id, merged);
|
|
1068
|
-
const topIdx = snap.topNodes.findIndex((n) => n.id === existing.id);
|
|
1069
|
-
if (topIdx !== -1) {
|
|
1070
|
-
snap.topNodes[topIdx] = merged;
|
|
1071
|
-
snapMutated = true;
|
|
1072
|
-
}
|
|
1073
|
-
} else {
|
|
1074
|
-
node.sourceObservationIds = boundSources([], node.sourceObservationIds ?? []);
|
|
1075
|
-
await kv.set(KV.graphNodes, node.id, node);
|
|
1076
|
-
await kv.set(KV.graphNameIndex, indexKey, node.id);
|
|
1077
|
-
await kv.set(KV.graphNodeDegree, node.id, 0);
|
|
1078
|
-
snap.stats.totalNodes += 1;
|
|
1079
|
-
snap.stats.nodesByType[node.type] = (snap.stats.nodesByType[node.type] ?? 0) + 1;
|
|
1080
|
-
newNodeCount += 1;
|
|
1081
|
-
if (snap.topNodes.length < SNAPSHOT_TOP_NODES) {
|
|
1082
|
-
snap.topNodes.push(node);
|
|
1083
|
-
snap.topDegrees[node.id] = 0;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
for (const rawEdge of edges) {
|
|
1088
|
-
const edge = {
|
|
1089
|
-
...rawEdge,
|
|
1090
|
-
sourceNodeId: idRemap.get(rawEdge.sourceNodeId) ?? rawEdge.sourceNodeId,
|
|
1091
|
-
targetNodeId: idRemap.get(rawEdge.targetNodeId) ?? rawEdge.targetNodeId
|
|
1092
|
-
};
|
|
1093
|
-
const eKey = edgeIndexKey(edge.sourceNodeId, edge.targetNodeId, edge.type);
|
|
1094
|
-
const existingId = await kv.get(KV.graphEdgeKey, eKey);
|
|
1095
|
-
let existing = null;
|
|
1096
|
-
if (existingId) {
|
|
1097
|
-
existing = await kv.get(KV.graphEdges, existingId);
|
|
1098
|
-
if (existing && snap.resetAt && typeof existing.createdAt === "string" && existing.createdAt < snap.resetAt) existing = null;
|
|
1099
|
-
}
|
|
1100
|
-
if (existing) {
|
|
1101
|
-
const merged = mergeEdge(existing, edge);
|
|
1102
|
-
await kv.set(KV.graphEdges, existing.id, merged);
|
|
1103
|
-
const topIdx = snap.topEdges.findIndex((e) => e.id === existing.id);
|
|
1104
|
-
if (topIdx !== -1) {
|
|
1105
|
-
snap.topEdges[topIdx] = merged;
|
|
1106
|
-
snapMutated = true;
|
|
1107
|
-
}
|
|
1108
|
-
} else {
|
|
1109
|
-
edge.sourceObservationIds = boundSources([], edge.sourceObservationIds ?? []);
|
|
1110
|
-
await kv.set(KV.graphEdges, edge.id, edge);
|
|
1111
|
-
await kv.set(KV.graphEdgeKey, eKey, edge.id);
|
|
1112
|
-
snap.stats.totalEdges += 1;
|
|
1113
|
-
snap.stats.edgesByType[edge.type] = (snap.stats.edgesByType[edge.type] ?? 0) + 1;
|
|
1114
|
-
newEdgeCount += 1;
|
|
1115
|
-
await applyDegreeDelta(kv, snap, edge.sourceNodeId, 1);
|
|
1116
|
-
await applyDegreeDelta(kv, snap, edge.targetNodeId, 1);
|
|
1117
|
-
newEdgesForTopCheck.push(edge);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
for (const edge of newEdgesForTopCheck) snapshotPushEdgeIfBothInTop(snap, edge);
|
|
1121
|
-
if (newNodeCount > 0 || newEdgeCount > 0 || snapMutated) {
|
|
1122
|
-
snap.updatedAt = capturedAt;
|
|
1123
|
-
snap.dirty = false;
|
|
1124
|
-
await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, snap);
|
|
1125
|
-
}
|
|
1126
|
-
return {
|
|
1127
|
-
newNodeCount,
|
|
1128
|
-
newEdgeCount
|
|
1129
|
-
};
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
const COMPACT_SCOPES = [
|
|
1133
|
-
"nodes",
|
|
1134
|
-
"edges",
|
|
1135
|
-
"history",
|
|
1136
|
-
"snapshot"
|
|
1137
|
-
];
|
|
1138
|
-
async function compactGraphProvenance(kv, opts = {}) {
|
|
1139
|
-
const { scope } = opts;
|
|
1140
|
-
if (scope !== void 0 && !COMPACT_SCOPES.includes(scope)) throw new Error(`unknown compact scope: ${String(scope)}`);
|
|
1141
|
-
const offset = opts.offset ?? 0;
|
|
1142
|
-
const dryRun = opts.dryRun === true;
|
|
1143
|
-
const limit = opts.limit ?? Number.POSITIVE_INFINITY;
|
|
1144
|
-
if (!Number.isInteger(offset) || offset < 0) throw new Error(`compact offset must be a non-negative integer: ${String(opts.offset)}`);
|
|
1145
|
-
if (!(limit === Number.POSITIVE_INFINITY || Number.isInteger(limit) && limit > 0)) throw new Error(`compact limit must be a positive integer: ${String(opts.limit)}`);
|
|
1146
|
-
const result = {
|
|
1147
|
-
nodesScanned: 0,
|
|
1148
|
-
nodesTrimmed: 0,
|
|
1149
|
-
edgesScanned: 0,
|
|
1150
|
-
edgesTrimmed: 0,
|
|
1151
|
-
historyScanned: 0,
|
|
1152
|
-
historyTrimmed: 0,
|
|
1153
|
-
idsRemoved: 0,
|
|
1154
|
-
snapshotTrimmed: false,
|
|
1155
|
-
nextOffset: null
|
|
1156
|
-
};
|
|
1157
|
-
const trimScope = async (listIds, recordScope) => {
|
|
1158
|
-
const allIds = [...new Set(await listIds())].filter((id) => typeof id === "string").sort();
|
|
1159
|
-
const end = Math.min(allIds.length, offset + limit);
|
|
1160
|
-
result.total = allIds.length;
|
|
1161
|
-
result.nextOffset = end < allIds.length ? end : null;
|
|
1162
|
-
let scanned = 0;
|
|
1163
|
-
let trimmed = 0;
|
|
1164
|
-
for (const id of allIds.slice(offset, end)) await withKeyedLock("graph:persist", async () => {
|
|
1165
|
-
const record = await kv.get(recordScope, id);
|
|
1166
|
-
if (!record) return;
|
|
1167
|
-
scanned += 1;
|
|
1168
|
-
const sources = record.sourceObservationIds ?? [];
|
|
1169
|
-
if (sources.length <= 32) return;
|
|
1170
|
-
const bounded = boundSources([], sources);
|
|
1171
|
-
result.idsRemoved += sources.length - bounded.length;
|
|
1172
|
-
if (!dryRun) await kv.set(recordScope, id, {
|
|
1173
|
-
...record,
|
|
1174
|
-
sourceObservationIds: bounded
|
|
1175
|
-
});
|
|
1176
|
-
trimmed += 1;
|
|
1177
|
-
});
|
|
1178
|
-
return {
|
|
1179
|
-
scanned,
|
|
1180
|
-
trimmed
|
|
1181
|
-
};
|
|
1182
|
-
};
|
|
1183
|
-
const trimSnapshot = () => withKeyedLock("graph:persist", async () => {
|
|
1184
|
-
const snap = await readSnapshot(kv);
|
|
1185
|
-
if (!snap) return;
|
|
1186
|
-
const trimList = (list) => list.map((r) => {
|
|
1187
|
-
const sources = r.sourceObservationIds ?? [];
|
|
1188
|
-
if (sources.length <= 32) return r;
|
|
1189
|
-
result.snapshotTrimmed = true;
|
|
1190
|
-
return {
|
|
1191
|
-
...r,
|
|
1192
|
-
sourceObservationIds: boundSources([], sources)
|
|
1193
|
-
};
|
|
1194
|
-
});
|
|
1195
|
-
const topNodes = trimList(snap.topNodes);
|
|
1196
|
-
const topEdges = trimList(snap.topEdges);
|
|
1197
|
-
if (result.snapshotTrimmed && !dryRun) await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, {
|
|
1198
|
-
...snap,
|
|
1199
|
-
topNodes,
|
|
1200
|
-
topEdges
|
|
1201
|
-
});
|
|
1202
|
-
});
|
|
1203
|
-
if (scope === void 0 || scope === "nodes") {
|
|
1204
|
-
const n = await trimScope(() => kv.list(KV.graphNameIndex), KV.graphNodes);
|
|
1205
|
-
result.nodesScanned = n.scanned;
|
|
1206
|
-
result.nodesTrimmed = n.trimmed;
|
|
1207
|
-
}
|
|
1208
|
-
if (scope === void 0 || scope === "edges") {
|
|
1209
|
-
const e = await trimScope(() => kv.list(KV.graphEdgeKey), KV.graphEdges);
|
|
1210
|
-
result.edgesScanned = e.scanned;
|
|
1211
|
-
result.edgesTrimmed = e.trimmed;
|
|
1212
|
-
}
|
|
1213
|
-
if (scope === void 0 || scope === "history") {
|
|
1214
|
-
const h = await trimScope(async () => (await kv.list(KV.graphEdgeHistory)).map((r) => r?.id), KV.graphEdgeHistory);
|
|
1215
|
-
result.historyScanned = h.scanned;
|
|
1216
|
-
result.historyTrimmed = h.trimmed;
|
|
1217
|
-
}
|
|
1218
|
-
if (scope === void 0 || scope === "snapshot") await trimSnapshot();
|
|
1219
|
-
if (scope === void 0) {
|
|
1220
|
-
delete result.total;
|
|
1221
|
-
result.nextOffset = null;
|
|
1222
|
-
}
|
|
1223
|
-
return result;
|
|
1224
|
-
}
|
|
1225
|
-
function registerGraphFunction(sdk, kv, provider) {
|
|
1226
|
-
sdk.registerFunction("mem::graph-extract", async (data) => {
|
|
1227
|
-
if (!data.observations || data.observations.length === 0) return {
|
|
1228
|
-
success: false,
|
|
1229
|
-
error: "No observations provided"
|
|
1230
|
-
};
|
|
1231
|
-
const obsIds = data.observations.map((o) => o.id);
|
|
1232
|
-
let nodes = [];
|
|
1233
|
-
let edges = [];
|
|
1234
|
-
try {
|
|
1235
|
-
const heuristic = extractGraphHeuristics(data.observations);
|
|
1236
|
-
nodes = heuristic.nodes;
|
|
1237
|
-
edges = heuristic.edges;
|
|
1238
|
-
} catch (err) {
|
|
1239
|
-
logger.warn("heuristic graph extraction failed", { error: err instanceof Error ? err.message : String(err) });
|
|
1240
|
-
}
|
|
1241
|
-
const llmEnabled = isGraphExtractionEnabled() && !provider.name.includes("noop");
|
|
1242
|
-
let llmError;
|
|
1243
|
-
if (llmEnabled) {
|
|
1244
|
-
const prompt = buildGraphExtractionPrompt(data.observations.map((o) => ({
|
|
1245
|
-
title: o.title,
|
|
1246
|
-
narrative: o.narrative,
|
|
1247
|
-
concepts: o.concepts,
|
|
1248
|
-
files: o.files,
|
|
1249
|
-
type: o.type
|
|
1250
|
-
})));
|
|
1251
|
-
try {
|
|
1252
|
-
const parsed = parseGraphXml(await provider.compress(GRAPH_EXTRACTION_SYSTEM, prompt), obsIds);
|
|
1253
|
-
nodes = nodes.concat(parsed.nodes);
|
|
1254
|
-
edges = edges.concat(parsed.edges);
|
|
1255
|
-
} catch (err) {
|
|
1256
|
-
llmError = err instanceof Error ? err.message : String(err);
|
|
1257
|
-
logger.error("LLM graph extraction failed", { error: llmError });
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
if (nodes.length === 0 && edges.length === 0) return llmError ? {
|
|
1261
|
-
success: false,
|
|
1262
|
-
error: llmError
|
|
1263
|
-
} : {
|
|
1264
|
-
success: true,
|
|
1265
|
-
nodesAdded: 0,
|
|
1266
|
-
edgesAdded: 0
|
|
1267
|
-
};
|
|
1268
|
-
try {
|
|
1269
|
-
const { newNodeCount, newEdgeCount } = await persistGraphDelta(kv, nodes, edges);
|
|
1270
|
-
await recordAudit(kv, "observe", "mem::graph-extract", obsIds, {
|
|
1271
|
-
nodesExtracted: nodes.length,
|
|
1272
|
-
edgesExtracted: edges.length
|
|
1273
|
-
});
|
|
1274
|
-
logger.info("Graph extraction complete", {
|
|
1275
|
-
nodes: nodes.length,
|
|
1276
|
-
edges: edges.length,
|
|
1277
|
-
newNodes: newNodeCount,
|
|
1278
|
-
newEdges: newEdgeCount,
|
|
1279
|
-
llm: llmEnabled && !llmError
|
|
1280
|
-
});
|
|
1281
|
-
return {
|
|
1282
|
-
success: true,
|
|
1283
|
-
nodesAdded: nodes.length,
|
|
1284
|
-
edgesAdded: edges.length
|
|
1285
|
-
};
|
|
1286
|
-
} catch (err) {
|
|
1287
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1288
|
-
logger.error("Graph extraction failed", { error: msg });
|
|
1289
|
-
return {
|
|
1290
|
-
success: false,
|
|
1291
|
-
error: msg
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
});
|
|
1295
|
-
sdk.registerFunction("mem::graph-query", async (data) => {
|
|
1296
|
-
const maxDepth = Math.min(data.maxDepth || 3, 5);
|
|
1297
|
-
const { limit, offset } = resolvePagination(data.limit, data.offset);
|
|
1298
|
-
if (!data.query && !data.startNodeId) {
|
|
1299
|
-
const snap = await readSnapshot(kv);
|
|
1300
|
-
if (snap && snap.stats.totalNodes > 0) return paginateFromSnapshot(snap, data.nodeType, limit, offset);
|
|
1301
|
-
return {
|
|
1302
|
-
nodes: [],
|
|
1303
|
-
edges: [],
|
|
1304
|
-
depth: 0,
|
|
1305
|
-
totalNodes: 0,
|
|
1306
|
-
totalEdges: 0,
|
|
1307
|
-
truncated: false,
|
|
1308
|
-
limit,
|
|
1309
|
-
offset,
|
|
1310
|
-
warning: "No graph snapshot available. Either no graph has been extracted yet, or you are on a legacy corpus from an older agentmemory build. Run POST /agentmemory/graph/snapshot-rebuild (safe up to ~25K nodes) or POST /agentmemory/graph/reset to wipe and let future extracts repopulate."
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
let allNodes;
|
|
1314
|
-
let allEdges;
|
|
1315
|
-
try {
|
|
1316
|
-
const [rawNodes, rawEdges] = await withTimeout(Promise.all([kv.list(KV.graphNodes), kv.list(KV.graphEdges)]), LIVE_ENUMERATION_BUDGET_MS, "graph-query enumeration");
|
|
1317
|
-
allNodes = rawNodes.filter((n) => !n.stale);
|
|
1318
|
-
allEdges = rawEdges.filter((e) => !e.stale);
|
|
1319
|
-
} catch (err) {
|
|
1320
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1321
|
-
logger.warn("Graph query enumeration timed out, using snapshot", { error: msg });
|
|
1322
|
-
const snap = await readSnapshot(kv);
|
|
1323
|
-
if (snap) return {
|
|
1324
|
-
...paginateFromSnapshot(snap, data.nodeType, limit, offset),
|
|
1325
|
-
warning: "Live graph enumeration exceeded budget. Query / startNodeId paths degrade on >25K-node corpora until a per-node edge index lands. Result reflects top-degree snapshot, not the requested walk."
|
|
1326
|
-
};
|
|
1327
|
-
return {
|
|
1328
|
-
nodes: [],
|
|
1329
|
-
edges: [],
|
|
1330
|
-
depth: 0,
|
|
1331
|
-
totalNodes: 0,
|
|
1332
|
-
totalEdges: 0,
|
|
1333
|
-
truncated: false,
|
|
1334
|
-
limit,
|
|
1335
|
-
offset,
|
|
1336
|
-
warning: "Graph enumeration exceeded budget and no snapshot is available."
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
if (data.query) {
|
|
1340
|
-
const lower = data.query.toLowerCase();
|
|
1341
|
-
return paginate(allNodes.filter((n) => n.name.toLowerCase().includes(lower) || Object.values(n.properties).some((v) => typeof v === "string" && v.toLowerCase().includes(lower))), allEdges, 0, limit, offset);
|
|
1342
|
-
}
|
|
1343
|
-
if (data.startNodeId) {
|
|
1344
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1345
|
-
const visitedEdges = /* @__PURE__ */ new Set();
|
|
1346
|
-
const resultNodes = [];
|
|
1347
|
-
const resultEdges = [];
|
|
1348
|
-
const queue = [{
|
|
1349
|
-
nodeId: data.startNodeId,
|
|
1350
|
-
depth: 0
|
|
1351
|
-
}];
|
|
1352
|
-
while (queue.length > 0) {
|
|
1353
|
-
const { nodeId, depth } = queue.shift();
|
|
1354
|
-
if (visited.has(nodeId) || depth > maxDepth) continue;
|
|
1355
|
-
visited.add(nodeId);
|
|
1356
|
-
const node = allNodes.find((n) => n.id === nodeId);
|
|
1357
|
-
if (node) {
|
|
1358
|
-
if (!data.nodeType || node.type === data.nodeType) resultNodes.push(node);
|
|
1359
|
-
}
|
|
1360
|
-
const neighborEdges = allEdges.filter((e) => e.sourceNodeId === nodeId || e.targetNodeId === nodeId);
|
|
1361
|
-
for (const edge of neighborEdges) {
|
|
1362
|
-
if (!visitedEdges.has(edge.id)) {
|
|
1363
|
-
visitedEdges.add(edge.id);
|
|
1364
|
-
resultEdges.push(edge);
|
|
1365
|
-
}
|
|
1366
|
-
const nextId = edge.sourceNodeId === nodeId ? edge.targetNodeId : edge.sourceNodeId;
|
|
1367
|
-
if (!visited.has(nextId)) queue.push({
|
|
1368
|
-
nodeId: nextId,
|
|
1369
|
-
depth: depth + 1
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return paginate(resultNodes, resultEdges, maxDepth, limit, offset);
|
|
1374
|
-
}
|
|
1375
|
-
return paginate([], [], 0, limit, offset);
|
|
1376
|
-
});
|
|
1377
|
-
sdk.registerFunction("mem::graph-stats", async () => {
|
|
1378
|
-
const snap = await readSnapshot(kv);
|
|
1379
|
-
if (snap) return {
|
|
1380
|
-
...snap.stats,
|
|
1381
|
-
fromSnapshot: true,
|
|
1382
|
-
updatedAt: snap.updatedAt,
|
|
1383
|
-
...snap.dirty ? { warning: "Snapshot is marked dirty (write was in-flight when read). Counts are eventually consistent." } : {}
|
|
1384
|
-
};
|
|
1385
|
-
return {
|
|
1386
|
-
totalNodes: 0,
|
|
1387
|
-
totalEdges: 0,
|
|
1388
|
-
nodesByType: {},
|
|
1389
|
-
edgesByType: {},
|
|
1390
|
-
fromSnapshot: false,
|
|
1391
|
-
warning: "No graph snapshot available. Run POST /agentmemory/graph/snapshot-rebuild (safe up to ~25K nodes) or POST /agentmemory/graph/reset to wipe and let future extracts repopulate."
|
|
1392
|
-
};
|
|
1393
|
-
});
|
|
1394
|
-
sdk.registerFunction("mem::graph-snapshot-rebuild", async (data) => {
|
|
1395
|
-
const started = Date.now();
|
|
1396
|
-
const forceRebuild = data?.force === true;
|
|
1397
|
-
try {
|
|
1398
|
-
if (!await readSnapshot(kv) && !forceRebuild) {
|
|
1399
|
-
logger.warn("Graph snapshot rebuild refused: no prior snapshot", { hint: "legacy corpus or empty store" });
|
|
1400
|
-
return {
|
|
1401
|
-
success: false,
|
|
1402
|
-
legacyCorpus: true,
|
|
1403
|
-
error: "No prior snapshot found. Rebuild would call kv.list on KV.graphNodes/Edges, which heartbeat-crashes the worker on corpora past the iii state response budget (~25K nodes). Either (a) call POST /agentmemory/graph/reset to drop into incremental-only mode and rebuild from new extracts, or (b) re-send with `force: true` if you're certain the corpus is small."
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
} catch (err) {
|
|
1407
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1408
|
-
logger.warn("Graph snapshot pre-flight read failed", { error: msg });
|
|
1409
|
-
}
|
|
1410
|
-
try {
|
|
1411
|
-
const outcome = await withKeyedLock("graph:persist", async () => {
|
|
1412
|
-
const [nodes, edges] = await withTimeout(Promise.all([kv.list(KV.graphNodes), kv.list(KV.graphEdges)]), LIVE_ENUMERATION_BUDGET_MS, "graph-snapshot-rebuild enumeration");
|
|
1413
|
-
if (nodes.length > REBUILD_SAFE_NODE_CEILING) return { tooLargeNodeCount: nodes.length };
|
|
1414
|
-
const liveNodes = nodes.filter((n) => !n.stale);
|
|
1415
|
-
const liveEdges = edges.filter((e) => !e.stale);
|
|
1416
|
-
const degree = /* @__PURE__ */ new Map();
|
|
1417
|
-
for (const e of liveEdges) {
|
|
1418
|
-
degree.set(e.sourceNodeId, (degree.get(e.sourceNodeId) ?? 0) + 1);
|
|
1419
|
-
degree.set(e.targetNodeId, (degree.get(e.targetNodeId) ?? 0) + 1);
|
|
1420
|
-
}
|
|
1421
|
-
const BATCH_SIZE = 100;
|
|
1422
|
-
for (let i = 0; i < liveNodes.length; i += BATCH_SIZE) {
|
|
1423
|
-
const batch = liveNodes.slice(i, i + BATCH_SIZE);
|
|
1424
|
-
await Promise.all(batch.flatMap((n) => [kv.set(KV.graphNameIndex, nameIndexKey(n.type, n.name), n.id), kv.set(KV.graphNodeDegree, n.id, degree.get(n.id) ?? 0)]));
|
|
1425
|
-
}
|
|
1426
|
-
for (let i = 0; i < liveEdges.length; i += BATCH_SIZE) {
|
|
1427
|
-
const batch = liveEdges.slice(i, i + BATCH_SIZE);
|
|
1428
|
-
await Promise.all(batch.map((e) => kv.set(KV.graphEdgeKey, edgeIndexKey(e.sourceNodeId, e.targetNodeId, e.type), e.id)));
|
|
1429
|
-
}
|
|
1430
|
-
const rebuilt = buildSnapshotFromArrays(nodes, edges);
|
|
1431
|
-
await kv.set(KV.graphSnapshot, SNAPSHOT_KEY, rebuilt);
|
|
1432
|
-
return { snap: rebuilt };
|
|
1433
|
-
});
|
|
1434
|
-
if ("tooLargeNodeCount" in outcome) {
|
|
1435
|
-
const totalNodes = outcome.tooLargeNodeCount;
|
|
1436
|
-
logger.warn("Graph snapshot rebuild aborted: corpus too large", {
|
|
1437
|
-
totalNodes,
|
|
1438
|
-
ceiling: REBUILD_SAFE_NODE_CEILING
|
|
1439
|
-
});
|
|
1440
|
-
return {
|
|
1441
|
-
success: false,
|
|
1442
|
-
tooLarge: true,
|
|
1443
|
-
totalNodes,
|
|
1444
|
-
ceiling: REBUILD_SAFE_NODE_CEILING,
|
|
1445
|
-
error: `Corpus has ${totalNodes} graph nodes; safe-rebuild ceiling is ${REBUILD_SAFE_NODE_CEILING}. Run POST /agentmemory/graph/reset to wipe and let future extracts rebuild incrementally.`
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
const { snap } = outcome;
|
|
1449
|
-
const tookMs = Date.now() - started;
|
|
1450
|
-
logger.info("Graph snapshot rebuilt", {
|
|
1451
|
-
totalNodes: snap.stats.totalNodes,
|
|
1452
|
-
totalEdges: snap.stats.totalEdges,
|
|
1453
|
-
topNodes: snap.topNodes.length,
|
|
1454
|
-
topEdges: snap.topEdges.length,
|
|
1455
|
-
tookMs
|
|
1456
|
-
});
|
|
1457
|
-
return {
|
|
1458
|
-
success: true,
|
|
1459
|
-
...snap.stats,
|
|
1460
|
-
topNodes: snap.topNodes.length,
|
|
1461
|
-
topEdges: snap.topEdges.length,
|
|
1462
|
-
updatedAt: snap.updatedAt,
|
|
1463
|
-
tookMs
|
|
1464
|
-
};
|
|
1465
|
-
} catch (err) {
|
|
1466
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1467
|
-
logger.error("Graph snapshot rebuild failed", { error: msg });
|
|
1468
|
-
return {
|
|
1469
|
-
success: false,
|
|
1470
|
-
error: msg
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
});
|
|
1474
|
-
sdk.registerFunction("mem::graph-compact", async (data) => {
|
|
1475
|
-
const started = Date.now();
|
|
1476
|
-
try {
|
|
1477
|
-
const result = await compactGraphProvenance(kv, data ?? {});
|
|
1478
|
-
const tookMs = Date.now() - started;
|
|
1479
|
-
logger.info("Graph provenance compacted", {
|
|
1480
|
-
...result,
|
|
1481
|
-
tookMs
|
|
1482
|
-
});
|
|
1483
|
-
if (result.idsRemoved > 0) await safeAudit(kv, "graph_compact", "mem::graph-compact", [], {
|
|
1484
|
-
scope: data?.scope ?? "all",
|
|
1485
|
-
offset: data?.offset,
|
|
1486
|
-
limit: data?.limit,
|
|
1487
|
-
nodesTrimmed: result.nodesTrimmed,
|
|
1488
|
-
edgesTrimmed: result.edgesTrimmed,
|
|
1489
|
-
historyTrimmed: result.historyTrimmed,
|
|
1490
|
-
idsRemoved: result.idsRemoved,
|
|
1491
|
-
snapshotTrimmed: result.snapshotTrimmed
|
|
1492
|
-
});
|
|
1493
|
-
return {
|
|
1494
|
-
success: true,
|
|
1495
|
-
...result,
|
|
1496
|
-
tookMs
|
|
1497
|
-
};
|
|
1498
|
-
} catch (err) {
|
|
1499
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1500
|
-
logger.error("Graph provenance compaction failed", { error: msg });
|
|
1501
|
-
return {
|
|
1502
|
-
success: false,
|
|
1503
|
-
error: msg
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
});
|
|
1507
|
-
sdk.registerFunction("mem::graph-reset", async () => {
|
|
1508
|
-
const started = Date.now();
|
|
1509
|
-
const resetSnapshot = {
|
|
1510
|
-
...emptySnapshot(),
|
|
1511
|
-
resetAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1512
|
-
};
|
|
1513
|
-
await withKeyedLock("graph:persist", () => kv.set(KV.graphSnapshot, SNAPSHOT_KEY, resetSnapshot));
|
|
1514
|
-
const counts = { [KV.graphSnapshot]: 1 };
|
|
1515
|
-
const tookMs = Date.now() - started;
|
|
1516
|
-
logger.info("Graph state reset", {
|
|
1517
|
-
counts,
|
|
1518
|
-
tookMs
|
|
1519
|
-
});
|
|
1520
|
-
return {
|
|
1521
|
-
success: true,
|
|
1522
|
-
cleared: counts,
|
|
1523
|
-
tookMs
|
|
1524
|
-
};
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
const GRAPH_COMPACT_BOOT_KEY = `graph-compact-on-boot:v1`;
|
|
1528
|
-
const GRAPH_COMPACT_BOOT_FUNCTION_ID = "mem::graph-compact-on-boot";
|
|
1529
|
-
const GRAPH_COMPACT_BOOT_DELAY_MS = 3e3;
|
|
1530
|
-
const SLICED_SCOPES = [
|
|
1531
|
-
"nodes",
|
|
1532
|
-
"edges",
|
|
1533
|
-
"history"
|
|
1534
|
-
];
|
|
1535
|
-
const DEFAULT_SLICE_SIZE = 100;
|
|
1536
|
-
const DEFAULT_PAUSE_MS = 200;
|
|
1537
|
-
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
1538
|
-
const DEFAULT_RETRY_DELAY_MS = 2e3;
|
|
1539
|
-
let current = { state: "pending" };
|
|
1540
|
-
let running = null;
|
|
1541
|
-
function getGraphCompactBootStatus() {
|
|
1542
|
-
return { ...current };
|
|
1543
|
-
}
|
|
1544
|
-
function setGraphCompactBootDisabled() {
|
|
1545
|
-
current = { state: "off" };
|
|
1546
|
-
}
|
|
1547
|
-
function statusFrom(p) {
|
|
1548
|
-
return {
|
|
1549
|
-
state: p.status,
|
|
1550
|
-
phase: p.phase,
|
|
1551
|
-
processed: p.offset,
|
|
1552
|
-
total: p.phaseTotal,
|
|
1553
|
-
scanned: p.scanned,
|
|
1554
|
-
trimmed: p.trimmed,
|
|
1555
|
-
idsRemoved: p.idsRemoved,
|
|
1556
|
-
startedAt: p.startedAt,
|
|
1557
|
-
completedAt: p.completedAt,
|
|
1558
|
-
error: p.error
|
|
1559
|
-
};
|
|
1560
|
-
}
|
|
1561
|
-
function isResumable(p) {
|
|
1562
|
-
return !!p && p.version === 1 && p.cap === 32 && (p.phase === "snapshot" || SLICED_SCOPES.includes(p.phase)) && Number.isInteger(p.offset) && p.offset >= 0;
|
|
1563
|
-
}
|
|
1564
|
-
function storedPlan(p, phase) {
|
|
1565
|
-
const plan = p.pending;
|
|
1566
|
-
if (!plan || plan.phase !== phase || plan.offset !== p.offset) return null;
|
|
1567
|
-
return [
|
|
1568
|
-
plan.scanned,
|
|
1569
|
-
plan.trimmed,
|
|
1570
|
-
plan.idsRemoved
|
|
1571
|
-
].every((n) => Number.isInteger(n) && n >= 0) ? plan : null;
|
|
1572
|
-
}
|
|
1573
|
-
function scannedOf(scope, r) {
|
|
1574
|
-
if (scope === "nodes") return [r.nodesScanned, r.nodesTrimmed];
|
|
1575
|
-
if (scope === "edges") return [r.edgesScanned, r.edgesTrimmed];
|
|
1576
|
-
return [r.historyScanned, r.historyTrimmed];
|
|
1577
|
-
}
|
|
1578
|
-
function describeGraphCompactBoot(s) {
|
|
1579
|
-
if (s.state === "off") return "off (AGENTMEMORY_GRAPH_COMPACT_ON_BOOT=false)";
|
|
1580
|
-
if (s.state === "pending") return "pending";
|
|
1581
|
-
const where = s.phase === "snapshot" || s.total === null || s.total === void 0 ? `${s.phase ?? "nodes"}` : `${s.phase} ${s.processed ?? 0} of ${s.total}`;
|
|
1582
|
-
if (s.state === "running") return `running, ${where}`;
|
|
1583
|
-
if (s.state === "failed") return `failed at ${where}: ${s.error ?? "unknown error"}`;
|
|
1584
|
-
return `done, ${s.trimmed ?? 0} records trimmed, ${s.idsRemoved ?? 0} ids removed`;
|
|
1585
|
-
}
|
|
1586
|
-
function runGraphCompactOnBoot(kv, opts = {}) {
|
|
1587
|
-
if (running) return running;
|
|
1588
|
-
running = runOnce(kv, opts).finally(() => {
|
|
1589
|
-
running = null;
|
|
1590
|
-
});
|
|
1591
|
-
return running;
|
|
1592
|
-
}
|
|
1593
|
-
async function runOnce(kv, opts) {
|
|
1594
|
-
const sliceSize = opts.sliceSize ?? DEFAULT_SLICE_SIZE;
|
|
1595
|
-
const pauseMs = opts.pauseMs ?? DEFAULT_PAUSE_MS;
|
|
1596
|
-
const maxAttempts = Math.max(1, opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
|
|
1597
|
-
const retryDelayMs = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
1598
|
-
const log = opts.log ?? (() => {});
|
|
1599
|
-
const warn = opts.warn ?? log;
|
|
1600
|
-
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1601
|
-
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1602
|
-
const attempt = async (fn) => {
|
|
1603
|
-
let lastErr;
|
|
1604
|
-
for (let i = 1; i <= maxAttempts; i++) try {
|
|
1605
|
-
return await fn();
|
|
1606
|
-
} catch (err) {
|
|
1607
|
-
lastErr = err;
|
|
1608
|
-
if (i < maxAttempts) await sleep(retryDelayMs * i);
|
|
1609
|
-
}
|
|
1610
|
-
throw lastErr;
|
|
1611
|
-
};
|
|
1612
|
-
let progress = null;
|
|
1613
|
-
const save = async () => {
|
|
1614
|
-
if (!progress) return;
|
|
1615
|
-
progress.updatedAt = now().toISOString();
|
|
1616
|
-
current = statusFrom(progress);
|
|
1617
|
-
await attempt(() => kv.set(KV.config, GRAPH_COMPACT_BOOT_KEY, progress));
|
|
1618
|
-
};
|
|
1619
|
-
try {
|
|
1620
|
-
const stored = await attempt(() => kv.get(KV.config, GRAPH_COMPACT_BOOT_KEY));
|
|
1621
|
-
if (isResumable(stored) && stored.status === "done") {
|
|
1622
|
-
current = statusFrom(stored);
|
|
1623
|
-
return getGraphCompactBootStatus();
|
|
1624
|
-
}
|
|
1625
|
-
const startedAt = now().toISOString();
|
|
1626
|
-
if (isResumable(stored)) {
|
|
1627
|
-
progress = {
|
|
1628
|
-
...stored,
|
|
1629
|
-
status: "running",
|
|
1630
|
-
error: void 0
|
|
1631
|
-
};
|
|
1632
|
-
log(`Graph provenance compaction: resuming in the background at ${progress.phase} ${progress.offset}`);
|
|
1633
|
-
} else {
|
|
1634
|
-
progress = {
|
|
1635
|
-
version: 1,
|
|
1636
|
-
cap: 32,
|
|
1637
|
-
status: "running",
|
|
1638
|
-
phase: "nodes",
|
|
1639
|
-
offset: 0,
|
|
1640
|
-
phaseTotal: null,
|
|
1641
|
-
scanned: 0,
|
|
1642
|
-
trimmed: 0,
|
|
1643
|
-
idsRemoved: 0,
|
|
1644
|
-
startedAt,
|
|
1645
|
-
updatedAt: startedAt
|
|
1646
|
-
};
|
|
1647
|
-
log(`Graph provenance compaction: started in the background (cap 32 ids per record)`);
|
|
1648
|
-
}
|
|
1649
|
-
await save();
|
|
1650
|
-
const p = progress;
|
|
1651
|
-
for (const scope of SLICED_SCOPES) {
|
|
1652
|
-
if (p.phase === "snapshot") break;
|
|
1653
|
-
if (SLICED_SCOPES.indexOf(p.phase) > SLICED_SCOPES.indexOf(scope)) continue;
|
|
1654
|
-
p.phase = scope;
|
|
1655
|
-
let phaseTrimmed = 0;
|
|
1656
|
-
let phaseScanned = 0;
|
|
1657
|
-
for (;;) {
|
|
1658
|
-
const slice = {
|
|
1659
|
-
scope,
|
|
1660
|
-
offset: p.offset,
|
|
1661
|
-
limit: sliceSize
|
|
1662
|
-
};
|
|
1663
|
-
let plan = storedPlan(p, scope);
|
|
1664
|
-
if (!plan) {
|
|
1665
|
-
const r = await attempt(() => compactGraphProvenance(kv, {
|
|
1666
|
-
...slice,
|
|
1667
|
-
dryRun: true
|
|
1668
|
-
}));
|
|
1669
|
-
const [scanned, trimmed] = scannedOf(scope, r);
|
|
1670
|
-
plan = {
|
|
1671
|
-
phase: scope,
|
|
1672
|
-
offset: p.offset,
|
|
1673
|
-
scanned,
|
|
1674
|
-
trimmed,
|
|
1675
|
-
idsRemoved: r.idsRemoved,
|
|
1676
|
-
nextOffset: r.nextOffset,
|
|
1677
|
-
total: r.total ?? null
|
|
1678
|
-
};
|
|
1679
|
-
if (plan.trimmed > 0) {
|
|
1680
|
-
p.pending = plan;
|
|
1681
|
-
await save();
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
if (plan.trimmed > 0) await attempt(() => compactGraphProvenance(kv, slice));
|
|
1685
|
-
phaseScanned += plan.scanned;
|
|
1686
|
-
phaseTrimmed += plan.trimmed;
|
|
1687
|
-
p.scanned += plan.scanned;
|
|
1688
|
-
p.trimmed += plan.trimmed;
|
|
1689
|
-
p.idsRemoved += plan.idsRemoved;
|
|
1690
|
-
p.pending = void 0;
|
|
1691
|
-
p.phaseTotal = plan.total;
|
|
1692
|
-
p.offset = plan.nextOffset ?? p.phaseTotal ?? p.offset;
|
|
1693
|
-
await save();
|
|
1694
|
-
if (plan.nextOffset === null) break;
|
|
1695
|
-
if (pauseMs > 0) await sleep(pauseMs);
|
|
1696
|
-
}
|
|
1697
|
-
log(`Graph provenance compaction: ${scope} done (${phaseScanned} checked, ${phaseTrimmed} trimmed)`);
|
|
1698
|
-
p.phase = SLICED_SCOPES[SLICED_SCOPES.indexOf(scope) + 1] ?? "snapshot";
|
|
1699
|
-
p.offset = 0;
|
|
1700
|
-
p.phaseTotal = null;
|
|
1701
|
-
await save();
|
|
1702
|
-
}
|
|
1703
|
-
let snapPlan = storedPlan(p, "snapshot");
|
|
1704
|
-
if (!snapPlan) {
|
|
1705
|
-
const dry = await attempt(() => compactGraphProvenance(kv, {
|
|
1706
|
-
scope: "snapshot",
|
|
1707
|
-
dryRun: true
|
|
1708
|
-
}));
|
|
1709
|
-
snapPlan = {
|
|
1710
|
-
phase: "snapshot",
|
|
1711
|
-
offset: p.offset,
|
|
1712
|
-
scanned: 0,
|
|
1713
|
-
trimmed: dry.snapshotTrimmed ? 1 : 0,
|
|
1714
|
-
idsRemoved: 0,
|
|
1715
|
-
nextOffset: null,
|
|
1716
|
-
total: null
|
|
1717
|
-
};
|
|
1718
|
-
if (snapPlan.trimmed > 0) {
|
|
1719
|
-
p.pending = snapPlan;
|
|
1720
|
-
await save();
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
if (snapPlan.trimmed > 0) await attempt(() => compactGraphProvenance(kv, { scope: "snapshot" }));
|
|
1724
|
-
const snapshotTrimmed = snapPlan.trimmed > 0;
|
|
1725
|
-
p.trimmed += snapPlan.trimmed;
|
|
1726
|
-
p.pending = void 0;
|
|
1727
|
-
p.status = "done";
|
|
1728
|
-
p.completedAt = now().toISOString();
|
|
1729
|
-
await save();
|
|
1730
|
-
if (p.idsRemoved > 0) await safeAudit(kv, "graph_compact", GRAPH_COMPACT_BOOT_FUNCTION_ID, [], {
|
|
1731
|
-
trigger: "boot",
|
|
1732
|
-
recordsTrimmed: p.trimmed,
|
|
1733
|
-
idsRemoved: p.idsRemoved,
|
|
1734
|
-
snapshotTrimmed
|
|
1735
|
-
});
|
|
1736
|
-
log(`Graph provenance compaction: done (${p.scanned} records checked, ${p.trimmed} trimmed, ${p.idsRemoved} ids removed)`);
|
|
1737
|
-
return getGraphCompactBootStatus();
|
|
1738
|
-
} catch (err) {
|
|
1739
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1740
|
-
if (progress) {
|
|
1741
|
-
progress.status = "failed";
|
|
1742
|
-
progress.error = msg;
|
|
1743
|
-
current = statusFrom(progress);
|
|
1744
|
-
try {
|
|
1745
|
-
await kv.set(KV.config, GRAPH_COMPACT_BOOT_KEY, {
|
|
1746
|
-
...progress,
|
|
1747
|
-
updatedAt: now().toISOString()
|
|
1748
|
-
});
|
|
1749
|
-
} catch {}
|
|
1750
|
-
} else current = {
|
|
1751
|
-
state: "failed",
|
|
1752
|
-
error: msg
|
|
1753
|
-
};
|
|
1754
|
-
warn(`Graph provenance compaction failed (${describeGraphCompactBoot(current)}). It resumes on the next start; to run it now, POST /agentmemory/graph/compact.`);
|
|
1755
|
-
return getGraphCompactBootStatus();
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
//#endregion
|
|
1759
|
-
//#region src/functions/status.ts
|
|
1760
|
-
const FAILURE_RATE_THRESHOLD = .2;
|
|
1761
|
-
const FAILURE_MIN_CALLS = 5;
|
|
1762
|
-
const GRAPH_SNAPSHOT_STALE_SECONDS = 1440 * 60;
|
|
1763
|
-
const LEVEL_RANK = {
|
|
1764
|
-
ok: 0,
|
|
1765
|
-
info: 1,
|
|
1766
|
-
warn: 2,
|
|
1767
|
-
error: 3
|
|
1768
|
-
};
|
|
1769
|
-
const FUNCTION_FIXES = {
|
|
1770
|
-
"mem::summarize": "Summaries come from your LLM provider. Check the provider key, model name and rate limits in ~/.agentmemory/.env, then look for provider errors in the server log.",
|
|
1771
|
-
"mem::compress": "Compression calls your LLM provider. Check the provider key and model, or set AGENTMEMORY_AUTO_COMPRESS=false to use zero-LLM compression.",
|
|
1772
|
-
"mem::graph-extract": "Graph extraction calls your LLM provider. Check the provider key and model, or set GRAPH_EXTRACTION_ENABLED=false."
|
|
1773
|
-
};
|
|
1774
|
-
const LLM_KEY_FIX = "Add one LLM provider key (for example ANTHROPIC_API_KEY or OPENAI_API_KEY) to ~/.agentmemory/.env and restart.";
|
|
1775
|
-
const OFF_WITHOUT_LLM = [
|
|
1776
|
-
"session summaries",
|
|
1777
|
-
"consolidation into semantic facts and procedures",
|
|
1778
|
-
"typed graph extraction",
|
|
1779
|
-
"LLM observation compression",
|
|
1780
|
-
"skill extraction from finished sessions"
|
|
1781
|
-
];
|
|
1782
|
-
const BM25_EXPLAINER = "The keyword index holds observations, memories and lessons, so its total is larger than the observation count.";
|
|
1783
|
-
function describeHealthAlert(slug) {
|
|
1784
|
-
let m;
|
|
1785
|
-
if (m = /^memory_(warn|critical)_(\d+)%_rss(\d+)mb$/.exec(slug)) return {
|
|
1786
|
-
level: m[1] === "critical" ? "error" : "warn",
|
|
1787
|
-
message: `Memory ${m[1] === "critical" ? "critically high" : "high"}: ${m[2]}% of the heap limit in use, process memory ${m[3]} MB.`,
|
|
1788
|
-
fix: "Restart agentmemory to release memory. If it climbs back, raise the limit with NODE_OPTIONS=--max-old-space-size=4096."
|
|
1789
|
-
};
|
|
1790
|
-
if (m = /^cpu_(warn|critical)_(\d+)%$/.exec(slug)) return {
|
|
1791
|
-
level: m[1] === "critical" ? "error" : "warn",
|
|
1792
|
-
message: `CPU ${m[1] === "critical" ? "critically high" : "high"}: ${m[2]}%.`,
|
|
1793
|
-
fix: "Usually a burst of observations or a rebuild. If it stays high with no agent running, check the server log for a loop."
|
|
1794
|
-
};
|
|
1795
|
-
if (m = /^event_loop_lag_(warn|critical)_(\d+)ms$/.exec(slug)) return {
|
|
1796
|
-
level: m[1] === "critical" ? "error" : "warn",
|
|
1797
|
-
message: `The worker is ${m[2]} ms behind on its work (event loop delay), so requests answer slowly.`,
|
|
1798
|
-
fix: "It usually clears after an import or index rebuild finishes. If it persists, restart agentmemory."
|
|
1799
|
-
};
|
|
1800
|
-
if (slug === "stream_relay_down") return {
|
|
1801
|
-
level: "warn",
|
|
1802
|
-
message: "Live updates are not reaching the viewer: the engine stopped relaying stream events, which happens after Redis restarts or drops the connection. Data is still saved.",
|
|
1803
|
-
fix: "Restart agentmemory (npx @agentmemory/agentmemory stop, then start) so the engine subscribes to Redis again, then reload the viewer."
|
|
1804
|
-
};
|
|
1805
|
-
if (slug === "connection_reconnecting") return {
|
|
1806
|
-
level: "warn",
|
|
1807
|
-
message: "The connection to the iii engine dropped and is reconnecting.",
|
|
1808
|
-
fix: "Wait a few seconds. If it does not recover, check that the engine is running with npx @agentmemory/agentmemory status."
|
|
1809
|
-
};
|
|
1810
|
-
if (m = /^connection_(.+)$/.exec(slug)) return {
|
|
1811
|
-
level: "error",
|
|
1812
|
-
message: `The connection to the iii engine is ${m[1]}, so nothing can be stored or searched.`,
|
|
1813
|
-
fix: "Restart agentmemory. Run npx @agentmemory/agentmemory doctor if the engine does not start."
|
|
1814
|
-
};
|
|
1815
|
-
return {
|
|
1816
|
-
level: "warn",
|
|
1817
|
-
message: slug,
|
|
1818
|
-
fix: "Look for this alert in the server log."
|
|
1819
|
-
};
|
|
1820
|
-
}
|
|
1821
|
-
function isOffWithoutLlm(functionId, providerKind) {
|
|
1822
|
-
return providerKind === "noop" && functionId in FUNCTION_FIXES;
|
|
1823
|
-
}
|
|
1824
|
-
function markLlmFunctions(metrics, providerKind) {
|
|
1825
|
-
return metrics.map((m) => ({
|
|
1826
|
-
...m,
|
|
1827
|
-
offWithoutLlm: isOffWithoutLlm(m.functionId, providerKind)
|
|
1828
|
-
}));
|
|
1829
|
-
}
|
|
1830
|
-
function secondsBetween(later, earlierIso) {
|
|
1831
|
-
if (!earlierIso) return null;
|
|
1832
|
-
const earlier = Date.parse(earlierIso);
|
|
1833
|
-
if (Number.isNaN(earlier)) return null;
|
|
1834
|
-
return Math.max(0, Math.round((later.getTime() - earlier) / 1e3));
|
|
1835
|
-
}
|
|
1836
|
-
function describeBackfillState(state) {
|
|
1837
|
-
if (state === "waiting-for-opt-in") return "paused, waiting for opt-in";
|
|
1838
|
-
if (state === "paused") return "paused, retried on the next start";
|
|
1839
|
-
return state;
|
|
1840
|
-
}
|
|
1841
|
-
function vectorBackfillFix(state) {
|
|
1842
|
-
if (state === "waiting-for-opt-in") return "Backfill is paused. Set AGENTMEMORY_VECTOR_BACKFILL=all and restart to opt in. This calls the embedding provider and is capped per boot by AGENTMEMORY_VECTOR_BACKFILL_MAX.";
|
|
1843
|
-
if (state === "running") return "Backfill is running in the background in batches of AGENTMEMORY_VECTOR_BACKFILL_MAX with a short pause between batches. If agentmemory stops first, it continues on the next start. Check the server log for embedding provider errors if progress stops.";
|
|
1844
|
-
if (state === "paused") return "Backfill stopped because the embedding provider did not return vectors. Search still works by keyword. Check the embedding provider key and the server log; the remaining documents are retried on the next start.";
|
|
1845
|
-
return "Backfill is not running. Check the server log and embedding provider configuration. A full backfill requires AGENTMEMORY_VECTOR_BACKFILL=all and a restart, and can consume embedding provider tokens.";
|
|
1846
|
-
}
|
|
1847
|
-
function evaluateStatus(input) {
|
|
1848
|
-
const problems = [];
|
|
1849
|
-
if (!input.health) problems.push({
|
|
1850
|
-
level: "info",
|
|
1851
|
-
code: "health-check-unavailable",
|
|
1852
|
-
message: "The health monitor has no snapshot yet or did not answer in time, so its state was not checked.",
|
|
1853
|
-
fix: "The monitor writes a snapshot every 30 seconds after start. Wait a minute; if this stays, check the server log."
|
|
1854
|
-
});
|
|
1855
|
-
const healthStatus = input.health?.status;
|
|
1856
|
-
const alerts = input.health?.alerts ?? [];
|
|
1857
|
-
if (alerts.length === 0 && healthStatus === "critical") problems.push({
|
|
1858
|
-
level: "error",
|
|
1859
|
-
code: "health-critical",
|
|
1860
|
-
message: "The health monitor reports a critical state.",
|
|
1861
|
-
fix: "Check the server log, then restart agentmemory."
|
|
1862
|
-
});
|
|
1863
|
-
else if (alerts.length === 0 && healthStatus === "degraded") problems.push({
|
|
1864
|
-
level: "warn",
|
|
1865
|
-
code: "health-degraded",
|
|
1866
|
-
message: "The health monitor reports a degraded state.",
|
|
1867
|
-
fix: "Check the server log for the cause."
|
|
1868
|
-
});
|
|
1869
|
-
const described = alerts.map(describeHealthAlert);
|
|
1870
|
-
if (healthStatus !== "critical") for (const d of described) d.level = "warn";
|
|
1871
|
-
else if (described.length && !described.some((d) => d.level === "error")) described[0].level = "error";
|
|
1872
|
-
for (const d of described) problems.push({
|
|
1873
|
-
level: d.level,
|
|
1874
|
-
code: "health-alert",
|
|
1875
|
-
message: d.message,
|
|
1876
|
-
fix: d.fix
|
|
1877
|
-
});
|
|
1878
|
-
if (input.circuitBreaker?.state === "open") problems.push({
|
|
1879
|
-
level: "error",
|
|
1880
|
-
code: "provider-circuit-open",
|
|
1881
|
-
message: `LLM provider calls are paused after ${input.circuitBreaker.failures ?? 0} consecutive failures.`,
|
|
1882
|
-
fix: "Check the provider key and model in ~/.agentmemory/.env. Calls resume automatically once the provider answers again."
|
|
1883
|
-
});
|
|
1884
|
-
if (input.provider === "noop") problems.push({
|
|
1885
|
-
level: "info",
|
|
1886
|
-
code: "no-llm-provider",
|
|
1887
|
-
message: "No LLM provider is configured, so summaries, consolidation and graph extraction stay off. Search still works.",
|
|
1888
|
-
fix: LLM_KEY_FIX
|
|
1889
|
-
});
|
|
1890
|
-
const noLlm = input.provider === "noop";
|
|
1891
|
-
const functions = markLlmFunctions(input.functionMetrics, input.provider).map((m) => ({
|
|
1892
|
-
...m,
|
|
1893
|
-
failureRate: m.totalCalls > 0 ? m.failureCount / m.totalCalls : 0
|
|
1894
|
-
}));
|
|
1895
|
-
for (const fn of functions) {
|
|
1896
|
-
if (fn.offWithoutLlm) continue;
|
|
1897
|
-
if (fn.totalCalls < FAILURE_MIN_CALLS || fn.failureRate < FAILURE_RATE_THRESHOLD) continue;
|
|
1898
|
-
problems.push({
|
|
1899
|
-
level: "warn",
|
|
1900
|
-
code: "function-failing",
|
|
1901
|
-
message: `${fn.functionId} failed ${fn.failureCount} of ${fn.totalCalls} calls (${Math.round(fn.failureRate * 100)}%).`,
|
|
1902
|
-
fix: FUNCTION_FIXES[fn.functionId] ?? "Look for this function id in the server log to see the error."
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
const missingObservations = input.index.missingObservations;
|
|
1906
|
-
if (!input.index.keywordRebuildRunning && missingObservations !== null && missingObservations > 0) problems.push({
|
|
1907
|
-
level: "warn",
|
|
1908
|
-
code: "index-missing-observations",
|
|
1909
|
-
message: `${missingObservations} stored observations are not in the search index, so search cannot find them.`,
|
|
1910
|
-
fix: "Restart agentmemory: the boot reconcile re-indexes observations missing from the snapshot."
|
|
1911
|
-
});
|
|
1912
|
-
if (input.index.keywordRebuildRunning) problems.push({
|
|
1913
|
-
level: "info",
|
|
1914
|
-
code: "keyword-index-rebuilding",
|
|
1915
|
-
message: "The keyword index is still rebuilding from stored data, so the check for observations missing from search waits until it finishes.",
|
|
1916
|
-
fix: "No action needed. The check runs on the next health update after the rebuild."
|
|
1917
|
-
});
|
|
1918
|
-
else if (missingObservations === null) problems.push({
|
|
1919
|
-
level: "info",
|
|
1920
|
-
code: "index-check-unavailable",
|
|
1921
|
-
message: "The session store did not answer in time, so the search index was not checked for missing observations.",
|
|
1922
|
-
fix: "The check runs again on the next health update. If it keeps timing out, the store is overloaded: check the server log."
|
|
1923
|
-
});
|
|
1924
|
-
if (input.index.bm25Incomplete) problems.push({
|
|
1925
|
-
level: "error",
|
|
1926
|
-
code: "bm25-rebuild-incomplete",
|
|
1927
|
-
message: "The keyword index could not load every session at boot, so some observations are missing from search until the next successful rebuild.",
|
|
1928
|
-
fix: "Check the server log for the session listing failure. Restarting or the next cold-start search retries automatically."
|
|
1929
|
-
});
|
|
1930
|
-
if (input.index.pendingVectorBackfill > 0) problems.push({
|
|
1931
|
-
level: "info",
|
|
1932
|
-
code: "index-vector-backfill-pending",
|
|
1933
|
-
message: `${input.index.pendingVectorBackfill} documents are waiting for a vector embedding.`,
|
|
1934
|
-
fix: vectorBackfillFix(input.index.vectorBackfillState)
|
|
1935
|
-
});
|
|
1936
|
-
const persistence = input.indexPersistence ?? null;
|
|
1937
|
-
const vectorLeg = persistence?.vector ?? null;
|
|
1938
|
-
if (persistence && vectorLeg) {
|
|
1939
|
-
if (vectorLeg.lastError) problems.push({
|
|
1940
|
-
level: "error",
|
|
1941
|
-
code: "index-save-failing",
|
|
1942
|
-
message: `The vector index could not be saved: ${vectorLeg.lastError}. Search keeps working from memory, but vectors added since the last save are lost on restart.`,
|
|
1943
|
-
fix: "Check the server log for the failing state write. The next save retries automatically."
|
|
1944
|
-
});
|
|
1945
|
-
const dirtyAge = secondsBetween(input.now, vectorLeg.dirtySince ?? void 0);
|
|
1946
|
-
if (!vectorLeg.lastError && dirtyAge !== null && dirtyAge * 1e3 > 2 * persistence.saveIntervalMs) problems.push({
|
|
1947
|
-
level: "warn",
|
|
1948
|
-
code: "index-save-stale",
|
|
1949
|
-
message: `The vector index has unsaved changes from ${formatDuration(dirtyAge)} ago.`,
|
|
1950
|
-
fix: "After the first checkpoint, saves are batched by AGENTMEMORY_INDEX_SAVE_INTERVAL_MS. Check the server log for save errors or a save that never finishes."
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
if (persistence?.pendingLogError) problems.push({
|
|
1954
|
-
level: "warn",
|
|
1955
|
-
code: "index-pending-log-failing",
|
|
1956
|
-
message: `New vectors could not be written to the pending vector log: ${persistence.pendingLogError}. They are still kept by the next index save, but a crash before then means they are embedded again at the next start.`,
|
|
1957
|
-
fix: "Check the server log for the failing state write. Each new vector retries the log write."
|
|
1958
|
-
});
|
|
1959
|
-
if (persistence?.vectorCountShortfall) {
|
|
1960
|
-
const { expected, loaded } = persistence.vectorCountShortfall;
|
|
1961
|
-
problems.push({
|
|
1962
|
-
level: "warn",
|
|
1963
|
-
code: "index-vector-count-shortfall",
|
|
1964
|
-
message: `Only ${loaded} of ${expected} vectors loaded from the last save.`,
|
|
1965
|
-
fix: vectorBackfillFix(input.index.vectorBackfillState)
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
let graph = null;
|
|
1969
|
-
if (input.graph) {
|
|
1970
|
-
const ageSeconds = secondsBetween(input.now, input.graph.updatedAt);
|
|
1971
|
-
graph = {
|
|
1972
|
-
...input.graph,
|
|
1973
|
-
ageSeconds,
|
|
1974
|
-
extractionEnabled: input.graphExtractionEnabled
|
|
1975
|
-
};
|
|
1976
|
-
if (input.graphExtractionEnabled && !input.graph.fromSnapshot) problems.push({
|
|
1977
|
-
level: "warn",
|
|
1978
|
-
code: "graph-no-snapshot",
|
|
1979
|
-
message: "Graph extraction is on but no graph snapshot exists, so graph counts read as zero.",
|
|
1980
|
-
fix: "Use Rebuild Graph in the viewer, or POST /agentmemory/graph/snapshot-rebuild."
|
|
1981
|
-
});
|
|
1982
|
-
else if (input.graph.fromSnapshot && ageSeconds !== null && ageSeconds > GRAPH_SNAPSHOT_STALE_SECONDS) {
|
|
1983
|
-
const age = ageSeconds >= 2 * 86400 ? `${Math.floor(ageSeconds / 86400)} days` : `${Math.round(ageSeconds / 3600)} hours`;
|
|
1984
|
-
problems.push({
|
|
1985
|
-
level: "info",
|
|
1986
|
-
code: "graph-snapshot-stale",
|
|
1987
|
-
message: `The graph snapshot is ${age} old, so dashboard graph counts can lag the live graph.`,
|
|
1988
|
-
fix: "POST /agentmemory/graph/snapshot-rebuild refreshes it."
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
1991
|
-
if (input.graph.dirty) problems.push({
|
|
1992
|
-
level: "info",
|
|
1993
|
-
code: "graph-snapshot-dirty",
|
|
1994
|
-
message: "The graph snapshot was read while a write was in flight; counts are eventually consistent.",
|
|
1995
|
-
fix: "No action needed: the next graph write settles the counts."
|
|
1996
|
-
});
|
|
1997
|
-
}
|
|
1998
|
-
const compaction = input.graphCompaction ?? null;
|
|
1999
|
-
if (compaction?.state === "running") problems.push({
|
|
2000
|
-
level: "info",
|
|
2001
|
-
code: "graph-compaction-running",
|
|
2002
|
-
message: `Oversized graph provenance is being trimmed in the background (${describeGraphCompactBoot(compaction)}). Search and capture keep working.`,
|
|
2003
|
-
fix: "No action needed. It runs once, resumes after a restart and stops by itself. AGENTMEMORY_GRAPH_COMPACT_ON_BOOT=false turns it off."
|
|
2004
|
-
});
|
|
2005
|
-
else if (compaction?.state === "failed") problems.push({
|
|
2006
|
-
level: "warn",
|
|
2007
|
-
code: "graph-compaction-failed",
|
|
2008
|
-
message: `The one-time graph provenance compaction stopped: ${compaction.error ?? "unknown error"}. Graph search can stay slow until it finishes.`,
|
|
2009
|
-
fix: `It resumes on the next start. To run it now: ${graphCompactCommand(input.ports.rest)}`
|
|
2010
|
-
});
|
|
2011
|
-
if (input.auditLegacy?.status === "too-large" || input.auditLegacy?.status === "unreadable") {
|
|
2012
|
-
const size = input.auditLegacy.sizeBytes;
|
|
2013
|
-
const sizeText = typeof size === "number" ? ` (${Math.round(size / (1024 * 1024))} MiB)` : "";
|
|
2014
|
-
problems.push({
|
|
2015
|
-
level: "info",
|
|
2016
|
-
code: "audit-legacy-frozen",
|
|
2017
|
-
message: `An older audit log${sizeText} was left in place instead of being migrated into monthly scopes, so it is no longer rewritten but its rows do not show up in audit queries.`,
|
|
2018
|
-
fix: input.stateBackend === "redis" ? "Safe to ignore. To remove it, run redis-cli -u \"$AGENTMEMORY_REDIS_URL\" DEL state:mem:audit, then restart agentmemory." : "Safe to ignore. To remove it, stop agentmemory, delete mem%3Aaudit.bin from the state store directory, then start it again."
|
|
2019
|
-
});
|
|
2020
|
-
}
|
|
2021
|
-
if (input.capture === null) problems.push({
|
|
2022
|
-
level: "info",
|
|
2023
|
-
code: "capture-check-unavailable",
|
|
2024
|
-
message: "The capture inbox did not answer in time, so capture was not checked.",
|
|
2025
|
-
fix: "The check runs again on the next status request. If it keeps timing out, the state store is overloaded: check the server log."
|
|
2026
|
-
});
|
|
2027
|
-
problems.push(...captureProblems(input.capture ?? null, input.now, input.ports.rest));
|
|
2028
|
-
if (input.stateStore && !input.stateStore.ok) problems.push({
|
|
2029
|
-
level: "error",
|
|
2030
|
-
code: "state-store-unreachable",
|
|
2031
|
-
message: input.stateBackend === "redis" ? "The state store is not answering: the engine cannot reach Redis, so nothing is being saved or read." : "The state store is not answering: the engine did not complete a state read in time, so nothing is being saved or read.",
|
|
2032
|
-
fix: input.stateBackend === "redis" ? "Check that Redis is running and reachable at AGENTMEMORY_REDIS_URL (redis-cli -u \"$AGENTMEMORY_REDIS_URL\" ping should answer PONG). Once Redis is back, restart agentmemory: the engine does not resume the live viewer stream after a Redis restart." : "Check the server log for engine errors, then restart agentmemory."
|
|
2033
|
-
});
|
|
2034
|
-
const status = problems.reduce((worst, p) => LEVEL_RANK[p.level] > LEVEL_RANK[worst] ? p.level : worst, "ok");
|
|
2035
|
-
return {
|
|
2036
|
-
status,
|
|
2037
|
-
headline: statusHeadline(status, problems),
|
|
2038
|
-
checkedAt: input.now.toISOString(),
|
|
2039
|
-
service: {
|
|
2040
|
-
version: input.version,
|
|
2041
|
-
engineVersion: input.engineVersion,
|
|
2042
|
-
uptimeSeconds: input.uptimeSeconds,
|
|
2043
|
-
stateBackend: input.stateBackend,
|
|
2044
|
-
stateStore: input.stateStore ?? null,
|
|
2045
|
-
ports: input.ports
|
|
2046
|
-
},
|
|
2047
|
-
health: input.health,
|
|
2048
|
-
provider: {
|
|
2049
|
-
llm: input.provider,
|
|
2050
|
-
embeddings: input.embeddingProvider,
|
|
2051
|
-
circuitBreaker: input.circuitBreaker,
|
|
2052
|
-
offWithoutLlm: noLlm ? OFF_WITHOUT_LLM : []
|
|
2053
|
-
},
|
|
2054
|
-
index: {
|
|
2055
|
-
...input.index,
|
|
2056
|
-
breakdown: indexBreakdown(input.index)
|
|
2057
|
-
},
|
|
2058
|
-
indexPersistence: persistence,
|
|
2059
|
-
graph,
|
|
2060
|
-
graphCompaction: compaction,
|
|
2061
|
-
capture: input.capture ?? null,
|
|
2062
|
-
observeDedup: input.observeDedup ?? null,
|
|
2063
|
-
functions,
|
|
2064
|
-
flags: input.flags.map((flag) => flag.enabled && flag.needsLlm && noLlm ? {
|
|
2065
|
-
...flag,
|
|
2066
|
-
inactiveReason: "needs an LLM provider, none is configured"
|
|
2067
|
-
} : flag),
|
|
2068
|
-
problems
|
|
2069
|
-
};
|
|
2070
|
-
}
|
|
2071
|
-
const RECENT_DROP_MS = 1440 * 60 * 1e3;
|
|
2072
|
-
const CURL_AUTH_HEADER = "-H \"Authorization: Bearer ${AGENTMEMORY_SECRET:-$(cat ~/.agentmemory/secret)}\"";
|
|
2073
|
-
function captureRestBase(restPort) {
|
|
2074
|
-
return `http://localhost:${restPort ?? 3111}/agentmemory`;
|
|
2075
|
-
}
|
|
2076
|
-
function captureProblems(capture, now, restPort) {
|
|
2077
|
-
if (!capture) return [];
|
|
2078
|
-
const problems = [];
|
|
2079
|
-
const base = captureRestBase(restPort);
|
|
2080
|
-
const inbox = capture.inbox;
|
|
2081
|
-
if (inbox && inbox.dead > 0) problems.push({
|
|
2082
|
-
level: "warn",
|
|
2083
|
-
code: "capture-dead-letters",
|
|
2084
|
-
message: `${plural(inbox.dead, "captured observation")} could not be stored after ${plural(capture.policy.maxAttempts, "attempt")} and ${inbox.dead === 1 ? "is parked as a dead letter" : "are parked as dead letters"}${inbox.lastError ? `: ${inbox.lastError}` : "."}`,
|
|
2085
|
-
fix: `List them with curl -s ${CURL_AUTH_HEADER} '${base}/capture?status=dead'. After fixing the cause, retry them with curl -X POST ${base}/capture/retry ${CURL_AUTH_HEADER} -H "Content-Type: application/json" -d '{"all":true}'.`
|
|
2086
|
-
});
|
|
2087
|
-
if (inbox && inbox.retrying + inbox.pending > 0) problems.push({
|
|
2088
|
-
level: "info",
|
|
2089
|
-
code: "capture-retrying",
|
|
2090
|
-
message: `${plural(inbox.retrying + inbox.pending, "accepted observation")} ${inbox.retrying + inbox.pending === 1 ? "is" : "are"} waiting to be stored${inbox.lastError ? ` after a failure: ${inbox.lastError}` : ""}.`,
|
|
2091
|
-
fix: `No action needed: the server retries every ${Math.round(capture.policy.retryIntervalMs / 1e3)} s with backoff, up to ${capture.policy.maxAttempts} attempts, and again after a restart.`
|
|
2092
|
-
});
|
|
2093
|
-
const waiting = capture.spool.reduce((n, s) => n + s.records, 0);
|
|
2094
|
-
if (waiting > 0) problems.push({
|
|
2095
|
-
level: "info",
|
|
2096
|
-
code: "capture-spool-waiting",
|
|
2097
|
-
message: `${plural(waiting, "observation")} captured while the server was unreachable ${waiting === 1 ? "is" : "are"} waiting in the local capture spool.`,
|
|
2098
|
-
fix: `They are sent on the next hook call and at every start. To send them now: npx @agentmemory/agentmemory capture --drain, or curl -X POST ${base}/capture/drain ${CURL_AUTH_HEADER}.`
|
|
2099
|
-
});
|
|
2100
|
-
let dropped = 0;
|
|
2101
|
-
let latest = null;
|
|
2102
|
-
for (const spool of capture.spool) {
|
|
2103
|
-
dropped += spool.stats.dropped;
|
|
2104
|
-
const lastDrop = spool.stats.lastDropAt ? Date.parse(spool.stats.lastDropAt) : NaN;
|
|
2105
|
-
if (Number.isFinite(lastDrop) && (!latest || lastDrop > latest.at)) latest = {
|
|
2106
|
-
at: lastDrop,
|
|
2107
|
-
reason: spool.stats.lastDropReason ?? "unknown",
|
|
2108
|
-
maxBytes: spool.maxBytes,
|
|
2109
|
-
maxAgeHours: spool.maxAgeHours
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
if (latest && now.getTime() - latest.at < RECENT_DROP_MS) problems.push({
|
|
2113
|
-
level: "warn",
|
|
2114
|
-
code: "capture-spool-dropped",
|
|
2115
|
-
message: `The local capture spool dropped ${plural(dropped, "observation")} in total (latest reason: ${latest.reason}), so those tool calls are not in memory.`,
|
|
2116
|
-
fix: `The spool holds at most ${Math.round(latest.maxBytes / 1024)} KiB for ${latest.maxAgeHours} hours. Raise AGENTMEMORY_CAPTURE_SPOOL_MAX_BYTES, keep agentmemory running while agents work, or check that AGENTMEMORY_CAPTURE_SPOOL is not false.`
|
|
2117
|
-
});
|
|
2118
|
-
return problems;
|
|
2119
|
-
}
|
|
2120
|
-
function statusHeadline(status, problems) {
|
|
2121
|
-
if (status === "ok") return "Every check passed. Capture, search and the stream are working.";
|
|
2122
|
-
if (status === "info") return `Working normally. ${plural(problems.length, "note")} below about features that are off or checks that did not run.`;
|
|
2123
|
-
const worst = problems.find((p) => p.level === status) ?? problems[0];
|
|
2124
|
-
const rest = problems.length - 1;
|
|
2125
|
-
return `${status === "error" ? "Not working correctly" : "Working, but needs attention"}: ${worst.message}${rest > 0 ? ` (${plural(rest, "more item")} below.)` : ""}`;
|
|
2126
|
-
}
|
|
2127
|
-
function indexBreakdown(idx) {
|
|
2128
|
-
if (idx.memoriesIndexed === void 0 && idx.lessonsIndexed === void 0) return null;
|
|
2129
|
-
const lessons = idx.lessonsIndexed ?? 0;
|
|
2130
|
-
return {
|
|
2131
|
-
observations: Math.max(0, idx.observationsIndexed - lessons),
|
|
2132
|
-
memories: idx.memoriesIndexed ?? 0,
|
|
2133
|
-
lessons
|
|
2134
|
-
};
|
|
2135
|
-
}
|
|
2136
|
-
function plural(count, word) {
|
|
2137
|
-
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
2138
|
-
}
|
|
2139
|
-
function documentBreakdown(idx) {
|
|
2140
|
-
const b = idx.breakdown;
|
|
2141
|
-
if (!b) return String(idx.bm25Documents);
|
|
2142
|
-
const parts = [
|
|
2143
|
-
plural(b.observations, "observation"),
|
|
2144
|
-
`${b.memories} ${b.memories === 1 ? "memory" : "memories"}`,
|
|
2145
|
-
plural(b.lessons, "lesson")
|
|
2146
|
-
];
|
|
2147
|
-
return `${idx.bm25Documents} (${parts.join(", ")})`;
|
|
2148
|
-
}
|
|
2149
|
-
function flagState(flag) {
|
|
2150
|
-
if (!flag.enabled) return "off";
|
|
2151
|
-
if (flag.inactiveReason) return `on, inactive: ${flag.inactiveReason}`;
|
|
2152
|
-
return "on";
|
|
2153
|
-
}
|
|
2154
|
-
function flagHowTo(flag) {
|
|
2155
|
-
if (!flag.enabled) return flag.enableHow;
|
|
2156
|
-
if (flag.inactiveReason) return LLM_KEY_FIX;
|
|
2157
|
-
return `Set ${flag.key}=false and restart to turn it off.`;
|
|
2158
|
-
}
|
|
2159
|
-
function processHealth(report) {
|
|
2160
|
-
const status = report.health?.status;
|
|
2161
|
-
if (!status) return "no data yet";
|
|
2162
|
-
if (status === "healthy" && report.status !== "ok") return "healthy (memory, CPU and engine checks only; the badge at the top covers every problem on this page)";
|
|
2163
|
-
return status;
|
|
2164
|
-
}
|
|
2165
|
-
function escapeHtml(value) {
|
|
2166
|
-
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2167
|
-
}
|
|
2168
|
-
function formatDuration(seconds) {
|
|
2169
|
-
if (seconds === null) return "unknown";
|
|
2170
|
-
if (seconds < 60) return `${seconds}s`;
|
|
2171
|
-
const minutes = Math.floor(seconds / 60);
|
|
2172
|
-
if (minutes < 60) return `${minutes}m`;
|
|
2173
|
-
const hours = Math.floor(minutes / 60);
|
|
2174
|
-
if (hours < 48) return `${hours}h ${minutes % 60}m`;
|
|
2175
|
-
return `${Math.floor(hours / 24)}d`;
|
|
2176
|
-
}
|
|
2177
|
-
function row(label, value) {
|
|
2178
|
-
return `<tr><th>${escapeHtml(label)}</th><td>${value}</td></tr>`;
|
|
2179
|
-
}
|
|
2180
|
-
function legSummary(report, leg) {
|
|
2181
|
-
if (leg.lastError) return `failing: ${leg.lastError}`;
|
|
2182
|
-
const saved = leg.lastSavedAt ? `saved ${formatDuration(secondsBetween(new Date(report.checkedAt), leg.lastSavedAt))} ago` : "not saved since start";
|
|
2183
|
-
return leg.dirtySince ? `${saved}, unsaved changes pending` : saved;
|
|
2184
|
-
}
|
|
2185
|
-
const PENDING_LOG_EXPLAINER = "Every new or removed vector is also written right away to this small log. After a crash or force-kill it is replayed at start, so vectors are not lost and nothing is embedded twice. Each index save empties it.";
|
|
2186
|
-
function indexPersistenceRows(report) {
|
|
2187
|
-
const persistence = report.indexPersistence;
|
|
2188
|
-
if (!persistence) return "";
|
|
2189
|
-
let rows = row("BM25 index", "rebuilt from stored content at boot");
|
|
2190
|
-
if (persistence.vector) {
|
|
2191
|
-
rows += row("Vector save", escapeHtml(legSummary(report, persistence.vector)));
|
|
2192
|
-
rows += row("Vector storage", escapeHtml(`${plural(persistence.buckets, "bucket")}, ${plural(persistence.pendingChanges, "unsaved change")}`));
|
|
2193
|
-
if (persistence.pendingLog !== void 0) rows += row("Pending vector log", escapeHtml(plural(persistence.pendingLog, "vector change")) + `<p class="note">${escapeHtml(PENDING_LOG_EXPLAINER)}</p>`);
|
|
2194
|
-
}
|
|
2195
|
-
return rows;
|
|
2196
|
-
}
|
|
2197
|
-
function describeCaptureSpool(capture) {
|
|
2198
|
-
const records = capture.spool.reduce((n, s) => n + s.records, 0);
|
|
2199
|
-
const bytes = capture.spool.reduce((n, s) => n + s.bytes, 0);
|
|
2200
|
-
if (!capture.spool.some((s) => s.enabled)) return "off (AGENTMEMORY_CAPTURE_SPOOL=false)";
|
|
2201
|
-
const retained = capture.spool.reduce((n, s) => n + (s.retained ?? 0), 0);
|
|
2202
|
-
const held = retained > 0 ? `, ${retained} sent and kept until saved` : "";
|
|
2203
|
-
return records === 0 ? `empty${held}` : `${plural(records, "observation")} waiting, ${Math.ceil(bytes / 1024)} KiB${held}`;
|
|
2204
|
-
}
|
|
2205
|
-
function captureRows(report) {
|
|
2206
|
-
const capture = report.capture;
|
|
2207
|
-
if (!capture) return row("Capture", "not reported");
|
|
2208
|
-
const inbox = capture.inbox;
|
|
2209
|
-
const since = capture.sinceStart;
|
|
2210
|
-
let rows = row("Inbox", escapeHtml(inbox ? `${inbox.pending} pending · ${inbox.retrying} retrying · ${inbox.dead} dead letters` : "not checked") + "<p class=\"note\">Every accepted observation is written here first and removed once it is stored, so a failure or restart retries it instead of losing it.</p>");
|
|
2211
|
-
rows += row("Since start", escapeHtml(`${since.accepted} accepted · ${since.completed} stored · ${since.duplicates} duplicates skipped · ${since.recovered} recovered by retry · ${since.deadLettered} dead`));
|
|
2212
|
-
rows += row("Local spool", escapeHtml(describeCaptureSpool(capture)) + `<p class="note">Hooks write here when the server is unreachable, and keep each accepted observation here until the server has saved it to disk. ${escapeHtml(capture.spool.map((s) => s.path).join(", "))}</p>`);
|
|
2213
|
-
const drained = capture.spool.map((s) => s.stats.lastDrainAt).filter(Boolean).sort().pop();
|
|
2214
|
-
if (drained) rows += row("Last spool drain", escapeHtml(`${formatDuration(secondsBetween(new Date(report.checkedAt), drained))} ago`));
|
|
2215
|
-
rows += row("Duplicate protection", escapeHtml(`event ids kept ${capture.policy.dedupRetentionHours} h, retries up to ${plural(capture.policy.maxAttempts, "attempt")}`));
|
|
2216
|
-
return rows;
|
|
2217
|
-
}
|
|
2218
|
-
function mb(bytes) {
|
|
2219
|
-
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
2220
|
-
}
|
|
2221
|
-
function processRows(report) {
|
|
2222
|
-
const h = report.health;
|
|
2223
|
-
let rows = row("Process health", escapeHtml(processHealth(report)));
|
|
2224
|
-
if (h?.memory) {
|
|
2225
|
-
const limit = h.memory.heapLimit || h.memory.heapTotal;
|
|
2226
|
-
const pct = limit > 0 ? Math.round(h.memory.heapUsed / limit * 100) : 0;
|
|
2227
|
-
rows += row("Heap", escapeHtml(`${mb(h.memory.heapUsed)} of ${mb(limit)} (${pct}%)`) + "<p class=\"note\">JavaScript memory in use against the most it may grow to. Only a concern above 80% while RSS is also over 512 MB.</p>");
|
|
2228
|
-
rows += row("RSS", escapeHtml(mb(h.memory.rss)) + "<p class=\"note\">Everything the process holds from the operating system, including code and buffers.</p>");
|
|
2229
|
-
}
|
|
2230
|
-
if (typeof h?.eventLoopLagMs === "number") rows += row("Event loop delay", escapeHtml(`${h.eventLoopLagMs.toFixed(1)} ms`) + "<p class=\"note\">How far behind the worker is. Under 100 ms is fine; above that requests answer slowly.</p>");
|
|
2231
|
-
return rows;
|
|
2232
|
-
}
|
|
2233
|
-
function renderStatusHtml(report, styleNonce, options = {}) {
|
|
2234
|
-
const viewerUrl = options.viewerUrl ?? "/agentmemory/viewer#health";
|
|
2235
|
-
const problems = report.problems.length ? report.problems.map((p) => `<li class="p ${p.level}"><span class="lvl">${p.level}</span><div><p>${escapeHtml(p.message)}</p>` + (p.fix ? `<p class="fix">${escapeHtml(p.fix)}</p>` : "") + `</div></li>`).join("") : `<li class="p ok"><span class="lvl">ok</span><div><p>No problems found.</p></div></li>`;
|
|
2236
|
-
const functions = report.functions.length ? report.functions.slice().sort((a, b) => b.totalCalls - a.totalCalls).map((f) => `<tr${!f.offWithoutLlm && f.failureRate >= FAILURE_RATE_THRESHOLD && f.totalCalls >= FAILURE_MIN_CALLS ? " class=\"bad\"" : ""}><td>${escapeHtml(f.functionId)}</td><td>${f.totalCalls}</td><td>${f.offWithoutLlm ? "-" : f.failureCount}</td><td>${f.offWithoutLlm ? "off, no LLM provider" : `${Math.round(f.failureRate * 100)}%`}</td><td>${Math.round(f.avgLatencyMs)} ms</td></tr>`).join("") : `<tr><td colspan="5">No function calls recorded yet.</td></tr>`;
|
|
2237
|
-
const flags = report.flags.map((f) => `<tr><td>${escapeHtml(f.label)}</td><td><code>${escapeHtml(f.key)}</code></td><td${f.inactiveReason ? " class=\"warn\"" : ""}>${escapeHtml(flagState(f))}</td><td>${escapeHtml(flagHowTo(f))}</td></tr>`).join("");
|
|
2238
|
-
const idx = report.index;
|
|
2239
|
-
const graph = report.graph;
|
|
2240
|
-
const ports = report.service.ports;
|
|
2241
|
-
return `<!DOCTYPE html>
|
|
2242
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
2243
|
-
<title>agentmemory status</title>
|
|
2244
|
-
<style nonce="${escapeHtml(styleNonce)}">
|
|
2245
|
-
:root{--bg:#F9F9F7;--bg-alt:#F0F0EC;--ink:#111111;--ink-muted:#666666;--line:#D4D4CF;--rule:#111111;--ok:#2D6A4F;--info:#1D4E89;--warn:#B8860B;--error:#CC0000;--font-display:'Playfair Display',Georgia,'Times New Roman',serif;--font-body:'Lora',Georgia,serif;--font-ui:'Inter',-apple-system,system-ui,sans-serif;--font-mono:'JetBrains Mono','SF Mono','Fira Code',ui-monospace,monospace}
|
|
2246
|
-
@media (prefers-color-scheme:dark){:root{--bg:#121316;--bg-alt:#1a1c20;--ink:#eef0f3;--ink-muted:#94979d;--line:#26282c;--rule:#c9cbd1;--ok:#4ade80;--info:#93c5fd;--warn:#fbbf24;--error:#f2555a}}
|
|
2247
|
-
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 var(--font-body)}
|
|
2248
|
-
main{max-width:960px;margin:0 auto;padding:32px 16px 64px}
|
|
2249
|
-
header{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;border-bottom:3px double var(--rule);padding-bottom:12px;margin-bottom:24px}
|
|
2250
|
-
h1{font:700 26px/1.2 var(--font-display);margin:0}h2{font:600 11px/1.4 var(--font-ui);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-muted);margin:32px 0 8px;padding-bottom:4px;border-bottom:1px solid var(--rule)}
|
|
2251
|
-
.badge{font:700 11px/1.4 var(--font-ui);text-transform:uppercase;letter-spacing:.08em;padding:2px 8px;border:1.5px solid currentColor}
|
|
2252
|
-
.ok{color:var(--ok)}.info{color:var(--info)}.warn{color:var(--warn)}.error{color:var(--error)}
|
|
2253
|
-
.meta{color:var(--ink-muted);font:12px/1.4 var(--font-ui);margin-left:auto}
|
|
2254
|
-
ul.problems{list-style:none;padding:0;margin:0}
|
|
2255
|
-
li.p{display:grid;grid-template-columns:64px 1fr;gap:12px;padding:12px 0;border-bottom:1px solid var(--line)}
|
|
2256
|
-
li.p p{margin:0;color:var(--ink)}li.p .fix{color:var(--ink-muted);margin-top:4px;font-size:13px}
|
|
2257
|
-
.lvl{font:700 11px/1.4 var(--font-ui);text-transform:uppercase;letter-spacing:.08em;padding-top:2px}
|
|
2258
|
-
table{width:100%;border-collapse:collapse;font:13px/1.5 var(--font-ui)}th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere}
|
|
2259
|
-
th{color:var(--ink-muted);font-weight:500;width:220px}
|
|
2260
|
-
table.list th{width:auto;font-size:11px;text-transform:uppercase;letter-spacing:.06em;overflow-wrap:normal}table.list td{overflow-wrap:break-word}table.list td code{overflow-wrap:break-word;word-break:normal}tr.bad td{color:var(--error)}
|
|
2261
|
-
tr:nth-child(even) td{background:var(--bg-alt)}
|
|
2262
|
-
code{font:12px var(--font-mono);overflow-wrap:anywhere}
|
|
2263
|
-
.lead{font:600 16px/1.45 var(--font-body);margin:0 0 8px}.live{font:13px/1.5 var(--font-ui);color:var(--ink-muted);margin:0 0 8px}
|
|
2264
|
-
.note{margin:2px 0 0;color:var(--ink-muted);font-size:12px}
|
|
2265
|
-
a{color:inherit}
|
|
2266
|
-
@media (max-width:640px){th{width:110px}table.list{table-layout:fixed}table.list th,table.list td{padding:6px 4px;font-size:12px}}
|
|
2267
|
-
</style></head><body><main>
|
|
2268
|
-
<header><h1>agentmemory</h1><span class="badge ${report.status}">${report.status}</span>
|
|
2269
|
-
<span class="meta">v${escapeHtml(report.service.version)} · engine ${escapeHtml(report.service.engineVersion)} · checked ${escapeHtml(report.checkedAt)} · <a href="?format=json">json</a></span></header>
|
|
2270
|
-
<p class="lead ${report.status}">${escapeHtml(report.headline)}</p>
|
|
2271
|
-
<p class="live">A snapshot from when this page loaded. <a href="${escapeHtml(viewerUrl)}">Open the live version in the viewer →</a> From a terminal: <code>curl -s ${escapeHtml(CURL_AUTH_HEADER)} ${ports.rest ? `http://localhost:${escapeHtml(ports.rest)}` : ""}/agentmemory/status</code> returns this report as JSON.</p>
|
|
2272
|
-
<h2>Problems</h2><ul class="problems">${problems}</ul>
|
|
2273
|
-
<h2>Engine and connection</h2><table>
|
|
2274
|
-
${row("Engine connection", escapeHtml(report.health?.connectionState ?? "unknown"))}
|
|
2275
|
-
${row("Uptime", escapeHtml(formatDuration(report.service.uptimeSeconds)))}
|
|
2276
|
-
${row("State backend", escapeHtml(report.service.stateBackend))}
|
|
2277
|
-
${row("REST port", escapeHtml(ports.rest ?? "unknown"))}
|
|
2278
|
-
${row("Streams port", escapeHtml(ports.streams ?? "unknown"))}
|
|
2279
|
-
${row("Viewer port", escapeHtml(ports.viewer ?? "not running"))}
|
|
2280
|
-
</table>
|
|
2281
|
-
<h2>LLM and embeddings</h2><table>
|
|
2282
|
-
${row("LLM", escapeHtml(report.provider.llm === "noop" ? "none configured" : report.provider.llm))}
|
|
2283
|
-
${report.provider.offWithoutLlm.length ? row("Off without an LLM", escapeHtml(report.provider.offWithoutLlm.join(", ")) + `<p class="note">${escapeHtml(LLM_KEY_FIX)}</p>`) : ""}
|
|
2284
|
-
${row("Embeddings", escapeHtml(report.provider.embeddings === "none" ? "none (keyword search only)" : report.provider.embeddings))}
|
|
2285
|
-
${row("Circuit breaker", escapeHtml(report.provider.circuitBreaker ? `${report.provider.circuitBreaker.state ?? "unknown"} (${report.provider.circuitBreaker.failures ?? 0} failures)` : "not in use"))}
|
|
2286
|
-
</table>
|
|
2287
|
-
<h2>Search index</h2><table>
|
|
2288
|
-
${row("Keyword index (BM25)", escapeHtml(documentBreakdown(idx)) + `<p class="note">${escapeHtml(BM25_EXPLAINER)}</p>`)}
|
|
2289
|
-
${row("Vector documents", escapeHtml(idx.vectorDocuments ?? "vector search off"))}
|
|
2290
|
-
${row("Observations searchable", escapeHtml(idx.breakdown ? idx.breakdown.observations : idx.observationsIndexed))}
|
|
2291
|
-
${row("Missing from index", escapeHtml(idx.missingObservations ?? "not checked"))}
|
|
2292
|
-
${row("Sessions", escapeHtml(idx.sessions ?? "unknown"))}
|
|
2293
|
-
${row("Keyword index rebuild", idx.bm25Incomplete ? "<span class=\"warn\">incomplete</span>" : "complete")}
|
|
2294
|
-
${row("Pending vector backfill", escapeHtml(idx.pendingVectorBackfill))}
|
|
2295
|
-
${idx.vectorBackfillState ? row("Vector backfill", escapeHtml(describeBackfillState(idx.vectorBackfillState))) : ""}
|
|
2296
|
-
${indexPersistenceRows(report)}
|
|
2297
|
-
</table>
|
|
2298
|
-
<h2>Capture</h2><table>
|
|
2299
|
-
${captureRows(report)}
|
|
2300
|
-
${report.observeDedup ? row("Repeats skipped", escapeHtml(`${report.observeDedup.skippedSinceStart} since start`) + `<p class="note">${escapeHtml(`A tool call with the same input and the same output as one stored in the last ${formatDuration(report.observeDedup.windowSeconds)} is skipped when the hook sends no event id.`)}</p>`) : ""}
|
|
2301
|
-
</table>
|
|
2302
|
-
<h2>Knowledge graph</h2><table>
|
|
2303
|
-
${graph ? row("Extraction", graph.extractionEnabled ? "on" : "off") + row("Nodes / edges", escapeHtml(`${graph.totalNodes ?? 0} / ${graph.totalEdges ?? 0}`)) + row("Snapshot age", escapeHtml(graph.fromSnapshot ? formatDuration(graph.ageSeconds) : "no snapshot")) : row("Graph", "unavailable")}
|
|
2304
|
-
${report.graphCompaction ? row("Provenance compaction", escapeHtml(describeGraphCompactBoot(report.graphCompaction))) : ""}
|
|
2305
|
-
</table>
|
|
2306
|
-
<h2>Features</h2><table class="list"><tr><th>Feature</th><th>Setting</th><th>State</th><th>How to change</th></tr>${flags}</table>
|
|
2307
|
-
<h2>Process</h2><table>
|
|
2308
|
-
${processRows(report)}
|
|
2309
|
-
</table>
|
|
2310
|
-
<h2>Functions</h2><table class="list"><tr><th>Function</th><th>Calls</th><th>Failed</th><th>Failure rate</th><th>Avg latency</th></tr>${functions}</table>
|
|
2311
|
-
<h2>More</h2><p>Environment checks (keys, engine binary, stale pid files) run on your machine with <code>npx @agentmemory/agentmemory doctor</code>. JSON: <code>GET /agentmemory/status</code> with <code>Accept: application/json</code>, or add <code>?format=json</code>.</p>
|
|
2312
|
-
</main></body></html>`;
|
|
2313
|
-
}
|
|
2314
|
-
function graphCompactCommand(restPort) {
|
|
2315
|
-
return `curl -X POST http://localhost:${restPort ?? 3111}/agentmemory/graph/compact ${CURL_AUTH_HEADER} -H "Content-Type: application/json" -d '{}'`;
|
|
2316
|
-
}
|
|
2317
|
-
const UNINDEXED_SCAN_REUSE_MS = 3e4;
|
|
2318
|
-
function singleFlight(run, reuseMs, now = Date.now) {
|
|
2319
|
-
let current = null;
|
|
2320
|
-
return () => {
|
|
2321
|
-
if (current && (current.settledAt === null || now() - current.settledAt < reuseMs)) return current.promise;
|
|
2322
|
-
const entry = {
|
|
2323
|
-
promise: run(),
|
|
2324
|
-
settledAt: null
|
|
2325
|
-
};
|
|
2326
|
-
entry.promise.then(() => {
|
|
2327
|
-
entry.settledAt = now();
|
|
2328
|
-
}, () => {
|
|
2329
|
-
if (current === entry) current = null;
|
|
2330
|
-
});
|
|
2331
|
-
current = entry;
|
|
2332
|
-
return entry.promise;
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
function prefersHtml(accept, format) {
|
|
2336
|
-
if (format === "json") return false;
|
|
2337
|
-
if (format === "html") return true;
|
|
2338
|
-
if (!accept) return false;
|
|
2339
|
-
return /\btext\/html\b/.test(accept);
|
|
2340
|
-
}
|
|
2341
|
-
//#endregion
|
|
2342
|
-
export { SHUTDOWN_HARD_EXIT_MS as A, spoolSummary as C, withWriteScrubbing as D, stripPrivateData as E, runtimeMetadataPath as M, resolveDataDir as O, spoolPolicy as S, scrubRecord as T, drainSpool as _, prefersHtml as a, retainSent as b, GRAPH_COMPACT_BOOT_DELAY_MS as c, setGraphCompactBootDisabled as d, COMPACT_SCOPES as f, registerGraphFunction as g, persistGraphDelta as h, markLlmFunctions as i, settleWithin as j, SHUTDOWN_FLUSH_TIMEOUT_MS as k, getGraphCompactBootStatus as l, boundSources as m, UNINDEXED_SCAN_REUSE_MS as n, renderStatusHtml as o, boundRecordSources as p, evaluateStatus as r, singleFlight as s, CURL_AUTH_HEADER as t, runGraphCompactOnBoot as u, parseSentMark as v, registerPrivacyFunction as w, spoolDir as x, reconcileSent as y };
|
|
2343
|
-
|
|
2344
|
-
//# sourceMappingURL=status-IRz7ta8-.mjs.map
|