@swarmclawai/swarmclaw 0.8.4 → 0.8.7
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/README.md +9 -9
- package/bin/swarmclaw.js +5 -1
- package/bin/worker-cmd.js +73 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +17 -7
- package/src/app/api/agents/route.ts +21 -8
- package/src/app/api/approvals/route.test.ts +6 -6
- package/src/app/api/approvals/route.ts +2 -1
- package/src/app/api/auth/route.ts +2 -3
- package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
- package/src/app/api/chatrooms/[id]/route.ts +7 -6
- package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/clear/route.ts +9 -9
- package/src/app/api/chats/[id]/devserver/route.ts +2 -1
- package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
- package/src/app/api/chats/[id]/fork/route.ts +3 -5
- package/src/app/api/chats/[id]/restore/route.ts +6 -7
- package/src/app/api/chats/[id]/retry/route.ts +3 -4
- package/src/app/api/chats/[id]/route.ts +61 -62
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/connectors/[id]/route.ts +7 -8
- package/src/app/api/connectors/route.ts +5 -4
- package/src/app/api/eval/run/route.ts +2 -1
- package/src/app/api/eval/suite/route.ts +2 -1
- package/src/app/api/external-agents/route.test.ts +1 -1
- package/src/app/api/external-agents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +1 -1
- package/src/app/api/gateways/[id]/route.ts +7 -5
- package/src/app/api/gateways/route.ts +1 -1
- package/src/app/api/knowledge/upload/route.ts +1 -1
- package/src/app/api/logs/route.ts +5 -7
- package/src/app/api/memory-images/[filename]/route.ts +2 -3
- package/src/app/api/openclaw/agent-files/route.ts +4 -3
- package/src/app/api/openclaw/approvals/route.ts +3 -4
- package/src/app/api/openclaw/config-sync/route.ts +3 -2
- package/src/app/api/openclaw/cron/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
- package/src/app/api/openclaw/exec-config/route.ts +3 -2
- package/src/app/api/openclaw/gateway/route.ts +5 -4
- package/src/app/api/openclaw/history/route.ts +3 -2
- package/src/app/api/openclaw/media/route.ts +2 -1
- package/src/app/api/openclaw/permissions/route.ts +3 -2
- package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
- package/src/app/api/openclaw/skills/install/route.ts +2 -1
- package/src/app/api/openclaw/skills/remove/route.ts +2 -1
- package/src/app/api/openclaw/skills/route.ts +3 -2
- package/src/app/api/orchestrator/run/route.ts +5 -14
- package/src/app/api/perf/route.ts +43 -0
- package/src/app/api/plugins/dependencies/route.ts +2 -1
- package/src/app/api/plugins/install/route.ts +2 -1
- package/src/app/api/plugins/marketplace/route.ts +3 -2
- package/src/app/api/plugins/settings/route.ts +2 -1
- package/src/app/api/preview-server/route.ts +11 -10
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/schedules/[id]/route.test.ts +128 -0
- package/src/app/api/schedules/[id]/route.ts +43 -43
- package/src/app/api/schedules/[id]/run/route.ts +11 -62
- package/src/app/api/schedules/route.ts +21 -87
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/doctor/route.ts +9 -8
- package/src/app/api/tasks/[id]/approve/route.ts +33 -30
- package/src/app/api/tasks/[id]/route.ts +12 -35
- package/src/app/api/tasks/import/github/route.ts +2 -1
- package/src/app/api/tasks/route.ts +79 -91
- package/src/app/api/wallets/[id]/approve/route.ts +2 -1
- package/src/app/api/wallets/[id]/route.ts +13 -19
- package/src/app/api/wallets/[id]/send/route.ts +2 -1
- package/src/app/api/wallets/route.ts +2 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.test.ts +3 -1
- package/src/app/page.tsx +23 -331
- package/src/cli/index.js +19 -0
- package/src/cli/index.ts +38 -7
- package/src/cli/spec.js +9 -0
- package/src/components/activity/activity-feed.tsx +7 -4
- package/src/components/agents/agent-card.tsx +32 -6
- package/src/components/agents/agent-chat-list.tsx +55 -22
- package/src/components/agents/agent-files-editor.tsx +3 -2
- package/src/components/agents/agent-sheet.tsx +123 -22
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/openclaw-skills-panel.tsx +2 -1
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +8 -2
- package/src/components/auth/setup-wizard.tsx +10 -9
- package/src/components/auth/user-picker.tsx +3 -2
- package/src/components/chat/chat-area.tsx +20 -1
- package/src/components/chat/chat-card.tsx +18 -3
- package/src/components/chat/chat-header.tsx +24 -4
- package/src/components/chat/chat-list.tsx +2 -11
- package/src/components/chat/heartbeat-history-panel.tsx +2 -1
- package/src/components/chat/message-bubble.tsx +45 -6
- package/src/components/chat/message-list.tsx +280 -145
- package/src/components/chat/streaming-bubble.tsx +217 -60
- package/src/components/chat/swarm-panel.test.ts +274 -0
- package/src/components/chat/swarm-panel.tsx +410 -0
- package/src/components/chat/swarm-status-card.tsx +346 -0
- package/src/components/chat/tool-call-bubble.tsx +48 -23
- package/src/components/chatrooms/chatroom-list.tsx +8 -5
- package/src/components/chatrooms/chatroom-message.tsx +10 -7
- package/src/components/chatrooms/chatroom-view.tsx +12 -9
- package/src/components/connectors/connector-health.tsx +6 -4
- package/src/components/connectors/connector-list.tsx +16 -11
- package/src/components/connectors/connector-sheet.tsx +12 -6
- package/src/components/home/home-view.tsx +38 -24
- package/src/components/input/chat-input.tsx +10 -1
- package/src/components/layout/app-layout.tsx +2 -38
- package/src/components/layout/sheet-layer.tsx +50 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
- package/src/components/plugins/plugin-list.tsx +8 -4
- package/src/components/plugins/plugin-sheet.tsx +2 -1
- package/src/components/providers/provider-list.tsx +3 -2
- package/src/components/providers/provider-sheet.tsx +2 -1
- package/src/components/runs/run-list.tsx +11 -7
- package/src/components/schedules/schedule-card.tsx +5 -3
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/attachment-chip.tsx +19 -3
- package/src/components/shared/notification-center.tsx +6 -3
- package/src/components/shared/settings/plugin-manager.tsx +3 -2
- package/src/components/shared/settings/section-embedding.tsx +2 -1
- package/src/components/shared/settings/section-orchestrator.tsx +2 -1
- package/src/components/shared/settings/section-user-preferences.tsx +107 -0
- package/src/components/shared/settings/settings-page.tsx +13 -9
- package/src/components/skills/clawhub-browser.tsx +15 -4
- package/src/components/skills/skill-list.tsx +15 -4
- package/src/components/tasks/approvals-panel.tsx +2 -1
- package/src/components/tasks/task-board.tsx +35 -37
- package/src/components/tasks/task-sheet.tsx +4 -3
- package/src/components/ui/full-screen-loader.tsx +164 -0
- package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
- package/src/components/wallets/wallet-panel.tsx +6 -5
- package/src/components/wallets/wallet-section.tsx +3 -2
- package/src/components/webhooks/webhook-list.tsx +4 -5
- package/src/components/webhooks/webhook-sheet.tsx +6 -6
- package/src/hooks/use-app-bootstrap.ts +202 -0
- package/src/hooks/use-mounted-ref.ts +14 -0
- package/src/hooks/use-now.ts +31 -0
- package/src/hooks/use-openclaw-gateway.ts +2 -1
- package/src/instrumentation.ts +20 -8
- package/src/lib/agent-default-tools.test.ts +52 -0
- package/src/lib/agent-default-tools.ts +40 -0
- package/src/lib/api-client.test.ts +21 -0
- package/src/lib/api-client.ts +6 -11
- package/src/lib/canvas-content.test.ts +360 -0
- package/src/lib/chat-streaming-state.test.ts +49 -2
- package/src/lib/chat-streaming-state.ts +26 -10
- package/src/lib/fetch-timeout.test.ts +54 -0
- package/src/lib/fetch-timeout.ts +60 -3
- package/src/lib/live-tool-events.test.ts +77 -0
- package/src/lib/live-tool-events.ts +73 -0
- package/src/lib/local-observability.test.ts +2 -2
- package/src/lib/openclaw-endpoint.test.ts +1 -1
- package/src/lib/providers/anthropic.ts +12 -16
- package/src/lib/providers/index.ts +4 -2
- package/src/lib/providers/ollama.ts +9 -6
- package/src/lib/providers/openai.ts +11 -14
- package/src/lib/runtime-env.test.ts +8 -8
- package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
- package/src/lib/schedule-dedupe.test.ts +1 -1
- package/src/lib/schedule-dedupe.ts +3 -2
- package/src/lib/server/agent-thread-session.test.ts +6 -6
- package/src/lib/server/agent-thread-session.ts +6 -9
- package/src/lib/server/alert-dispatch.ts +2 -1
- package/src/lib/server/api-routes.test.ts +6 -6
- package/src/lib/server/approval-connector-notify.test.ts +4 -4
- package/src/lib/server/approvals-auto-approve.test.ts +29 -29
- package/src/lib/server/approvals.test.ts +317 -0
- package/src/lib/server/approvals.ts +5 -4
- package/src/lib/server/autonomy-runtime.test.ts +11 -11
- package/src/lib/server/browser-state.ts +2 -2
- package/src/lib/server/capability-router.test.ts +1 -1
- package/src/lib/server/capability-router.ts +3 -2
- package/src/lib/server/chat-execution-advanced.test.ts +15 -2
- package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
- package/src/lib/server/chat-execution-disabled.test.ts +3 -3
- package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
- package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
- package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
- package/src/lib/server/chat-execution-tool-events.ts +116 -0
- package/src/lib/server/chat-execution-utils.test.ts +479 -0
- package/src/lib/server/chat-execution-utils.ts +533 -0
- package/src/lib/server/chat-execution.ts +153 -748
- package/src/lib/server/chat-streaming-utils.ts +174 -0
- package/src/lib/server/chat-turn-tool-routing.ts +310 -0
- package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
- package/src/lib/server/clawhub-client.ts +2 -1
- package/src/lib/server/collection-helpers.test.ts +92 -0
- package/src/lib/server/collection-helpers.ts +25 -3
- package/src/lib/server/connectors/access.ts +146 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
- package/src/lib/server/connectors/bluebubbles.ts +4 -4
- package/src/lib/server/connectors/commands.ts +367 -0
- package/src/lib/server/connectors/connector-routing.test.ts +4 -4
- package/src/lib/server/connectors/delivery.ts +142 -0
- package/src/lib/server/connectors/discord.ts +37 -40
- package/src/lib/server/connectors/email.ts +11 -10
- package/src/lib/server/connectors/googlechat.ts +4 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
- package/src/lib/server/connectors/ingress-delivery.ts +23 -0
- package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
- package/src/lib/server/connectors/manager.test.ts +352 -77
- package/src/lib/server/connectors/manager.ts +134 -673
- package/src/lib/server/connectors/matrix.ts +4 -4
- package/src/lib/server/connectors/message-sentinel.ts +7 -0
- package/src/lib/server/connectors/openclaw.test.ts +1 -1
- package/src/lib/server/connectors/openclaw.ts +8 -10
- package/src/lib/server/connectors/outbox.test.ts +192 -0
- package/src/lib/server/connectors/outbox.ts +369 -0
- package/src/lib/server/connectors/pairing.test.ts +18 -1
- package/src/lib/server/connectors/pairing.ts +49 -4
- package/src/lib/server/connectors/policy.ts +9 -3
- package/src/lib/server/connectors/reconnect-state.ts +71 -0
- package/src/lib/server/connectors/response-media.ts +256 -0
- package/src/lib/server/connectors/runtime-state.ts +67 -0
- package/src/lib/server/connectors/session.test.ts +357 -0
- package/src/lib/server/connectors/session.ts +422 -0
- package/src/lib/server/connectors/signal.ts +7 -7
- package/src/lib/server/connectors/slack.ts +43 -43
- package/src/lib/server/connectors/teams.ts +4 -4
- package/src/lib/server/connectors/telegram.ts +37 -43
- package/src/lib/server/connectors/types.ts +31 -1
- package/src/lib/server/connectors/whatsapp.test.ts +108 -0
- package/src/lib/server/connectors/whatsapp.ts +106 -34
- package/src/lib/server/context-manager.test.ts +409 -0
- package/src/lib/server/cost.test.ts +1 -1
- package/src/lib/server/daemon-policy.ts +78 -0
- package/src/lib/server/daemon-state-connectors.test.ts +167 -0
- package/src/lib/server/daemon-state.test.ts +283 -55
- package/src/lib/server/daemon-state.ts +106 -109
- package/src/lib/server/data-dir.test.ts +5 -5
- package/src/lib/server/data-dir.ts +4 -0
- package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
- package/src/lib/server/delegation-jobs.test.ts +87 -0
- package/src/lib/server/delegation-jobs.ts +42 -48
- package/src/lib/server/devserver-launch.ts +1 -1
- package/src/lib/server/document-utils.ts +7 -9
- package/src/lib/server/elevenlabs.ts +2 -1
- package/src/lib/server/embeddings.test.ts +105 -0
- package/src/lib/server/ethereum.ts +3 -2
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -1
- package/src/lib/server/eval/scorer.ts +2 -1
- package/src/lib/server/evm-swap.ts +2 -1
- package/src/lib/server/gateway/protocol.test.ts +1 -1
- package/src/lib/server/guardian.ts +2 -1
- package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
- package/src/lib/server/heartbeat-service.test.ts +406 -0
- package/src/lib/server/heartbeat-service.ts +54 -7
- package/src/lib/server/heartbeat-wake.test.ts +19 -0
- package/src/lib/server/heartbeat-wake.ts +17 -16
- package/src/lib/server/integrity-monitor.test.ts +149 -0
- package/src/lib/server/json-utils.ts +22 -0
- package/src/lib/server/knowledge-db.test.ts +13 -13
- package/src/lib/server/link-understanding.ts +2 -1
- package/src/lib/server/llm-response-cache.test.ts +1 -1
- package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
- package/src/lib/server/main-agent-loop.test.ts +6 -6
- package/src/lib/server/main-agent-loop.ts +21 -7
- package/src/lib/server/mcp-client.test.ts +1 -1
- package/src/lib/server/mcp-conformance.test.ts +1 -1
- package/src/lib/server/mcp-conformance.ts +3 -2
- package/src/lib/server/memory-consolidation.ts +2 -1
- package/src/lib/server/memory-db.test.ts +485 -0
- package/src/lib/server/memory-db.ts +39 -26
- package/src/lib/server/memory-graph.test.ts +2 -2
- package/src/lib/server/memory-policy.test.ts +7 -7
- package/src/lib/server/memory-retrieval.test.ts +1 -1
- package/src/lib/server/openclaw-config-sync.ts +2 -1
- package/src/lib/server/openclaw-deploy.test.ts +1 -1
- package/src/lib/server/openclaw-deploy.ts +8 -12
- package/src/lib/server/openclaw-exec-config.ts +2 -1
- package/src/lib/server/openclaw-gateway.ts +6 -7
- package/src/lib/server/openclaw-skills-normalize.ts +2 -1
- package/src/lib/server/openclaw-sync.ts +7 -5
- package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
- package/src/lib/server/orchestrator-lg.ts +199 -327
- package/src/lib/server/path-utils.ts +31 -0
- package/src/lib/server/perf.ts +161 -0
- package/src/lib/server/plugins-approval-guidance.ts +115 -0
- package/src/lib/server/plugins.test.ts +1 -1
- package/src/lib/server/plugins.ts +22 -132
- package/src/lib/server/process-manager.ts +5 -8
- package/src/lib/server/provider-health.test.ts +137 -0
- package/src/lib/server/provider-health.ts +3 -3
- package/src/lib/server/provider-model-discovery.ts +3 -12
- package/src/lib/server/queue-followups.test.ts +9 -9
- package/src/lib/server/queue-reconcile.test.ts +2 -2
- package/src/lib/server/queue-recovery.test.ts +269 -0
- package/src/lib/server/queue.test.ts +570 -0
- package/src/lib/server/queue.ts +62 -455
- package/src/lib/server/resolve-image.ts +30 -0
- package/src/lib/server/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
- package/src/lib/server/schedule-normalization.test.ts +279 -0
- package/src/lib/server/schedule-service.ts +263 -0
- package/src/lib/server/scheduler.ts +17 -74
- package/src/lib/server/session-mailbox.test.ts +191 -0
- package/src/lib/server/session-run-manager.test.ts +640 -0
- package/src/lib/server/session-run-manager.ts +59 -15
- package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
- package/src/lib/server/session-tools/calendar.ts +2 -1
- package/src/lib/server/session-tools/canvas.ts +2 -1
- package/src/lib/server/session-tools/chatroom.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +26 -28
- package/src/lib/server/session-tools/context-mgmt.ts +3 -2
- package/src/lib/server/session-tools/crawl.ts +4 -3
- package/src/lib/server/session-tools/crud.ts +105 -324
- package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
- package/src/lib/server/session-tools/delegate.ts +6 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
- package/src/lib/server/session-tools/discovery.ts +4 -3
- package/src/lib/server/session-tools/document.ts +2 -1
- package/src/lib/server/session-tools/email.ts +2 -1
- package/src/lib/server/session-tools/extract.ts +2 -1
- package/src/lib/server/session-tools/file.ts +4 -3
- package/src/lib/server/session-tools/http.ts +2 -1
- package/src/lib/server/session-tools/human-loop.ts +2 -1
- package/src/lib/server/session-tools/image-gen.ts +4 -3
- package/src/lib/server/session-tools/index.ts +26 -30
- package/src/lib/server/session-tools/mailbox.ts +2 -1
- package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
- package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
- package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
- package/src/lib/server/session-tools/monitor.ts +2 -1
- package/src/lib/server/session-tools/platform.ts +2 -1
- package/src/lib/server/session-tools/plugin-creator.ts +2 -1
- package/src/lib/server/session-tools/replicate.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
- package/src/lib/server/session-tools/shell.ts +4 -9
- package/src/lib/server/session-tools/subagent.ts +322 -170
- package/src/lib/server/session-tools/table.ts +6 -5
- package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
- package/src/lib/server/session-tools/wallet.ts +7 -6
- package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
- package/src/lib/server/session-tools/web-utils.ts +317 -0
- package/src/lib/server/session-tools/web.ts +62 -328
- package/src/lib/server/skill-prompt-budget.test.ts +1 -1
- package/src/lib/server/skills-normalize.ts +2 -1
- package/src/lib/server/storage-item-access.test.ts +302 -0
- package/src/lib/server/storage.ts +366 -314
- package/src/lib/server/stream-agent-chat.test.ts +82 -3
- package/src/lib/server/stream-agent-chat.ts +146 -510
- package/src/lib/server/stream-continuation.ts +412 -0
- package/src/lib/server/subagent-lineage.test.ts +647 -0
- package/src/lib/server/subagent-lineage.ts +435 -0
- package/src/lib/server/subagent-runtime.test.ts +484 -0
- package/src/lib/server/subagent-runtime.ts +419 -0
- package/src/lib/server/subagent-swarm.test.ts +391 -0
- package/src/lib/server/subagent-swarm.ts +564 -0
- package/src/lib/server/system-events.ts +3 -3
- package/src/lib/server/task-followups.test.ts +491 -0
- package/src/lib/server/task-followups.ts +391 -0
- package/src/lib/server/task-lifecycle.test.ts +205 -0
- package/src/lib/server/task-lifecycle.ts +200 -0
- package/src/lib/server/task-quality-gate.test.ts +1 -1
- package/src/lib/server/task-resume.ts +208 -0
- package/src/lib/server/task-service.test.ts +108 -0
- package/src/lib/server/task-service.ts +264 -0
- package/src/lib/server/task-validation.test.ts +1 -1
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
- package/src/lib/server/tool-capability-policy.test.ts +2 -2
- package/src/lib/server/tool-capability-policy.ts +3 -2
- package/src/lib/server/tool-planning.ts +2 -1
- package/src/lib/server/tool-retry.ts +2 -3
- package/src/lib/server/wake-dispatcher.test.ts +303 -0
- package/src/lib/server/wake-dispatcher.ts +318 -0
- package/src/lib/server/wake-mode.test.ts +161 -0
- package/src/lib/server/wake-mode.ts +174 -0
- package/src/lib/server/wallet-service.ts +8 -9
- package/src/lib/server/watch-jobs.ts +2 -1
- package/src/lib/server/workspace-context.ts +2 -2
- package/src/lib/shared-utils.test.ts +142 -0
- package/src/lib/shared-utils.ts +62 -0
- package/src/lib/tool-event-summary.ts +2 -1
- package/src/lib/view-routes.test.ts +100 -0
- package/src/lib/wallet.test.ts +322 -6
- package/src/proxy.test.ts +4 -4
- package/src/proxy.ts +2 -3
- package/src/stores/set-if-changed.ts +40 -0
- package/src/stores/slices/agent-slice.ts +111 -0
- package/src/stores/slices/auth-slice.ts +25 -0
- package/src/stores/slices/data-slice.ts +301 -0
- package/src/stores/slices/index.ts +7 -0
- package/src/stores/slices/session-slice.ts +112 -0
- package/src/stores/slices/task-slice.ts +63 -0
- package/src/stores/slices/ui-slice.ts +192 -0
- package/src/stores/use-app-store.ts +17 -822
- package/src/stores/use-approval-store.ts +2 -1
- package/src/stores/use-chat-store.ts +8 -1
- package/src/types/index.ts +10 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let memDb: typeof import('./memory-db')
|
|
15
|
+
|
|
16
|
+
before(async () => {
|
|
17
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-memory-db-'))
|
|
18
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
19
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
20
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
21
|
+
memDb = await import('./memory-db')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
after(() => {
|
|
25
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
26
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
27
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
28
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
29
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
30
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('memory-db', () => {
|
|
35
|
+
// --- Basic CRUD ---
|
|
36
|
+
|
|
37
|
+
describe('add and get', () => {
|
|
38
|
+
it('stores a memory and retrieves it by ID', () => {
|
|
39
|
+
const db = memDb.getMemoryDb()
|
|
40
|
+
const entry = db.add({
|
|
41
|
+
agentId: 'agent-1',
|
|
42
|
+
sessionId: 'session-1',
|
|
43
|
+
category: 'note',
|
|
44
|
+
title: 'Test Memory',
|
|
45
|
+
content: 'This is a test memory entry.',
|
|
46
|
+
})
|
|
47
|
+
assert.ok(entry.id)
|
|
48
|
+
assert.equal(entry.title, 'Test Memory')
|
|
49
|
+
assert.equal(entry.content, 'This is a test memory entry.')
|
|
50
|
+
assert.equal(entry.category, 'note')
|
|
51
|
+
assert.equal(entry.agentId, 'agent-1')
|
|
52
|
+
|
|
53
|
+
const retrieved = db.get(entry.id)
|
|
54
|
+
assert.ok(retrieved)
|
|
55
|
+
assert.equal(retrieved!.id, entry.id)
|
|
56
|
+
assert.equal(retrieved!.title, 'Test Memory')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('generates unique IDs for each entry', () => {
|
|
60
|
+
const db = memDb.getMemoryDb()
|
|
61
|
+
const e1 = db.add({ agentId: null, category: 'note', title: 'A', content: 'a-content' })
|
|
62
|
+
const e2 = db.add({ agentId: null, category: 'note', title: 'B', content: 'b-content' })
|
|
63
|
+
assert.notEqual(e1.id, e2.id)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('returns null for non-existent ID', () => {
|
|
67
|
+
const db = memDb.getMemoryDb()
|
|
68
|
+
assert.equal(db.get('nonexistent-id-xyz'), null)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// --- Update ---
|
|
73
|
+
|
|
74
|
+
describe('update', () => {
|
|
75
|
+
it('updates a memory entry', () => {
|
|
76
|
+
const db = memDb.getMemoryDb()
|
|
77
|
+
const entry = db.add({
|
|
78
|
+
agentId: 'agent-up',
|
|
79
|
+
category: 'note',
|
|
80
|
+
title: 'Original Title',
|
|
81
|
+
content: 'Original content.',
|
|
82
|
+
})
|
|
83
|
+
const updated = db.update(entry.id, { title: 'Updated Title', content: 'Updated content.' })
|
|
84
|
+
assert.ok(updated)
|
|
85
|
+
assert.equal(updated!.title, 'Updated Title')
|
|
86
|
+
assert.equal(updated!.content, 'Updated content.')
|
|
87
|
+
assert.equal(updated!.agentId, 'agent-up')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('returns null when updating non-existent entry', () => {
|
|
91
|
+
const db = memDb.getMemoryDb()
|
|
92
|
+
assert.equal(db.update('nonexistent-id', { title: 'Nope' }), null)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// --- Delete ---
|
|
97
|
+
|
|
98
|
+
describe('delete', () => {
|
|
99
|
+
it('removes a memory entry', () => {
|
|
100
|
+
const db = memDb.getMemoryDb()
|
|
101
|
+
const entry = db.add({
|
|
102
|
+
agentId: 'agent-del',
|
|
103
|
+
category: 'note',
|
|
104
|
+
title: 'To Delete',
|
|
105
|
+
content: 'This will be deleted.',
|
|
106
|
+
})
|
|
107
|
+
assert.ok(db.get(entry.id))
|
|
108
|
+
db.delete(entry.id)
|
|
109
|
+
assert.equal(db.get(entry.id), null)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// --- List ---
|
|
114
|
+
|
|
115
|
+
describe('list', () => {
|
|
116
|
+
it('lists memories for an agent', () => {
|
|
117
|
+
const db = memDb.getMemoryDb()
|
|
118
|
+
const agentId = `agent-list-${Date.now()}`
|
|
119
|
+
db.add({ agentId, category: 'note', title: 'List 1', content: 'Content 1' })
|
|
120
|
+
db.add({ agentId, category: 'note', title: 'List 2', content: 'Content 2' })
|
|
121
|
+
db.add({ agentId: 'other-agent', category: 'note', title: 'Other', content: 'Other content' })
|
|
122
|
+
|
|
123
|
+
const agentMemories = db.list(agentId)
|
|
124
|
+
assert.ok(agentMemories.length >= 2, `Expected at least 2 agent memories, got ${agentMemories.length}`)
|
|
125
|
+
const titles = agentMemories.map((m) => m.title)
|
|
126
|
+
assert.ok(titles.includes('List 1'))
|
|
127
|
+
assert.ok(titles.includes('List 2'))
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('respects limit parameter', () => {
|
|
131
|
+
const db = memDb.getMemoryDb()
|
|
132
|
+
const agentId = `agent-limit-${Date.now()}`
|
|
133
|
+
for (let i = 0; i < 10; i++) {
|
|
134
|
+
db.add({ agentId, category: 'note', title: `Mem ${i}`, content: `Content ${i}` })
|
|
135
|
+
}
|
|
136
|
+
const limited = db.list(agentId, 3)
|
|
137
|
+
assert.equal(limited.length, 3)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// --- FTS5 Search ---
|
|
142
|
+
|
|
143
|
+
describe('search (FTS5)', () => {
|
|
144
|
+
it('finds memories by content keyword', () => {
|
|
145
|
+
const db = memDb.getMemoryDb()
|
|
146
|
+
const agentId = `agent-fts-${Date.now()}`
|
|
147
|
+
db.add({
|
|
148
|
+
agentId,
|
|
149
|
+
category: 'note',
|
|
150
|
+
title: 'Kubernetes Deployment',
|
|
151
|
+
content: 'Deployed the application to a Kubernetes cluster using Helm charts.',
|
|
152
|
+
})
|
|
153
|
+
db.add({
|
|
154
|
+
agentId,
|
|
155
|
+
category: 'note',
|
|
156
|
+
title: 'Database Migration',
|
|
157
|
+
content: 'Ran the PostgreSQL migration scripts successfully.',
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const results = db.search('kubernetes deployment helm', agentId)
|
|
161
|
+
assert.ok(results.length >= 1, `Expected FTS results for kubernetes, got ${results.length}`)
|
|
162
|
+
const titles = results.map((r) => r.title)
|
|
163
|
+
assert.ok(titles.includes('Kubernetes Deployment'))
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('returns empty for skip-query patterns', () => {
|
|
167
|
+
const db = memDb.getMemoryDb()
|
|
168
|
+
assert.deepEqual(db.search(''), [])
|
|
169
|
+
assert.deepEqual(db.search('swarm_heartbeat_check'), [])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('returns empty for very long queries', () => {
|
|
173
|
+
const db = memDb.getMemoryDb()
|
|
174
|
+
const longQuery = 'x'.repeat(1300)
|
|
175
|
+
assert.deepEqual(db.search(longQuery), [])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// --- buildFtsQuery ---
|
|
180
|
+
|
|
181
|
+
describe('buildFtsQuery', () => {
|
|
182
|
+
it('removes stop words', () => {
|
|
183
|
+
const query = memDb.buildFtsQuery('what is the purpose of this')
|
|
184
|
+
// 'what', 'is', 'the', 'of', 'this' are stop words; 'purpose' should remain
|
|
185
|
+
assert.ok(query.includes('purpose'))
|
|
186
|
+
assert.ok(!query.includes('"the"'))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('returns empty for all stop words', () => {
|
|
190
|
+
const query = memDb.buildFtsQuery('the is a an')
|
|
191
|
+
assert.equal(query, '')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('limits to MAX_FTS_QUERY_TERMS', () => {
|
|
195
|
+
const query = memDb.buildFtsQuery('alpha bravo charlie delta echo foxtrot golf hotel india juliet')
|
|
196
|
+
// Should have at most 4 terms (slice 0..4)
|
|
197
|
+
const termCount = (query.match(/AND/g) || []).length + 1
|
|
198
|
+
assert.ok(termCount <= 4, `Expected at most 4 terms, got ${termCount}`)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('handles empty input', () => {
|
|
202
|
+
assert.equal(memDb.buildFtsQuery(''), '')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('deduplicates terms', () => {
|
|
206
|
+
const query = memDb.buildFtsQuery('kubernetes kubernetes kubernetes')
|
|
207
|
+
// Should only have one kubernetes
|
|
208
|
+
const occurrences = (query.match(/kubernetes/g) || []).length
|
|
209
|
+
assert.equal(occurrences, 1)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('skips very short terms', () => {
|
|
213
|
+
const query = memDb.buildFtsQuery('go is ok no')
|
|
214
|
+
// All terms are <3 chars or stop words
|
|
215
|
+
assert.equal(query, '')
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// --- Content hash dedup ---
|
|
220
|
+
|
|
221
|
+
describe('content hash dedup', () => {
|
|
222
|
+
it('reinforces instead of duplicating same content for same agent', () => {
|
|
223
|
+
const db = memDb.getMemoryDb()
|
|
224
|
+
const agentId = `agent-dedup-${Date.now()}`
|
|
225
|
+
const first = db.add({
|
|
226
|
+
agentId,
|
|
227
|
+
category: 'fact',
|
|
228
|
+
title: 'Dedup Test',
|
|
229
|
+
content: 'Identical content for dedup testing.',
|
|
230
|
+
})
|
|
231
|
+
const second = db.add({
|
|
232
|
+
agentId,
|
|
233
|
+
category: 'fact',
|
|
234
|
+
title: 'Dedup Test Different Title',
|
|
235
|
+
content: 'Identical content for dedup testing.',
|
|
236
|
+
})
|
|
237
|
+
// Should return the same ID (reinforced, not duplicated)
|
|
238
|
+
assert.equal(second.id, first.id)
|
|
239
|
+
assert.ok((second.reinforcementCount || 0) >= 1, 'Expected reinforcement count to increase')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// --- Memory linking ---
|
|
244
|
+
|
|
245
|
+
describe('link and unlink', () => {
|
|
246
|
+
it('links two memories bidirectionally', () => {
|
|
247
|
+
const db = memDb.getMemoryDb()
|
|
248
|
+
const a = db.add({ agentId: 'agent-link', category: 'note', title: 'Memory A', content: 'Content A' })
|
|
249
|
+
const b = db.add({ agentId: 'agent-link', category: 'note', title: 'Memory B', content: 'Content B' })
|
|
250
|
+
|
|
251
|
+
db.link(a.id, [b.id])
|
|
252
|
+
|
|
253
|
+
const aAfter = db.get(a.id)
|
|
254
|
+
const bAfter = db.get(b.id)
|
|
255
|
+
assert.ok(aAfter!.linkedMemoryIds?.includes(b.id), 'A should link to B')
|
|
256
|
+
assert.ok(bAfter!.linkedMemoryIds?.includes(a.id), 'B should link back to A')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('unlinks memories bidirectionally', () => {
|
|
260
|
+
const db = memDb.getMemoryDb()
|
|
261
|
+
const a = db.add({ agentId: 'agent-unlink', category: 'note', title: 'Unlink A', content: 'Unlink Content A' })
|
|
262
|
+
const b = db.add({ agentId: 'agent-unlink', category: 'note', title: 'Unlink B', content: 'Unlink Content B' })
|
|
263
|
+
|
|
264
|
+
db.link(a.id, [b.id])
|
|
265
|
+
db.unlink(a.id, [b.id])
|
|
266
|
+
|
|
267
|
+
const aAfter = db.get(a.id)
|
|
268
|
+
const bAfter = db.get(b.id)
|
|
269
|
+
const aLinks = aAfter?.linkedMemoryIds || []
|
|
270
|
+
const bLinks = bAfter?.linkedMemoryIds || []
|
|
271
|
+
assert.ok(!aLinks.includes(b.id), 'A should no longer link to B')
|
|
272
|
+
assert.ok(!bLinks.includes(a.id), 'B should no longer link to A')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('link returns null for non-existent source', () => {
|
|
276
|
+
const db = memDb.getMemoryDb()
|
|
277
|
+
assert.equal(db.link('nonexistent', ['also-nonexistent']), null)
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// --- Pinned memories ---
|
|
282
|
+
|
|
283
|
+
describe('pinned memories', () => {
|
|
284
|
+
it('lists pinned memories for an agent', () => {
|
|
285
|
+
const db = memDb.getMemoryDb()
|
|
286
|
+
const agentId = `agent-pinned-${Date.now()}`
|
|
287
|
+
db.add({ agentId, category: 'note', title: 'Regular', content: 'Not pinned' })
|
|
288
|
+
db.add({ agentId, category: 'note', title: 'Pinned One', content: 'This is pinned', pinned: true })
|
|
289
|
+
|
|
290
|
+
const pinned = db.listPinned(agentId)
|
|
291
|
+
assert.ok(pinned.length >= 1)
|
|
292
|
+
assert.ok(pinned.some((m) => m.title === 'Pinned One'))
|
|
293
|
+
assert.ok(pinned.every((m) => m.pinned === true))
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// --- Scope filtering ---
|
|
298
|
+
|
|
299
|
+
describe('filterMemoriesByScope', () => {
|
|
300
|
+
it('returns all entries with mode=all', () => {
|
|
301
|
+
const entries = [
|
|
302
|
+
{ id: '1', agentId: 'a1', category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
303
|
+
{ id: '2', agentId: null, category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
304
|
+
]
|
|
305
|
+
const result = memDb.filterMemoriesByScope(entries, { mode: 'all' })
|
|
306
|
+
assert.equal(result.length, 2)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('filters to global-only with mode=global', () => {
|
|
310
|
+
const entries = [
|
|
311
|
+
{ id: '1', agentId: 'a1', category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
312
|
+
{ id: '2', agentId: null, category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
313
|
+
]
|
|
314
|
+
const result = memDb.filterMemoriesByScope(entries, { mode: 'global' })
|
|
315
|
+
assert.equal(result.length, 1)
|
|
316
|
+
assert.equal(result[0].id, '2')
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('filters by agent with mode=agent', () => {
|
|
320
|
+
const entries = [
|
|
321
|
+
{ id: '1', agentId: 'a1', category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
322
|
+
{ id: '2', agentId: 'a2', category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
323
|
+
]
|
|
324
|
+
const result = memDb.filterMemoriesByScope(entries, { mode: 'agent', agentId: 'a1' })
|
|
325
|
+
assert.equal(result.length, 1)
|
|
326
|
+
assert.equal(result[0].agentId, 'a1')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('includes shared-with entries in agent mode', () => {
|
|
330
|
+
const entries = [
|
|
331
|
+
{ id: '1', agentId: 'a2', sharedWith: ['a1'], category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
332
|
+
]
|
|
333
|
+
const result = memDb.filterMemoriesByScope(entries, { mode: 'agent', agentId: 'a1' })
|
|
334
|
+
assert.equal(result.length, 1)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('returns empty for agent mode without agentId', () => {
|
|
338
|
+
const entries = [
|
|
339
|
+
{ id: '1', agentId: 'a1', category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
340
|
+
]
|
|
341
|
+
const result = memDb.filterMemoriesByScope(entries, { mode: 'agent' })
|
|
342
|
+
assert.equal(result.length, 0)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('filters by session with mode=session', () => {
|
|
346
|
+
const entries = [
|
|
347
|
+
{ id: '1', agentId: 'a1', sessionId: 's1', category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
348
|
+
{ id: '2', agentId: 'a1', sessionId: 's2', category: 'note', title: 'x', content: 'y', createdAt: 0, updatedAt: 0 },
|
|
349
|
+
]
|
|
350
|
+
const result = memDb.filterMemoriesByScope(entries, { mode: 'session', sessionId: 's1' })
|
|
351
|
+
assert.equal(result.length, 1)
|
|
352
|
+
assert.equal(result[0].sessionId, 's1')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// --- normalizeMemoryScopeMode ---
|
|
357
|
+
|
|
358
|
+
describe('normalizeMemoryScopeMode', () => {
|
|
359
|
+
it('normalizes known modes', () => {
|
|
360
|
+
assert.equal(memDb.normalizeMemoryScopeMode('all'), 'all')
|
|
361
|
+
assert.equal(memDb.normalizeMemoryScopeMode('global'), 'global')
|
|
362
|
+
assert.equal(memDb.normalizeMemoryScopeMode('agent'), 'agent')
|
|
363
|
+
assert.equal(memDb.normalizeMemoryScopeMode('session'), 'session')
|
|
364
|
+
assert.equal(memDb.normalizeMemoryScopeMode('project'), 'project')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('maps shared to global', () => {
|
|
368
|
+
assert.equal(memDb.normalizeMemoryScopeMode('shared'), 'global')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('defaults to auto for unknown', () => {
|
|
372
|
+
assert.equal(memDb.normalizeMemoryScopeMode('invalid'), 'auto')
|
|
373
|
+
assert.equal(memDb.normalizeMemoryScopeMode(''), 'auto')
|
|
374
|
+
assert.equal(memDb.normalizeMemoryScopeMode(null), 'auto')
|
|
375
|
+
assert.equal(memDb.normalizeMemoryScopeMode(undefined), 'auto')
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// --- getLatestBySessionCategory ---
|
|
380
|
+
|
|
381
|
+
describe('getLatestBySessionCategory', () => {
|
|
382
|
+
it('returns a memory for a valid session+category', () => {
|
|
383
|
+
const db = memDb.getMemoryDb()
|
|
384
|
+
const sessionId = `sess-latest-${Date.now()}`
|
|
385
|
+
db.add({ agentId: 'a', sessionId, category: 'working/context', title: 'Entry A', content: 'content alpha unique' })
|
|
386
|
+
db.add({ agentId: 'a', sessionId, category: 'working/context', title: 'Entry B', content: 'content beta unique' })
|
|
387
|
+
|
|
388
|
+
const latest = db.getLatestBySessionCategory(sessionId, 'working/context')
|
|
389
|
+
assert.ok(latest, 'Should return a memory entry')
|
|
390
|
+
assert.equal(latest!.sessionId, sessionId)
|
|
391
|
+
assert.equal(latest!.category, 'working/context')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('returns null for non-matching category', () => {
|
|
395
|
+
const db = memDb.getMemoryDb()
|
|
396
|
+
const sessionId = `sess-nomatch-${Date.now()}`
|
|
397
|
+
db.add({ agentId: 'a', sessionId, category: 'note', title: 'X', content: 'x content unique nomatch' })
|
|
398
|
+
assert.equal(db.getLatestBySessionCategory(sessionId, 'working/context'), null)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('returns null for empty session/category', () => {
|
|
402
|
+
const db = memDb.getMemoryDb()
|
|
403
|
+
assert.equal(db.getLatestBySessionCategory('', 'note'), null)
|
|
404
|
+
assert.equal(db.getLatestBySessionCategory('valid', ''), null)
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// --- countsByAgent ---
|
|
409
|
+
|
|
410
|
+
describe('countsByAgent', () => {
|
|
411
|
+
it('returns counts grouped by agent', () => {
|
|
412
|
+
const db = memDb.getMemoryDb()
|
|
413
|
+
// Data already exists from previous tests — just verify the shape
|
|
414
|
+
const counts = db.countsByAgent()
|
|
415
|
+
assert.equal(typeof counts, 'object')
|
|
416
|
+
// Should have at least one key
|
|
417
|
+
assert.ok(Object.keys(counts).length >= 1)
|
|
418
|
+
for (const [, val] of Object.entries(counts)) {
|
|
419
|
+
assert.equal(typeof val, 'number')
|
|
420
|
+
assert.ok(val > 0)
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
// --- Delete cleans up links ---
|
|
426
|
+
|
|
427
|
+
describe('delete cleans up linked references', () => {
|
|
428
|
+
it('removes deleted ID from other memories linkedMemoryIds', () => {
|
|
429
|
+
const db = memDb.getMemoryDb()
|
|
430
|
+
const a = db.add({ agentId: 'agent-cleanup', category: 'note', title: 'Cleanup A', content: 'Cleanup A content' })
|
|
431
|
+
const b = db.add({ agentId: 'agent-cleanup', category: 'note', title: 'Cleanup B', content: 'Cleanup B content' })
|
|
432
|
+
const c = db.add({ agentId: 'agent-cleanup', category: 'note', title: 'Cleanup C', content: 'Cleanup C content' })
|
|
433
|
+
|
|
434
|
+
db.link(a.id, [b.id, c.id])
|
|
435
|
+
|
|
436
|
+
// Verify links exist
|
|
437
|
+
const bBefore = db.get(b.id)
|
|
438
|
+
assert.ok(bBefore?.linkedMemoryIds?.includes(a.id))
|
|
439
|
+
|
|
440
|
+
// Delete A
|
|
441
|
+
db.delete(a.id)
|
|
442
|
+
|
|
443
|
+
// B and C should no longer reference A
|
|
444
|
+
const bAfter = db.get(b.id)
|
|
445
|
+
const cAfter = db.get(c.id)
|
|
446
|
+
const bLinks = bAfter?.linkedMemoryIds || []
|
|
447
|
+
const cLinks = cAfter?.linkedMemoryIds || []
|
|
448
|
+
assert.ok(!bLinks.includes(a.id), 'B should not reference deleted A')
|
|
449
|
+
assert.ok(!cLinks.includes(a.id), 'C should not reference deleted A')
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// --- addKnowledge ---
|
|
454
|
+
|
|
455
|
+
describe('addKnowledge', () => {
|
|
456
|
+
it('creates a global knowledge entry', () => {
|
|
457
|
+
const entry = memDb.addKnowledge({
|
|
458
|
+
title: 'API Rate Limits',
|
|
459
|
+
content: 'The API has a rate limit of 100 requests per minute.',
|
|
460
|
+
tags: ['api', 'limits'],
|
|
461
|
+
})
|
|
462
|
+
assert.ok(entry.id)
|
|
463
|
+
assert.equal(entry.category, 'knowledge')
|
|
464
|
+
assert.equal(entry.agentId, null)
|
|
465
|
+
assert.equal(entry.title, 'API Rate Limits')
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// --- searchKnowledge ---
|
|
470
|
+
|
|
471
|
+
describe('searchKnowledge', () => {
|
|
472
|
+
it('finds knowledge entries by query', () => {
|
|
473
|
+
// Add a knowledge entry with a unique term
|
|
474
|
+
memDb.addKnowledge({
|
|
475
|
+
title: 'Photosynthesis Process',
|
|
476
|
+
content: 'Chlorophyll absorbs sunlight to convert carbon dioxide into glucose.',
|
|
477
|
+
tags: ['biology', 'science'],
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
const results = memDb.searchKnowledge('chlorophyll photosynthesis glucose')
|
|
481
|
+
assert.ok(results.length >= 1, `Expected at least 1 result, got ${results.length}`)
|
|
482
|
+
assert.ok(results.every((r) => r.category === 'knowledge'))
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
})
|
|
@@ -16,10 +16,13 @@ import {
|
|
|
16
16
|
} from './memory-graph'
|
|
17
17
|
import { isWorkingMemoryCategory } from './memory-tiers'
|
|
18
18
|
|
|
19
|
-
import { DATA_DIR } from './data-dir'
|
|
19
|
+
import { DATA_DIR, MEMORY_IMAGES_DIR, WORKSPACE_DIR } from './data-dir'
|
|
20
|
+
import { safeJsonParse } from './json-utils'
|
|
21
|
+
import { tryResolvePathWithinBaseDir } from './path-utils'
|
|
20
22
|
|
|
21
23
|
const DB_PATH = path.join(DATA_DIR, 'memory.db')
|
|
22
|
-
const IMAGES_DIR =
|
|
24
|
+
const IMAGES_DIR = MEMORY_IMAGES_DIR
|
|
25
|
+
const APP_STATE_ROOT_DIR = path.dirname(DATA_DIR)
|
|
23
26
|
|
|
24
27
|
const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024 // 10MB
|
|
25
28
|
const IMAGE_EXT_WHITELIST = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff'])
|
|
@@ -303,15 +306,6 @@ export function getMemoryLookupLimits(settingsOverride?: Record<string, unknown>
|
|
|
303
306
|
return normalizeMemoryLookupLimits(settings)
|
|
304
307
|
}
|
|
305
308
|
|
|
306
|
-
function parseJsonSafe<T>(value: unknown, fallback: T): T {
|
|
307
|
-
if (typeof value !== 'string' || !value.trim()) return fallback
|
|
308
|
-
try {
|
|
309
|
-
return JSON.parse(value) as T
|
|
310
|
-
} catch {
|
|
311
|
-
return fallback
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
309
|
function normalizeReferencePath(raw: unknown): string | undefined {
|
|
316
310
|
if (typeof raw !== 'string') return undefined
|
|
317
311
|
const value = raw.trim()
|
|
@@ -354,9 +348,15 @@ export function buildFtsQuery(input: string): string {
|
|
|
354
348
|
|
|
355
349
|
function resolveExists(pathValue: string | undefined): boolean | undefined {
|
|
356
350
|
if (!pathValue) return undefined
|
|
357
|
-
const
|
|
351
|
+
const candidates = path.isAbsolute(pathValue)
|
|
352
|
+
? [pathValue]
|
|
353
|
+
: [
|
|
354
|
+
tryResolvePathWithinBaseDir(APP_STATE_ROOT_DIR, pathValue),
|
|
355
|
+
tryResolvePathWithinBaseDir(WORKSPACE_DIR, pathValue),
|
|
356
|
+
].filter((candidate): candidate is string => !!candidate)
|
|
357
|
+
if (candidates.length === 0) return undefined
|
|
358
358
|
try {
|
|
359
|
-
return fs.existsSync(
|
|
359
|
+
return candidates.some((candidate) => fs.existsSync(candidate))
|
|
360
360
|
} catch {
|
|
361
361
|
return undefined
|
|
362
362
|
}
|
|
@@ -577,10 +577,10 @@ function initDb() {
|
|
|
577
577
|
const migrateLegacyRows = db.transaction(() => {
|
|
578
578
|
let migrated = 0
|
|
579
579
|
for (const row of rowsForMigration) {
|
|
580
|
-
const legacyFilePaths =
|
|
581
|
-
const refs = normalizeReferences(
|
|
582
|
-
const image = normalizeImage(
|
|
583
|
-
const linkedIds = normalizeLinkedMemoryIds(
|
|
580
|
+
const legacyFilePaths = safeJsonParse<FileReference[]>(row.filePaths, [])
|
|
581
|
+
const refs = normalizeReferences(safeJsonParse<MemoryReference[]>(row.refs, []), legacyFilePaths)
|
|
582
|
+
const image = normalizeImage(safeJsonParse<MemoryImage | null>(row.image, null), row.imagePath)
|
|
583
|
+
const linkedIds = normalizeLinkedMemoryIds(safeJsonParse<string[]>(row.linkedMemoryIds, []), row.id)
|
|
584
584
|
|
|
585
585
|
const nextRefs = refs?.length ? JSON.stringify(refs) : null
|
|
586
586
|
const nextImage = image ? JSON.stringify(image) : null
|
|
@@ -699,11 +699,11 @@ function initDb() {
|
|
|
699
699
|
}
|
|
700
700
|
|
|
701
701
|
function rowToEntry(row: Record<string, unknown>): MemoryEntry {
|
|
702
|
-
const legacyFilePaths =
|
|
703
|
-
const references = normalizeReferences(
|
|
704
|
-
const image = normalizeImage(
|
|
702
|
+
const legacyFilePaths = safeJsonParse<FileReference[]>(row.filePaths, [])
|
|
703
|
+
const references = normalizeReferences(safeJsonParse<MemoryReference[]>(row.references, []), legacyFilePaths)
|
|
704
|
+
const image = normalizeImage(safeJsonParse<MemoryImage | null>(row.image, null), typeof row.imagePath === 'string' ? row.imagePath : null)
|
|
705
705
|
const filePaths = referencesToLegacyFilePaths(references)
|
|
706
|
-
const linkedMemoryIds = normalizeLinkedMemoryIds(
|
|
706
|
+
const linkedMemoryIds = normalizeLinkedMemoryIds(safeJsonParse<string[]>(row.linkedMemoryIds, []), typeof row.id === 'string' ? row.id : undefined)
|
|
707
707
|
|
|
708
708
|
return {
|
|
709
709
|
id: String(row.id || ''),
|
|
@@ -712,14 +712,14 @@ function initDb() {
|
|
|
712
712
|
category: typeof row.category === 'string' ? row.category : 'note',
|
|
713
713
|
title: typeof row.title === 'string' ? row.title : 'Untitled',
|
|
714
714
|
content: typeof row.content === 'string' ? row.content : '',
|
|
715
|
-
metadata:
|
|
715
|
+
metadata: safeJsonParse<Record<string, unknown> | undefined>(row.metadata, undefined),
|
|
716
716
|
references,
|
|
717
717
|
filePaths,
|
|
718
718
|
image,
|
|
719
719
|
imagePath: image?.path || undefined,
|
|
720
720
|
linkedMemoryIds: linkedMemoryIds.length ? linkedMemoryIds : undefined,
|
|
721
721
|
pinned: row.pinned === 1,
|
|
722
|
-
sharedWith:
|
|
722
|
+
sharedWith: safeJsonParse<string[]>(row.sharedWith, []).length ? safeJsonParse<string[]>(row.sharedWith, []) : undefined,
|
|
723
723
|
accessCount: typeof row.accessCount === 'number' ? row.accessCount : 0,
|
|
724
724
|
lastAccessedAt: typeof row.lastAccessedAt === 'number' ? row.lastAccessedAt : 0,
|
|
725
725
|
contentHash: typeof row.contentHash === 'string' ? row.contentHash : undefined,
|
|
@@ -888,14 +888,27 @@ function initDb() {
|
|
|
888
888
|
const row = stmts.getById.get(id) as Record<string, unknown> | undefined
|
|
889
889
|
const entry = row ? rowToEntry(row) : null
|
|
890
890
|
if (entry?.image?.path || entry?.imagePath) {
|
|
891
|
-
const
|
|
892
|
-
|
|
891
|
+
const imagePath = entry.image?.path || entry.imagePath || ''
|
|
892
|
+
const candidatePaths = path.isAbsolute(imagePath)
|
|
893
|
+
? [imagePath]
|
|
894
|
+
: [
|
|
895
|
+
tryResolvePathWithinBaseDir(APP_STATE_ROOT_DIR, imagePath),
|
|
896
|
+
tryResolvePathWithinBaseDir(WORKSPACE_DIR, imagePath),
|
|
897
|
+
].filter((candidate): candidate is string => !!candidate)
|
|
898
|
+
for (const imgPath of candidatePaths) {
|
|
899
|
+
try {
|
|
900
|
+
fs.unlinkSync(imgPath)
|
|
901
|
+
break
|
|
902
|
+
} catch {
|
|
903
|
+
// file may not exist
|
|
904
|
+
}
|
|
905
|
+
}
|
|
893
906
|
}
|
|
894
907
|
stmts.delete.run(id)
|
|
895
908
|
// Remove this ID from any other memory's linkedMemoryIds
|
|
896
909
|
const linking = stmts.findMemoriesLinkingTo.all(`%"${id}"%`) as any[]
|
|
897
910
|
for (const row of linking) {
|
|
898
|
-
const ids = normalizeLinkedMemoryIds(
|
|
911
|
+
const ids = normalizeLinkedMemoryIds(safeJsonParse<string[]>(row.linkedMemoryIds, []), row.id)
|
|
899
912
|
const filtered = ids.filter((lid: string) => lid !== id)
|
|
900
913
|
stmts.updateLinks.run(filtered.length ? JSON.stringify(filtered) : null, Date.now(), row.id)
|
|
901
914
|
}
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
normalizeMemoryLookupLimits,
|
|
6
6
|
resolveLookupRequest,
|
|
7
7
|
traverseLinkedMemoryGraph,
|
|
8
|
-
} from './memory-graph
|
|
9
|
-
import type { MemoryLookupLimits, LinkedMemoryNode } from './memory-graph
|
|
8
|
+
} from './memory-graph'
|
|
9
|
+
import type { MemoryLookupLimits, LinkedMemoryNode } from './memory-graph'
|
|
10
10
|
|
|
11
11
|
describe('normalizeLinkedMemoryIds', () => {
|
|
12
12
|
it('filters empty strings and self-references', () => {
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
} from './memory-policy'
|
|
11
11
|
|
|
12
12
|
test('normalizeMemoryCategory maps flat categories into hierarchical buckets', () => {
|
|
13
|
-
assert.equal(normalizeMemoryCategory('preference', 'User prefers terse replies'), 'identity/preferences')
|
|
14
|
-
assert.equal(normalizeMemoryCategory('decision', 'Ship the Docker path'), 'projects/decisions')
|
|
15
|
-
assert.equal(normalizeMemoryCategory('error', 'Root cause found'), 'execution/errors')
|
|
16
|
-
assert.equal(normalizeMemoryCategory('project', 'Repo setup'), 'projects/context')
|
|
13
|
+
assert.equal(normalizeMemoryCategory('preference', 'User prefers terse replies', null), 'identity/preferences')
|
|
14
|
+
assert.equal(normalizeMemoryCategory('decision', 'Ship the Docker path', null), 'projects/decisions')
|
|
15
|
+
assert.equal(normalizeMemoryCategory('error', 'Root cause found', null), 'execution/errors')
|
|
16
|
+
assert.equal(normalizeMemoryCategory('project', 'Repo setup', null), 'projects/context')
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
test('shouldInjectMemoryContext skips low-signal greetings and acknowledgements', () => {
|
|
@@ -54,12 +54,12 @@ test('isDirectMemoryWriteRequest detects remember-and-confirm turns without matc
|
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
test('shouldAutoCaptureMemory filters noisy turns', () => {
|
|
57
|
-
assert.equal(shouldAutoCaptureMemory({ message: 'thanks', response: 'Happy to help with that.'
|
|
58
|
-
assert.equal(shouldAutoCaptureMemory({ message: 'Please save this to memory', response: 'Stored memory "note".'
|
|
57
|
+
assert.equal(shouldAutoCaptureMemory({ message: 'thanks', response: 'Happy to help with that.' }), false)
|
|
58
|
+
assert.equal(shouldAutoCaptureMemory({ message: 'Please save this to memory', response: 'Stored memory "note".' }), false)
|
|
59
59
|
assert.equal(shouldAutoCaptureMemory({
|
|
60
60
|
message: 'We decided to use the shared staging environment and keep the worker count at 2 for now.',
|
|
61
61
|
response: 'Decision captured: shared staging, worker count 2, and we will revisit after load testing next week.',
|
|
62
|
-
source: 'chat',
|
|
62
|
+
// source: 'chat',
|
|
63
63
|
}), true)
|
|
64
64
|
})
|
|
65
65
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
3
|
import type { MemoryEntry } from '@/types'
|
|
4
|
-
import { filterMemoriesByScope, normalizeMemoryScopeMode } from './memory-db
|
|
4
|
+
import { filterMemoriesByScope, normalizeMemoryScopeMode } from './memory-db'
|
|
5
5
|
|
|
6
6
|
function makeEntry(id: string, patch: Partial<MemoryEntry> = {}): MemoryEntry {
|
|
7
7
|
return {
|