@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,409 @@
|
|
|
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
|
+
import type { Message } from '@/types'
|
|
8
|
+
|
|
9
|
+
const originalEnv = {
|
|
10
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
11
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
12
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let tempDir = ''
|
|
16
|
+
let cm: typeof import('./context-manager')
|
|
17
|
+
|
|
18
|
+
before(async () => {
|
|
19
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-context-manager-'))
|
|
20
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
21
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
22
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
23
|
+
cm = await import('./context-manager')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
after(() => {
|
|
27
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
28
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
29
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
30
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
31
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
32
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function makeMsg(role: 'user' | 'assistant', text: string, toolEvents?: Message['toolEvents']): Message {
|
|
37
|
+
return { role, text, time: Date.now(), toolEvents }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('context-manager', () => {
|
|
41
|
+
// --- estimateTokens ---
|
|
42
|
+
|
|
43
|
+
describe('estimateTokens', () => {
|
|
44
|
+
it('returns 0 for empty string', () => {
|
|
45
|
+
assert.equal(cm.estimateTokens(''), 0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns 0 for falsy input', () => {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
assert.equal(cm.estimateTokens(null as any), 0)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('estimates ~1 token per 4 chars', () => {
|
|
54
|
+
const tokens = cm.estimateTokens('abcdefghijklmnop') // 16 chars
|
|
55
|
+
assert.equal(tokens, 4)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('rounds up fractional token counts', () => {
|
|
59
|
+
const tokens = cm.estimateTokens('abcde') // 5 chars -> ceil(5/4) = 2
|
|
60
|
+
assert.equal(tokens, 2)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// --- estimateMessagesTokens ---
|
|
65
|
+
|
|
66
|
+
describe('estimateMessagesTokens', () => {
|
|
67
|
+
it('returns 0 for empty array', () => {
|
|
68
|
+
assert.equal(cm.estimateMessagesTokens([]), 0)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('includes per-message overhead', () => {
|
|
72
|
+
const msgs = [makeMsg('user', 'hi')]
|
|
73
|
+
const tokens = cm.estimateMessagesTokens(msgs)
|
|
74
|
+
// 4 overhead + ceil(2/4) = 4 + 1 = 5
|
|
75
|
+
assert.equal(tokens, 5)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('includes tool event tokens', () => {
|
|
79
|
+
const msgs = [makeMsg('assistant', 'ok', [
|
|
80
|
+
{ name: 'web_search', input: '{"q":"test query string here"}', output: 'result data here' },
|
|
81
|
+
])]
|
|
82
|
+
const tokens = cm.estimateMessagesTokens(msgs)
|
|
83
|
+
// Should be > just the text tokens
|
|
84
|
+
assert.ok(tokens > 5, `Expected more than 5 tokens with tool events, got ${tokens}`)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// --- getContextWindowSize ---
|
|
89
|
+
|
|
90
|
+
describe('getContextWindowSize', () => {
|
|
91
|
+
it('returns known model window size', () => {
|
|
92
|
+
assert.equal(cm.getContextWindowSize('anthropic', 'claude-opus-4-6'), 200_000)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('falls back to provider default for unknown model', () => {
|
|
96
|
+
assert.equal(cm.getContextWindowSize('anthropic', 'claude-unknown-model'), 200_000)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('falls back to 8192 for unknown provider and model', () => {
|
|
100
|
+
assert.equal(cm.getContextWindowSize('unknown-provider', 'unknown-model'), 8_192)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('returns openai model sizes', () => {
|
|
104
|
+
assert.equal(cm.getContextWindowSize('openai', 'gpt-4o'), 128_000)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// --- getContextStatus ---
|
|
109
|
+
|
|
110
|
+
describe('getContextStatus', () => {
|
|
111
|
+
it('returns ok for small context usage', () => {
|
|
112
|
+
const msgs = [makeMsg('user', 'hello')]
|
|
113
|
+
const status = cm.getContextStatus(msgs, 100, 'anthropic', 'claude-opus-4-6')
|
|
114
|
+
assert.equal(status.strategy, 'ok')
|
|
115
|
+
assert.ok(status.percentUsed < 70)
|
|
116
|
+
assert.equal(status.contextWindow, 200_000)
|
|
117
|
+
assert.equal(status.messageCount, 1)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('returns warning at 70%+ usage', () => {
|
|
121
|
+
// 200k window, need ~140k tokens. 140000 tokens * 4 chars = 560000 chars
|
|
122
|
+
const bigText = 'x'.repeat(560_000)
|
|
123
|
+
const msgs = [makeMsg('user', bigText)]
|
|
124
|
+
const status = cm.getContextStatus(msgs, 0, 'anthropic', 'claude-opus-4-6')
|
|
125
|
+
assert.equal(status.strategy, 'warning')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('returns critical at 90%+ usage', () => {
|
|
129
|
+
const bigText = 'x'.repeat(720_000) // 180k tokens
|
|
130
|
+
const msgs = [makeMsg('user', bigText)]
|
|
131
|
+
const status = cm.getContextStatus(msgs, 0, 'anthropic', 'claude-opus-4-6')
|
|
132
|
+
assert.equal(status.strategy, 'critical')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// --- getContextDegradationWarning ---
|
|
137
|
+
|
|
138
|
+
describe('getContextDegradationWarning', () => {
|
|
139
|
+
it('returns null below 60%', () => {
|
|
140
|
+
const msgs = [makeMsg('user', 'short message')]
|
|
141
|
+
const warning = cm.getContextDegradationWarning(msgs, 100, 'anthropic', 'claude-opus-4-6')
|
|
142
|
+
assert.equal(warning, null)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('returns warning at 85%+', () => {
|
|
146
|
+
const bigText = 'x'.repeat(680_000) // ~170k tokens
|
|
147
|
+
const msgs = [makeMsg('user', bigText)]
|
|
148
|
+
const warning = cm.getContextDegradationWarning(msgs, 0, 'anthropic', 'claude-opus-4-6')
|
|
149
|
+
assert.ok(warning !== null)
|
|
150
|
+
assert.ok(warning!.includes('CRITICAL'))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('returns softer warning between 60-70%', () => {
|
|
154
|
+
// Need 60-70% of 200k = 120k-140k tokens = 480k-560k chars
|
|
155
|
+
const text = 'x'.repeat(500_000)
|
|
156
|
+
const msgs = [makeMsg('user', text)]
|
|
157
|
+
const warning = cm.getContextDegradationWarning(msgs, 0, 'anthropic', 'claude-opus-4-6')
|
|
158
|
+
assert.ok(warning !== null)
|
|
159
|
+
assert.ok(warning!.includes('Consider saving'))
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// --- shouldAutoCompact ---
|
|
164
|
+
|
|
165
|
+
describe('shouldAutoCompact', () => {
|
|
166
|
+
it('returns false for small context', () => {
|
|
167
|
+
const msgs = [makeMsg('user', 'hello')]
|
|
168
|
+
assert.equal(cm.shouldAutoCompact(msgs, 100, 'anthropic', 'claude-opus-4-6'), false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns true when context exceeds threshold', () => {
|
|
172
|
+
const bigText = 'x'.repeat(660_000) // ~165k tokens -> 82.5% of 200k
|
|
173
|
+
const msgs = [makeMsg('user', bigText)]
|
|
174
|
+
assert.equal(cm.shouldAutoCompact(msgs, 0, 'anthropic', 'claude-opus-4-6'), true)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('respects custom trigger percent', () => {
|
|
178
|
+
const bigText = 'x'.repeat(400_000) // ~100k tokens -> 50% of 200k
|
|
179
|
+
const msgs = [makeMsg('user', bigText)]
|
|
180
|
+
assert.equal(cm.shouldAutoCompact(msgs, 0, 'anthropic', 'claude-opus-4-6', 40), true)
|
|
181
|
+
assert.equal(cm.shouldAutoCompact(msgs, 0, 'anthropic', 'claude-opus-4-6', 60), false)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// --- slidingWindowCompact ---
|
|
186
|
+
|
|
187
|
+
describe('slidingWindowCompact', () => {
|
|
188
|
+
it('returns all messages when under limit', () => {
|
|
189
|
+
const msgs = [makeMsg('user', 'a'), makeMsg('assistant', 'b')]
|
|
190
|
+
const result = cm.slidingWindowCompact(msgs, 5)
|
|
191
|
+
assert.equal(result.length, 2)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('keeps only last N messages', () => {
|
|
195
|
+
const msgs = Array.from({ length: 20 }, (_, i) => makeMsg('user', `msg-${i}`))
|
|
196
|
+
const result = cm.slidingWindowCompact(msgs, 5)
|
|
197
|
+
assert.equal(result.length, 5)
|
|
198
|
+
assert.equal(result[0].text, 'msg-15')
|
|
199
|
+
assert.equal(result[4].text, 'msg-19')
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// --- splitMessagesByTokenBudget ---
|
|
204
|
+
|
|
205
|
+
describe('splitMessagesByTokenBudget', () => {
|
|
206
|
+
it('returns empty array for empty messages', () => {
|
|
207
|
+
assert.deepEqual(cm.splitMessagesByTokenBudget([], 1000), [])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('keeps all in one chunk when within budget', () => {
|
|
211
|
+
const msgs = [makeMsg('user', 'hi'), makeMsg('assistant', 'hello')]
|
|
212
|
+
const chunks = cm.splitMessagesByTokenBudget(msgs, 10000)
|
|
213
|
+
assert.equal(chunks.length, 1)
|
|
214
|
+
assert.equal(chunks[0].length, 2)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('splits messages across multiple chunks', () => {
|
|
218
|
+
const msgs = Array.from({ length: 10 }, () =>
|
|
219
|
+
makeMsg('user', 'x'.repeat(400)), // ~100 tokens + 4 overhead each
|
|
220
|
+
)
|
|
221
|
+
// Budget of 210 tokens should fit ~2 messages per chunk
|
|
222
|
+
const chunks = cm.splitMessagesByTokenBudget(msgs, 210)
|
|
223
|
+
assert.ok(chunks.length > 1, `Expected multiple chunks, got ${chunks.length}`)
|
|
224
|
+
// All messages should be accounted for
|
|
225
|
+
const totalMsgs = chunks.reduce((sum, c) => sum + c.length, 0)
|
|
226
|
+
assert.equal(totalMsgs, 10)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// --- computeAdaptiveChunkRatio ---
|
|
231
|
+
|
|
232
|
+
describe('computeAdaptiveChunkRatio', () => {
|
|
233
|
+
it('returns base ratio for empty messages', () => {
|
|
234
|
+
assert.equal(cm.computeAdaptiveChunkRatio([], 200_000), 0.4)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('returns base ratio for small messages', () => {
|
|
238
|
+
const msgs = [makeMsg('user', 'short')]
|
|
239
|
+
const ratio = cm.computeAdaptiveChunkRatio(msgs, 200_000)
|
|
240
|
+
assert.equal(ratio, 0.4)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('reduces ratio for large average messages', () => {
|
|
244
|
+
// Need avgRatio > 0.1: safeAvgTokens / contextWindow > 0.1
|
|
245
|
+
// For 200k window: safeAvgTokens > 20k -> avgTokens > 20k/1.2 ~ 16667 -> chars > 66668
|
|
246
|
+
const msgs = Array.from({ length: 4 }, () => makeMsg('user', 'x'.repeat(80_000)))
|
|
247
|
+
const ratio = cm.computeAdaptiveChunkRatio(msgs, 200_000)
|
|
248
|
+
assert.ok(ratio < 0.4, `Expected ratio < 0.4, got ${ratio}`)
|
|
249
|
+
assert.ok(ratio >= 0.15, `Expected ratio >= 0.15, got ${ratio}`)
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// --- extractToolFailures ---
|
|
254
|
+
|
|
255
|
+
describe('extractToolFailures', () => {
|
|
256
|
+
it('returns empty array when no failures', () => {
|
|
257
|
+
const msgs = [makeMsg('assistant', 'ok', [
|
|
258
|
+
{ name: 'web', input: '{}', output: 'data' },
|
|
259
|
+
])]
|
|
260
|
+
assert.deepEqual(cm.extractToolFailures(msgs), [])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('extracts tool failures', () => {
|
|
264
|
+
const msgs = [makeMsg('assistant', 'fail', [
|
|
265
|
+
{ name: 'shell', input: 'ls', output: 'command not found', error: true },
|
|
266
|
+
])]
|
|
267
|
+
const failures = cm.extractToolFailures(msgs)
|
|
268
|
+
assert.equal(failures.length, 1)
|
|
269
|
+
assert.ok(failures[0].includes('[shell]'))
|
|
270
|
+
assert.ok(failures[0].includes('error'))
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('limits to MAX_TOOL_FAILURES (8)', () => {
|
|
274
|
+
const events = Array.from({ length: 20 }, (_, i) => ({
|
|
275
|
+
name: `tool-${i}`, input: '{}', output: `err-${i}`, error: true as const,
|
|
276
|
+
}))
|
|
277
|
+
const msgs = [makeMsg('assistant', 'many errors', events)]
|
|
278
|
+
const failures = cm.extractToolFailures(msgs)
|
|
279
|
+
assert.equal(failures.length, 8)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// --- extractFileOperations ---
|
|
284
|
+
|
|
285
|
+
describe('extractFileOperations', () => {
|
|
286
|
+
it('returns empty sets when no file ops', () => {
|
|
287
|
+
const msgs = [makeMsg('user', 'hello')]
|
|
288
|
+
const ops = cm.extractFileOperations(msgs)
|
|
289
|
+
assert.deepEqual(ops, { read: [], modified: [] })
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('extracts read operations', () => {
|
|
293
|
+
const msgs = [makeMsg('assistant', 'reading', [
|
|
294
|
+
{ name: 'read_file', input: JSON.stringify({ filePath: '/tmp/test.ts' }) },
|
|
295
|
+
])]
|
|
296
|
+
const ops = cm.extractFileOperations(msgs)
|
|
297
|
+
assert.deepEqual(ops.read, ['/tmp/test.ts'])
|
|
298
|
+
assert.deepEqual(ops.modified, [])
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('extracts write operations', () => {
|
|
302
|
+
const msgs = [makeMsg('assistant', 'writing', [
|
|
303
|
+
{ name: 'write_file', input: JSON.stringify({ filePath: '/tmp/out.ts' }) },
|
|
304
|
+
{ name: 'edit_file', input: JSON.stringify({ filePath: '/tmp/edit.ts' }) },
|
|
305
|
+
])]
|
|
306
|
+
const ops = cm.extractFileOperations(msgs)
|
|
307
|
+
assert.deepEqual(ops.read, [])
|
|
308
|
+
assert.equal(ops.modified.length, 2)
|
|
309
|
+
assert.ok(ops.modified.includes('/tmp/out.ts'))
|
|
310
|
+
assert.ok(ops.modified.includes('/tmp/edit.ts'))
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('deduplicates paths', () => {
|
|
314
|
+
const msgs = [
|
|
315
|
+
makeMsg('assistant', 'op1', [
|
|
316
|
+
{ name: 'read_file', input: JSON.stringify({ filePath: '/tmp/same.ts' }) },
|
|
317
|
+
]),
|
|
318
|
+
makeMsg('assistant', 'op2', [
|
|
319
|
+
{ name: 'read_file', input: JSON.stringify({ filePath: '/tmp/same.ts' }) },
|
|
320
|
+
]),
|
|
321
|
+
]
|
|
322
|
+
const ops = cm.extractFileOperations(msgs)
|
|
323
|
+
assert.equal(ops.read.length, 1)
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// --- llmCompact ---
|
|
328
|
+
|
|
329
|
+
describe('llmCompact', () => {
|
|
330
|
+
it('returns original messages when under keepLastN', async () => {
|
|
331
|
+
const msgs = [makeMsg('user', 'hi'), makeMsg('assistant', 'hey')]
|
|
332
|
+
const result = await cm.llmCompact({
|
|
333
|
+
messages: msgs,
|
|
334
|
+
provider: 'anthropic',
|
|
335
|
+
model: 'claude-opus-4-6',
|
|
336
|
+
agentId: null,
|
|
337
|
+
sessionId: 'test-session',
|
|
338
|
+
summarize: async () => 'summary',
|
|
339
|
+
})
|
|
340
|
+
assert.equal(result.prunedCount, 0)
|
|
341
|
+
assert.equal(result.summaryAdded, false)
|
|
342
|
+
assert.equal(result.messages.length, 2)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('summarizes old messages and keeps recent ones', async () => {
|
|
346
|
+
const msgs = Array.from({ length: 15 }, (_, i) =>
|
|
347
|
+
makeMsg(i % 2 === 0 ? 'user' : 'assistant', `message-${i}`),
|
|
348
|
+
)
|
|
349
|
+
const result = await cm.llmCompact({
|
|
350
|
+
messages: msgs,
|
|
351
|
+
provider: 'anthropic',
|
|
352
|
+
model: 'claude-opus-4-6',
|
|
353
|
+
agentId: null,
|
|
354
|
+
sessionId: 'test-session',
|
|
355
|
+
summarize: async () => 'This is a test summary of the conversation.',
|
|
356
|
+
keepLastN: 5,
|
|
357
|
+
})
|
|
358
|
+
assert.equal(result.prunedCount, 10)
|
|
359
|
+
assert.equal(result.summaryAdded, true)
|
|
360
|
+
// summary message + 5 recent
|
|
361
|
+
assert.equal(result.messages.length, 6)
|
|
362
|
+
assert.ok(result.messages[0].text.includes('[Context Summary]'))
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('falls back to sliding window when summarizer fails', async () => {
|
|
366
|
+
const msgs = Array.from({ length: 15 }, (_, i) =>
|
|
367
|
+
makeMsg(i % 2 === 0 ? 'user' : 'assistant', `message-${i}`),
|
|
368
|
+
)
|
|
369
|
+
const result = await cm.llmCompact({
|
|
370
|
+
messages: msgs,
|
|
371
|
+
provider: 'anthropic',
|
|
372
|
+
model: 'claude-opus-4-6',
|
|
373
|
+
agentId: null,
|
|
374
|
+
sessionId: 'test-session',
|
|
375
|
+
summarize: async () => { throw new Error('LLM unavailable') },
|
|
376
|
+
keepLastN: 5,
|
|
377
|
+
})
|
|
378
|
+
assert.equal(result.summaryAdded, false)
|
|
379
|
+
assert.equal(result.messages.length, 5)
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// --- consolidateToMemory ---
|
|
384
|
+
|
|
385
|
+
describe('consolidateToMemory', () => {
|
|
386
|
+
it('returns 0 when agentId is null', () => {
|
|
387
|
+
const msgs = [makeMsg('assistant', 'We decided to use Rust.')]
|
|
388
|
+
assert.equal(cm.consolidateToMemory(msgs, null, 'session-1'), 0)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('stores memories for decision-containing messages', () => {
|
|
392
|
+
const msgs = [makeMsg('assistant', 'We decided to refactor the module using a new approach.')]
|
|
393
|
+
const stored = cm.consolidateToMemory(msgs, 'agent-test', 'session-test')
|
|
394
|
+
assert.ok(stored >= 1, `Expected at least 1 memory stored, got ${stored}`)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('skips user messages', () => {
|
|
398
|
+
const msgs = [makeMsg('user', 'We decided to use TypeScript.')]
|
|
399
|
+
const stored = cm.consolidateToMemory(msgs, 'agent-test', 'session-test')
|
|
400
|
+
assert.equal(stored, 0)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('skips messages without decision/fact/result keywords', () => {
|
|
404
|
+
const msgs = [makeMsg('assistant', 'Hello there, how are you?')]
|
|
405
|
+
const stored = cm.consolidateToMemory(msgs, 'agent-test', 'session-test')
|
|
406
|
+
assert.equal(stored, 0)
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
3
|
import type { Agent } from '@/types'
|
|
4
|
-
import { checkAgentBudgetLimits, getAgentSpendWindows } from './cost
|
|
4
|
+
import { checkAgentBudgetLimits, getAgentSpendWindows } from './cost'
|
|
5
5
|
|
|
6
6
|
function buildNowTs(): number {
|
|
7
7
|
const d = new Date()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
|
|
2
|
+
import { isProductionRuntime } from '@/lib/runtime-env'
|
|
3
|
+
import type { Session } from '@/types'
|
|
4
|
+
|
|
5
|
+
const SYNTHETIC_HEALTH_SESSION_USERS = new Set(['workbench', 'comparison-bench'])
|
|
6
|
+
const SYNTHETIC_HEALTH_SESSION_PREFIXES = ['wb-', 'cmp-']
|
|
7
|
+
|
|
8
|
+
function parseBoolish(value: unknown, fallback: boolean): boolean {
|
|
9
|
+
if (typeof value === 'boolean') return value
|
|
10
|
+
if (typeof value !== 'string') return fallback
|
|
11
|
+
const normalized = value.trim().toLowerCase()
|
|
12
|
+
if (!normalized) return fallback
|
|
13
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
|
|
14
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false
|
|
15
|
+
return fallback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function daemonAutostartEnvEnabled(): boolean {
|
|
19
|
+
return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, isProductionRuntime())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isDaemonBackgroundServicesEnabled(): boolean {
|
|
23
|
+
return parseBoolish(process.env.SWARMCLAW_DAEMON_BACKGROUND_SERVICES, true)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseHeartbeatIntervalSec(
|
|
27
|
+
value: unknown,
|
|
28
|
+
fallback = DEFAULT_HEARTBEAT_INTERVAL_SEC,
|
|
29
|
+
): number {
|
|
30
|
+
const parsed = typeof value === 'number'
|
|
31
|
+
? value
|
|
32
|
+
: typeof value === 'string'
|
|
33
|
+
? Number.parseInt(value, 10)
|
|
34
|
+
: Number.NaN
|
|
35
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
36
|
+
return Math.max(0, Math.min(3600, Math.trunc(parsed)))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function shouldNotifyProviderReachabilityIssue(provider: string): boolean {
|
|
40
|
+
return provider !== 'openclaw'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasSyntheticHealthPrefix(value: unknown): boolean {
|
|
44
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
45
|
+
return SYNTHETIC_HEALTH_SESSION_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function shouldSuppressSessionHeartbeatHealthAlert(
|
|
49
|
+
session: Pick<Session, 'id' | 'name' | 'user' | 'shortcutForAgentId'>,
|
|
50
|
+
): boolean {
|
|
51
|
+
const user = typeof session.user === 'string' ? session.user.trim().toLowerCase() : ''
|
|
52
|
+
if (SYNTHETIC_HEALTH_SESSION_USERS.has(user)) return true
|
|
53
|
+
if (hasSyntheticHealthPrefix(session.id)) return true
|
|
54
|
+
if (hasSyntheticHealthPrefix(session.shortcutForAgentId)) return true
|
|
55
|
+
|
|
56
|
+
const name = typeof session.name === 'string' ? session.name.trim().toLowerCase() : ''
|
|
57
|
+
return name.startsWith('workbench ')
|
|
58
|
+
|| name.startsWith('assistant benchmark ')
|
|
59
|
+
|| name.startsWith('comparison ')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function shouldSuppressSyntheticAgentHealthAlert(agentId: string): boolean {
|
|
63
|
+
return hasSyntheticHealthPrefix(agentId)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildSessionHeartbeatHealthDedupKey(
|
|
67
|
+
sessionId: string,
|
|
68
|
+
state: 'stale' | 'auto-disabled',
|
|
69
|
+
): string {
|
|
70
|
+
return `health-alert:session-heartbeat:${state}:${sessionId}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseCronToMs(cron: string | null | undefined, fallbackMs: number): number | null {
|
|
74
|
+
if (!cron || typeof cron !== 'string') return null
|
|
75
|
+
const hourMatch = cron.match(/\*\/(\d+)/)
|
|
76
|
+
if (hourMatch) return parseInt(hourMatch[1], 10) * 3600_000
|
|
77
|
+
return fallbackMs
|
|
78
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { runWithTempDataDir } from './test-utils/run-with-temp-data-dir'
|
|
4
|
+
|
|
5
|
+
describe('connector lifecycle for daemon recovery', () => {
|
|
6
|
+
it('preserves enabled connectors across runtime stop/start and auto-starts them again', () => {
|
|
7
|
+
const output = runWithTempDataDir(`
|
|
8
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
9
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
10
|
+
const pluginsMod = await import('./src/lib/server/plugins.ts')
|
|
11
|
+
const storage = storageMod.default || storageMod
|
|
12
|
+
const manager = managerMod.default || managerMod
|
|
13
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
14
|
+
|
|
15
|
+
let startCount = 0
|
|
16
|
+
plugins.getPluginManager().registerBuiltin('test-daemon-autostart-plugin', {
|
|
17
|
+
name: 'Test Daemon Autostart Plugin',
|
|
18
|
+
connectors: [{
|
|
19
|
+
id: 'test-daemon-autostart',
|
|
20
|
+
name: 'Test Daemon Autostart',
|
|
21
|
+
description: 'Connector started by runtime autostart',
|
|
22
|
+
startListener: async () => {
|
|
23
|
+
startCount += 1
|
|
24
|
+
return async () => {}
|
|
25
|
+
},
|
|
26
|
+
}],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const now = Date.now()
|
|
30
|
+
storage.saveSettings({})
|
|
31
|
+
storage.saveConnectors({
|
|
32
|
+
conn_auto: {
|
|
33
|
+
id: 'conn_auto',
|
|
34
|
+
name: 'Autostart Connector',
|
|
35
|
+
platform: 'test-daemon-autostart',
|
|
36
|
+
agentId: null,
|
|
37
|
+
credentialId: null,
|
|
38
|
+
config: { botToken: 'test-token' },
|
|
39
|
+
isEnabled: true,
|
|
40
|
+
status: 'stopped',
|
|
41
|
+
createdAt: now,
|
|
42
|
+
updatedAt: now,
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await manager.startConnector('conn_auto')
|
|
47
|
+
const firstStart = {
|
|
48
|
+
running: manager.listRunningConnectors(),
|
|
49
|
+
connector: storage.loadConnectors().conn_auto,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await manager.stopAllConnectors({ disable: false })
|
|
53
|
+
const afterStop = storage.loadConnectors().conn_auto
|
|
54
|
+
|
|
55
|
+
await manager.autoStartConnectors()
|
|
56
|
+
const secondStart = {
|
|
57
|
+
running: manager.listRunningConnectors(),
|
|
58
|
+
connector: storage.loadConnectors().conn_auto,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await manager.stopAllConnectors()
|
|
62
|
+
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
startCount,
|
|
65
|
+
firstStart,
|
|
66
|
+
afterStop,
|
|
67
|
+
secondStart,
|
|
68
|
+
}))
|
|
69
|
+
`, { prefix: 'swarmclaw-daemon-test-' })
|
|
70
|
+
|
|
71
|
+
assert.equal(output.startCount, 2)
|
|
72
|
+
assert.equal(output.firstStart.running.some((entry: { id: string }) => entry.id === 'conn_auto'), true)
|
|
73
|
+
assert.equal(output.firstStart.connector.status, 'running')
|
|
74
|
+
assert.equal(output.afterStop.isEnabled, true)
|
|
75
|
+
assert.equal(output.afterStop.status, 'stopped')
|
|
76
|
+
assert.equal(output.secondStart.running.some((entry: { id: string }) => entry.id === 'conn_auto'), true)
|
|
77
|
+
assert.equal(output.secondStart.connector.status, 'running')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('restarts unhealthy connectors through the daemon recovery path', () => {
|
|
81
|
+
const output = runWithTempDataDir(`
|
|
82
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
83
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
84
|
+
const pluginsMod = await import('./src/lib/server/plugins.ts')
|
|
85
|
+
const storage = storageMod.default || storageMod
|
|
86
|
+
const manager = managerMod.default || managerMod
|
|
87
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
88
|
+
|
|
89
|
+
let startCount = 0
|
|
90
|
+
let stopCount = 0
|
|
91
|
+
plugins.getPluginManager().registerBuiltin('test-daemon-restart-plugin', {
|
|
92
|
+
name: 'Test Daemon Restart Plugin',
|
|
93
|
+
connectors: [{
|
|
94
|
+
id: 'test-daemon-restart',
|
|
95
|
+
name: 'Test Daemon Restart',
|
|
96
|
+
description: 'Connector restarted by daemon-style recovery',
|
|
97
|
+
startListener: async () => {
|
|
98
|
+
startCount += 1
|
|
99
|
+
return async () => {}
|
|
100
|
+
},
|
|
101
|
+
}],
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const now = Date.now()
|
|
105
|
+
storage.saveSettings({})
|
|
106
|
+
storage.saveConnectors({
|
|
107
|
+
conn_restart: {
|
|
108
|
+
id: 'conn_restart',
|
|
109
|
+
name: 'Restart Connector',
|
|
110
|
+
platform: 'test-daemon-restart',
|
|
111
|
+
agentId: null,
|
|
112
|
+
credentialId: null,
|
|
113
|
+
config: { botToken: 'test-token' },
|
|
114
|
+
isEnabled: true,
|
|
115
|
+
status: 'running',
|
|
116
|
+
lastError: null,
|
|
117
|
+
createdAt: now,
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const running = globalThis.__swarmclaw_running_connectors__ || new Map()
|
|
123
|
+
globalThis.__swarmclaw_running_connectors__ = running
|
|
124
|
+
running.set('conn_restart', {
|
|
125
|
+
connector: storage.loadConnectors().conn_restart,
|
|
126
|
+
authenticated: true,
|
|
127
|
+
isAlive: () => false,
|
|
128
|
+
stop: async () => {
|
|
129
|
+
stopCount += 1
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await manager.checkConnectorHealth()
|
|
134
|
+
const reconnectState = manager.getReconnectState('conn_restart')
|
|
135
|
+
if (reconnectState && !reconnectState.exhausted) {
|
|
136
|
+
await manager.startConnector('conn_restart')
|
|
137
|
+
manager.clearReconnectState('conn_restart')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const health = Object.values(storage.loadConnectorHealth())
|
|
141
|
+
.filter((entry) => entry.connectorId === 'conn_restart')
|
|
142
|
+
.map((entry) => entry.event)
|
|
143
|
+
|
|
144
|
+
console.log(JSON.stringify({
|
|
145
|
+
startCount,
|
|
146
|
+
stopCount,
|
|
147
|
+
status: manager.getConnectorStatus('conn_restart'),
|
|
148
|
+
reconnectState: manager.getReconnectState('conn_restart'),
|
|
149
|
+
connector: storage.loadConnectors().conn_restart,
|
|
150
|
+
health,
|
|
151
|
+
running: manager.listRunningConnectors(),
|
|
152
|
+
}))
|
|
153
|
+
|
|
154
|
+
await manager.stopAllConnectors()
|
|
155
|
+
`, { prefix: 'swarmclaw-daemon-test-' })
|
|
156
|
+
|
|
157
|
+
assert.equal(output.startCount, 1)
|
|
158
|
+
assert.equal(output.stopCount, 1)
|
|
159
|
+
assert.equal(output.status, 'running')
|
|
160
|
+
assert.equal(output.reconnectState, null)
|
|
161
|
+
assert.equal(output.connector.status, 'running')
|
|
162
|
+
assert.equal(output.connector.lastError, null)
|
|
163
|
+
assert.equal(output.health.includes('disconnected'), true)
|
|
164
|
+
assert.equal(output.health.includes('started'), true)
|
|
165
|
+
assert.equal(output.running.some((entry: { id: string }) => entry.id === 'conn_restart'), true)
|
|
166
|
+
})
|
|
167
|
+
})
|