@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,406 @@
|
|
|
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 workspaceDir = ''
|
|
15
|
+
let mod: typeof import('./heartbeat-service')
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-heartbeat-svc-'))
|
|
19
|
+
workspaceDir = path.join(tempDir, 'workspace')
|
|
20
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
21
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
22
|
+
process.env.WORKSPACE_DIR = workspaceDir
|
|
23
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
24
|
+
mod = await import('./heartbeat-service')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
after(() => {
|
|
28
|
+
// Stop the service in case any test started it
|
|
29
|
+
try { mod.stopHeartbeatService() } catch { /* ignore */ }
|
|
30
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
31
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
32
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
33
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
34
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
35
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
36
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ── stripBlockedItems ───────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('stripBlockedItems', () => {
|
|
42
|
+
it('returns empty string for empty input', () => {
|
|
43
|
+
assert.equal(mod.stripBlockedItems(''), '')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('removes list items with (blocked) marker', () => {
|
|
47
|
+
const input = '- Task A\n- Task B (blocked, no update)\n- Task C'
|
|
48
|
+
const result = mod.stripBlockedItems(input)
|
|
49
|
+
assert.ok(result.includes('Task A'))
|
|
50
|
+
assert.ok(!result.includes('Task B'))
|
|
51
|
+
assert.ok(result.includes('Task C'))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('removes items with various blocked formats', () => {
|
|
55
|
+
const input = [
|
|
56
|
+
'- Normal item',
|
|
57
|
+
'* Blocked one (blocked: awaiting approval)',
|
|
58
|
+
'+ Another (blocked by dependency)',
|
|
59
|
+
'- Keep this',
|
|
60
|
+
].join('\n')
|
|
61
|
+
const result = mod.stripBlockedItems(input)
|
|
62
|
+
assert.ok(!result.includes('Blocked one'))
|
|
63
|
+
assert.ok(!result.includes('Another (blocked'))
|
|
64
|
+
assert.ok(result.includes('Normal item'))
|
|
65
|
+
assert.ok(result.includes('Keep this'))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('preserves headers even if they mention blocked', () => {
|
|
69
|
+
const input = '## Blocked Tasks\n- Item (blocked)'
|
|
70
|
+
const result = mod.stripBlockedItems(input)
|
|
71
|
+
assert.ok(result.includes('## Blocked Tasks'))
|
|
72
|
+
assert.ok(!result.includes('- Item'))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('preserves non-list lines mentioning blocked', () => {
|
|
76
|
+
const input = 'Some blocked context text\n- Real blocked item (blocked)'
|
|
77
|
+
const result = mod.stripBlockedItems(input)
|
|
78
|
+
assert.ok(result.includes('Some blocked context'))
|
|
79
|
+
assert.ok(!result.includes('Real blocked'))
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// ── isHeartbeatContentEffectivelyEmpty ───────────────────────────────────
|
|
84
|
+
|
|
85
|
+
describe('isHeartbeatContentEffectivelyEmpty', () => {
|
|
86
|
+
it('returns true for null/undefined/empty', () => {
|
|
87
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty(null), true)
|
|
88
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty(undefined), true)
|
|
89
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty(''), true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('returns true for headers only', () => {
|
|
93
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty('# Title\n## Subtitle\n### H3'), true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('returns true for empty list items', () => {
|
|
97
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty('- \n* \n+ '), true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('returns true for empty checkboxes', () => {
|
|
101
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty('- [ ] \n- [x] '), true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('returns false for content with real text', () => {
|
|
105
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty('# Title\n- Do something useful'), false)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('returns false for plain text', () => {
|
|
109
|
+
assert.equal(mod.isHeartbeatContentEffectivelyEmpty('Run the backup job'), false)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// ── buildIdentityContext ────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
describe('buildIdentityContext', () => {
|
|
116
|
+
it('returns empty string when no identity fields', () => {
|
|
117
|
+
assert.equal(mod.buildIdentityContext({}, {}), '')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('builds context from agent fields', () => {
|
|
121
|
+
const result = mod.buildIdentityContext(null, { name: 'Bot', emoji: '🤖' })
|
|
122
|
+
assert.ok(result.includes('## Your Identity'))
|
|
123
|
+
assert.ok(result.includes('Name: Bot'))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('prefers file identity fields over agent fields', () => {
|
|
127
|
+
// Without an actual IDENTITY.md on disk, file fields will be empty,
|
|
128
|
+
// so agent fields should be used as fallback
|
|
129
|
+
const result = mod.buildIdentityContext({ cwd: workspaceDir }, { name: 'Agent', vibe: 'chill' })
|
|
130
|
+
assert.ok(result.includes('Name: Agent'))
|
|
131
|
+
assert.ok(result.includes('Vibe: chill'))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('reads IDENTITY.md from session cwd when present', () => {
|
|
135
|
+
const identityPath = path.join(workspaceDir, 'IDENTITY.md')
|
|
136
|
+
fs.writeFileSync(identityPath, '- **Name**: FileBot\n- **Emoji**: 🐛\n')
|
|
137
|
+
try {
|
|
138
|
+
const result = mod.buildIdentityContext({ cwd: workspaceDir }, { name: 'Agent' })
|
|
139
|
+
assert.ok(result.includes('Name: FileBot'))
|
|
140
|
+
} finally {
|
|
141
|
+
fs.unlinkSync(identityPath)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// ── readHeartbeatFile ───────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
describe('readHeartbeatFile', () => {
|
|
149
|
+
it('returns empty string when no HEARTBEAT.md', () => {
|
|
150
|
+
assert.equal(mod.readHeartbeatFile({ cwd: workspaceDir }), '')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('reads HEARTBEAT.md from session cwd', () => {
|
|
154
|
+
const hbPath = path.join(workspaceDir, 'HEARTBEAT.md')
|
|
155
|
+
fs.writeFileSync(hbPath, '# Tasks\n- Check logs')
|
|
156
|
+
try {
|
|
157
|
+
const result = mod.readHeartbeatFile({ cwd: workspaceDir })
|
|
158
|
+
assert.ok(result.includes('Check logs'))
|
|
159
|
+
} finally {
|
|
160
|
+
fs.unlinkSync(hbPath)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// ── heartbeatConfigForSession ───────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe('heartbeatConfigForSession', () => {
|
|
168
|
+
it('uses global defaults when no overrides', () => {
|
|
169
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
170
|
+
{ id: 's1' },
|
|
171
|
+
{ heartbeatIntervalSec: 60 },
|
|
172
|
+
{},
|
|
173
|
+
)
|
|
174
|
+
assert.equal(cfg.intervalSec, 60)
|
|
175
|
+
assert.equal(cfg.enabled, true)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('disables when interval is 0', () => {
|
|
179
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
180
|
+
{ id: 's1' },
|
|
181
|
+
{ heartbeatIntervalSec: 0 },
|
|
182
|
+
{},
|
|
183
|
+
)
|
|
184
|
+
assert.equal(cfg.enabled, false)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('agent layer overrides global settings', () => {
|
|
188
|
+
const agents: Record<string, Record<string, unknown>> = {
|
|
189
|
+
'a1': { heartbeatIntervalSec: 120, heartbeatEnabled: true, heartbeatPrompt: 'Custom agent prompt' },
|
|
190
|
+
}
|
|
191
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
192
|
+
{ id: 's1', agentId: 'a1' },
|
|
193
|
+
{ heartbeatIntervalSec: 60 },
|
|
194
|
+
agents,
|
|
195
|
+
)
|
|
196
|
+
assert.equal(cfg.intervalSec, 120)
|
|
197
|
+
assert.equal(cfg.prompt, 'Custom agent prompt')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('agent can disable heartbeat', () => {
|
|
201
|
+
const agents: Record<string, Record<string, unknown>> = {
|
|
202
|
+
'a1': { heartbeatEnabled: false },
|
|
203
|
+
}
|
|
204
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
205
|
+
{ id: 's1', agentId: 'a1' },
|
|
206
|
+
{ heartbeatIntervalSec: 60 },
|
|
207
|
+
agents,
|
|
208
|
+
)
|
|
209
|
+
assert.equal(cfg.enabled, false)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('session layer overrides agent settings', () => {
|
|
213
|
+
const agents: Record<string, Record<string, unknown>> = {
|
|
214
|
+
'a1': { heartbeatEnabled: true },
|
|
215
|
+
}
|
|
216
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
217
|
+
{ id: 's1', agentId: 'a1', heartbeatEnabled: false },
|
|
218
|
+
{ heartbeatIntervalSec: 60 },
|
|
219
|
+
agents,
|
|
220
|
+
)
|
|
221
|
+
assert.equal(cfg.enabled, false)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('session interval overrides agent interval', () => {
|
|
225
|
+
const agents: Record<string, Record<string, unknown>> = {
|
|
226
|
+
'a1': { heartbeatIntervalSec: 120 },
|
|
227
|
+
}
|
|
228
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
229
|
+
{ id: 's1', agentId: 'a1', heartbeatIntervalSec: 300 },
|
|
230
|
+
{ heartbeatIntervalSec: 60 },
|
|
231
|
+
agents,
|
|
232
|
+
)
|
|
233
|
+
assert.equal(cfg.intervalSec, 300)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('supports duration string format for interval', () => {
|
|
237
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
238
|
+
{ id: 's1' },
|
|
239
|
+
{ heartbeatInterval: '1h30m' },
|
|
240
|
+
{},
|
|
241
|
+
)
|
|
242
|
+
assert.equal(cfg.intervalSec, 5400) // 1h30m = 5400s
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('resolves model from settings and agent layers', () => {
|
|
246
|
+
const agents: Record<string, Record<string, unknown>> = {
|
|
247
|
+
'a1': { heartbeatModel: 'gpt-4' },
|
|
248
|
+
}
|
|
249
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
250
|
+
{ id: 's1', agentId: 'a1' },
|
|
251
|
+
{ heartbeatIntervalSec: 60, heartbeatModel: 'gpt-3.5' },
|
|
252
|
+
agents,
|
|
253
|
+
)
|
|
254
|
+
assert.equal(cfg.model, 'gpt-4')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('returns showOk and showAlerts defaults', () => {
|
|
258
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
259
|
+
{ id: 's1' },
|
|
260
|
+
{ heartbeatIntervalSec: 60 },
|
|
261
|
+
{},
|
|
262
|
+
)
|
|
263
|
+
assert.equal(cfg.showOk, false)
|
|
264
|
+
assert.equal(cfg.showAlerts, true)
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// ── start/stop/status service lifecycle ─────────────────────────────────
|
|
269
|
+
|
|
270
|
+
describe('heartbeat service lifecycle', () => {
|
|
271
|
+
it('reports not running initially', () => {
|
|
272
|
+
// Stop in case a previous test left it running
|
|
273
|
+
mod.stopHeartbeatService()
|
|
274
|
+
const status = mod.getHeartbeatServiceStatus()
|
|
275
|
+
assert.equal(status.running, false)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('start sets running to true', () => {
|
|
279
|
+
mod.startHeartbeatService()
|
|
280
|
+
try {
|
|
281
|
+
const status = mod.getHeartbeatServiceStatus()
|
|
282
|
+
assert.equal(status.running, true)
|
|
283
|
+
} finally {
|
|
284
|
+
mod.stopHeartbeatService()
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('stop sets running to false', () => {
|
|
289
|
+
mod.startHeartbeatService()
|
|
290
|
+
mod.stopHeartbeatService()
|
|
291
|
+
const status = mod.getHeartbeatServiceStatus()
|
|
292
|
+
assert.equal(status.running, false)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('restart clears tracked sessions and restarts', () => {
|
|
296
|
+
mod.startHeartbeatService()
|
|
297
|
+
mod.restartHeartbeatService()
|
|
298
|
+
try {
|
|
299
|
+
const status = mod.getHeartbeatServiceStatus()
|
|
300
|
+
assert.equal(status.running, true)
|
|
301
|
+
assert.equal(status.trackedSessions, 0)
|
|
302
|
+
} finally {
|
|
303
|
+
mod.stopHeartbeatService()
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('double start replaces the timer (no duplicate intervals)', () => {
|
|
308
|
+
mod.startHeartbeatService()
|
|
309
|
+
mod.startHeartbeatService()
|
|
310
|
+
try {
|
|
311
|
+
const status = mod.getHeartbeatServiceStatus()
|
|
312
|
+
assert.equal(status.running, true)
|
|
313
|
+
} finally {
|
|
314
|
+
mod.stopHeartbeatService()
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// ── buildAgentHeartbeatPrompt ───────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe('buildAgentHeartbeatPrompt', () => {
|
|
322
|
+
it('returns fallback prompt when agent is null', () => {
|
|
323
|
+
const result = mod.buildAgentHeartbeatPrompt({ id: 's1' }, null, 'fallback', '')
|
|
324
|
+
assert.equal(result, 'fallback')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('includes AGENT_HEARTBEAT_TICK header', () => {
|
|
328
|
+
const result = mod.buildAgentHeartbeatPrompt(
|
|
329
|
+
{ id: 's1', messages: [] },
|
|
330
|
+
{ name: 'Bot' },
|
|
331
|
+
'Check status',
|
|
332
|
+
'',
|
|
333
|
+
)
|
|
334
|
+
assert.ok(result.includes('AGENT_HEARTBEAT_TICK'))
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('includes heartbeat file content when provided', () => {
|
|
338
|
+
const result = mod.buildAgentHeartbeatPrompt(
|
|
339
|
+
{ id: 's1', messages: [] },
|
|
340
|
+
{ name: 'Bot' },
|
|
341
|
+
'Check status',
|
|
342
|
+
'# Tasks\n- Do the thing',
|
|
343
|
+
)
|
|
344
|
+
assert.ok(result.includes('HEARTBEAT.md contents'))
|
|
345
|
+
assert.ok(result.includes('Do the thing'))
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('excludes HEARTBEAT.md section when content is effectively empty', () => {
|
|
349
|
+
const result = mod.buildAgentHeartbeatPrompt(
|
|
350
|
+
{ id: 's1', messages: [] },
|
|
351
|
+
{ name: 'Bot' },
|
|
352
|
+
'Check status',
|
|
353
|
+
'# Title\n- [ ] ',
|
|
354
|
+
)
|
|
355
|
+
assert.ok(!result.includes('HEARTBEAT.md contents'))
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('includes dynamic goal when set on agent', () => {
|
|
359
|
+
const result = mod.buildAgentHeartbeatPrompt(
|
|
360
|
+
{ id: 's1', messages: [] },
|
|
361
|
+
{ name: 'Bot', heartbeatGoal: 'Monitor CI pipeline' },
|
|
362
|
+
'Check status',
|
|
363
|
+
'',
|
|
364
|
+
)
|
|
365
|
+
assert.ok(result.includes('Monitor CI pipeline'))
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('includes recent conversation context', () => {
|
|
369
|
+
const result = mod.buildAgentHeartbeatPrompt(
|
|
370
|
+
{
|
|
371
|
+
id: 's1',
|
|
372
|
+
messages: [
|
|
373
|
+
{ role: 'user', text: 'Check the logs', toolEvents: [] },
|
|
374
|
+
{ role: 'assistant', text: 'Logs look clean', toolEvents: [] },
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
{ name: 'Bot' },
|
|
378
|
+
'Check status',
|
|
379
|
+
'',
|
|
380
|
+
)
|
|
381
|
+
assert.ok(result.includes('[user]: Check the logs'))
|
|
382
|
+
assert.ok(result.includes('[assistant]: Logs look clean'))
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('includes agent soul in prompt', () => {
|
|
386
|
+
const result = mod.buildAgentHeartbeatPrompt(
|
|
387
|
+
{ id: 's1', messages: [] },
|
|
388
|
+
{ name: 'Bot', soul: 'You are a cheerful monitoring assistant' },
|
|
389
|
+
'Check status',
|
|
390
|
+
'',
|
|
391
|
+
)
|
|
392
|
+
assert.ok(result.includes('Persona: You are a cheerful'))
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('strips blocked items from heartbeat file content', () => {
|
|
396
|
+
const result = mod.buildAgentHeartbeatPrompt(
|
|
397
|
+
{ id: 's1', messages: [] },
|
|
398
|
+
{ name: 'Bot' },
|
|
399
|
+
'Check status',
|
|
400
|
+
'- Active task\n- Blocked task (blocked, waiting)\n- Another active task',
|
|
401
|
+
)
|
|
402
|
+
assert.ok(result.includes('Active task'))
|
|
403
|
+
assert.ok(!result.includes('Blocked task'))
|
|
404
|
+
assert.ok(result.includes('Another active task'))
|
|
405
|
+
})
|
|
406
|
+
})
|
|
@@ -15,6 +15,7 @@ import { buildIdentityContinuityContext } from './identity-continuity'
|
|
|
15
15
|
import { buildMainLoopHeartbeatPrompt, isMainSession } from './main-agent-loop'
|
|
16
16
|
import { ensureAgentThreadSession } from './agent-thread-session'
|
|
17
17
|
import { isAgentDisabled } from './agent-availability'
|
|
18
|
+
import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
|
|
18
19
|
|
|
19
20
|
const HEARTBEAT_TICK_MS = 60_000
|
|
20
21
|
const MAX_CONCURRENT_HEARTBEATS = 5
|
|
@@ -33,14 +34,12 @@ interface HeartbeatState {
|
|
|
33
34
|
failures: Map<string, FailureRecord>
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
-
const globalScope = globalThis as typeof globalThis & { [globalKey]?: HeartbeatState }
|
|
38
|
-
const state: HeartbeatState = globalScope[globalKey] ?? (globalScope[globalKey] = {
|
|
37
|
+
const state: HeartbeatState = hmrSingleton<HeartbeatState>('__swarmclaw_heartbeat_service__', () => ({
|
|
39
38
|
timer: null,
|
|
40
39
|
running: false,
|
|
41
40
|
lastBySession: new Map<string, number>(),
|
|
42
41
|
failures: new Map<string, FailureRecord>(),
|
|
43
|
-
})
|
|
42
|
+
}))
|
|
44
43
|
|
|
45
44
|
function parseIntBounded(value: unknown, fallback: number, min: number, max: number): number {
|
|
46
45
|
const parsed = typeof value === 'number'
|
|
@@ -194,6 +193,31 @@ export function buildIdentityContext(session: Record<string, unknown> | undefine
|
|
|
194
193
|
return `## Your Identity\n${lines.join('\n')}`
|
|
195
194
|
}
|
|
196
195
|
|
|
196
|
+
// ── Blocked-item suppression ────────────────────────────────────────────
|
|
197
|
+
// Ported from OpenClaw's duplicate-suppression pattern: instead of letting
|
|
198
|
+
// the LLM see blocked tasks every tick (and parrot "still blocked"), we
|
|
199
|
+
// strip those lines before they ever reach the prompt. A line is
|
|
200
|
+
// considered blocked if it contains "(blocked" anywhere (case-insensitive),
|
|
201
|
+
// which covers "(blocked, no update)", "(blocked: awaiting …)", etc.
|
|
202
|
+
const BLOCKED_MARKER_RE = /\(blocked\b/i
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Remove blocked checklist items from HEARTBEAT.md content so the LLM
|
|
206
|
+
* doesn't keep surfacing them. Headers and non-list lines pass through
|
|
207
|
+
* unchanged.
|
|
208
|
+
*/
|
|
209
|
+
export function stripBlockedItems(content: string): string {
|
|
210
|
+
if (!content) return ''
|
|
211
|
+
const lines = content.split('\n')
|
|
212
|
+
const filtered = lines.filter((line) => {
|
|
213
|
+
const trimmed = line.trim()
|
|
214
|
+
// Only filter checklist / list items that are explicitly marked blocked
|
|
215
|
+
if (/^[-*+]\s/.test(trimmed) && BLOCKED_MARKER_RE.test(trimmed)) return false
|
|
216
|
+
return true
|
|
217
|
+
})
|
|
218
|
+
return filtered.join('\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
197
221
|
/** Detect HEARTBEAT.md files that contain only skeleton structure (headers, empty list items) but no real content. */
|
|
198
222
|
export function isHeartbeatContentEffectivelyEmpty(content: string | undefined | null): boolean {
|
|
199
223
|
if (!content || typeof content !== 'string') return true
|
|
@@ -236,8 +260,9 @@ export function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackProm
|
|
|
236
260
|
})
|
|
237
261
|
.join('\n')
|
|
238
262
|
|
|
239
|
-
//
|
|
240
|
-
const
|
|
263
|
+
// Strip blocked items, then check if anything meaningful remains
|
|
264
|
+
const strippedContent = stripBlockedItems(heartbeatFileContent)
|
|
265
|
+
const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(strippedContent) ? '' : strippedContent
|
|
241
266
|
|
|
242
267
|
return [
|
|
243
268
|
'AGENT_HEARTBEAT_TICK',
|
|
@@ -507,7 +532,7 @@ async function tickHeartbeats() {
|
|
|
507
532
|
count: (prev?.count ?? 0) + 1,
|
|
508
533
|
lastFailedAt: Date.now(),
|
|
509
534
|
})
|
|
510
|
-
const msg =
|
|
535
|
+
const msg = errorMessage(err)
|
|
511
536
|
log.warn('heartbeat', `Heartbeat run failed for session ${sid}`, msg)
|
|
512
537
|
})
|
|
513
538
|
}
|
|
@@ -577,3 +602,25 @@ export function getHeartbeatServiceStatus() {
|
|
|
577
602
|
trackedSessions: state.lastBySession.size,
|
|
578
603
|
}
|
|
579
604
|
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Remove tracking entries for sessions that no longer exist.
|
|
608
|
+
* Called periodically by the daemon health sweep.
|
|
609
|
+
*/
|
|
610
|
+
export function pruneHeartbeatState(liveSessionIds: Set<string>): number {
|
|
611
|
+
let removed = 0
|
|
612
|
+
for (const id of state.lastBySession.keys()) {
|
|
613
|
+
if (!liveSessionIds.has(id)) {
|
|
614
|
+
state.lastBySession.delete(id)
|
|
615
|
+
state.failures.delete(id)
|
|
616
|
+
removed++
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Also clean up orphaned failure entries
|
|
620
|
+
for (const id of state.failures.keys()) {
|
|
621
|
+
if (!liveSessionIds.has(id)) {
|
|
622
|
+
state.failures.delete(id)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return removed
|
|
626
|
+
}
|
|
@@ -4,6 +4,7 @@ import { afterEach, describe, it } from 'node:test'
|
|
|
4
4
|
import {
|
|
5
5
|
buildHeartbeatWakePrompt,
|
|
6
6
|
buildWakeTriggerContext,
|
|
7
|
+
deriveHeartbeatWakeDeliveryMode,
|
|
7
8
|
hasPendingHeartbeatWake,
|
|
8
9
|
mergeHeartbeatWakeRequest,
|
|
9
10
|
requestHeartbeatNow,
|
|
@@ -109,4 +110,22 @@ describe('heartbeat-wake helpers', () => {
|
|
|
109
110
|
['connector-message', 'watch_job'],
|
|
110
111
|
)
|
|
111
112
|
})
|
|
113
|
+
|
|
114
|
+
it('forces connector-triggered wakes into tool-only delivery mode', () => {
|
|
115
|
+
const connectorWake = mergeHeartbeatWakeRequest(undefined, {
|
|
116
|
+
sessionId: 'sess-3',
|
|
117
|
+
reason: 'connector-message',
|
|
118
|
+
source: 'connector:whatsapp',
|
|
119
|
+
requestedAt: 1,
|
|
120
|
+
})
|
|
121
|
+
const scheduleWake = mergeHeartbeatWakeRequest(undefined, {
|
|
122
|
+
sessionId: 'sess-4',
|
|
123
|
+
reason: 'schedule',
|
|
124
|
+
source: 'schedule:daily',
|
|
125
|
+
requestedAt: 1,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
assert.equal(deriveHeartbeatWakeDeliveryMode(connectorWake.events), 'tool_only')
|
|
129
|
+
assert.equal(deriveHeartbeatWakeDeliveryMode(scheduleWake.events), 'default')
|
|
130
|
+
})
|
|
112
131
|
})
|
|
@@ -16,6 +16,7 @@ import { loadSessions, loadAgents, loadSettings } from './storage'
|
|
|
16
16
|
import { enqueueSessionRun, getSessionExecutionState } from './session-run-manager'
|
|
17
17
|
import { log } from './logger'
|
|
18
18
|
import { isAgentDisabled } from './agent-availability'
|
|
19
|
+
import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
|
|
19
20
|
|
|
20
21
|
export interface WakeRequestInput {
|
|
21
22
|
eventId?: string
|
|
@@ -56,21 +57,12 @@ const MAX_RESUME_CHARS = 280
|
|
|
56
57
|
const MAX_DETAIL_CHARS = 800
|
|
57
58
|
type WakeTimerKind = 'normal' | 'retry'
|
|
58
59
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
timerKind: WakeTimerKind | null
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const state = globalScope[globalKey] ?? (globalScope[globalKey] = {
|
|
69
|
-
pending: new Map(),
|
|
70
|
-
timer: null,
|
|
71
|
-
timerDueAt: null,
|
|
72
|
-
timerKind: null,
|
|
73
|
-
})
|
|
60
|
+
const state = hmrSingleton('__swarmclaw_heartbeat_wake__', () => ({
|
|
61
|
+
pending: new Map<string, WakeRequest>(),
|
|
62
|
+
timer: null as ReturnType<typeof setTimeout> | null,
|
|
63
|
+
timerDueAt: null as number | null,
|
|
64
|
+
timerKind: null as WakeTimerKind | null,
|
|
65
|
+
}))
|
|
74
66
|
|
|
75
67
|
function trimText(value: unknown, maxChars: number): string | undefined {
|
|
76
68
|
if (typeof value !== 'string') return undefined
|
|
@@ -235,6 +227,14 @@ export function buildWakeTriggerContext(events: WakeEvent[], nowIso?: string): s
|
|
|
235
227
|
return lines.join('\n')
|
|
236
228
|
}
|
|
237
229
|
|
|
230
|
+
export function deriveHeartbeatWakeDeliveryMode(
|
|
231
|
+
events: WakeEvent[],
|
|
232
|
+
): 'default' | 'tool_only' {
|
|
233
|
+
return events.some((event) => event.reason.toLowerCase() === 'connector-message')
|
|
234
|
+
? 'tool_only'
|
|
235
|
+
: 'default'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
238
|
export function buildHeartbeatWakePrompt(input: {
|
|
239
239
|
wake: WakeRequest
|
|
240
240
|
basePrompt?: string
|
|
@@ -347,6 +347,7 @@ function flushWakes(): void {
|
|
|
347
347
|
showOk: cfg.showOk,
|
|
348
348
|
showAlerts: cfg.showAlerts,
|
|
349
349
|
target: cfg.target,
|
|
350
|
+
deliveryMode: deriveHeartbeatWakeDeliveryMode(wake.events),
|
|
350
351
|
},
|
|
351
352
|
})
|
|
352
353
|
|
|
@@ -360,7 +361,7 @@ function flushWakes(): void {
|
|
|
360
361
|
retryCount: wake.retryCount + 1,
|
|
361
362
|
})
|
|
362
363
|
delayedForRetry = true
|
|
363
|
-
log.warn('heartbeat-wake', `Wake failed: ${
|
|
364
|
+
log.warn('heartbeat-wake', `Wake failed: ${errorMessage(err)}`)
|
|
364
365
|
}
|
|
365
366
|
}
|
|
366
367
|
|