@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,137 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { before, describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
// provider-health uses globalThis to store state — we can import directly
|
|
5
|
+
// since the pure logic functions don't need DATA_DIR. But spawnSync is used
|
|
6
|
+
// by commandExists/delegateToolReady, so rankDelegatesByHealth will hit
|
|
7
|
+
// the real filesystem. We test pure state functions here.
|
|
8
|
+
|
|
9
|
+
let providerHealth: typeof import('./provider-health')
|
|
10
|
+
|
|
11
|
+
before(async () => {
|
|
12
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
13
|
+
providerHealth = await import('./provider-health')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('provider-health', () => {
|
|
17
|
+
// -------------------------------------------------------------------------
|
|
18
|
+
// markProviderFailure / markProviderSuccess / isProviderCoolingDown
|
|
19
|
+
// -------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
it('fresh provider is not cooling down', () => {
|
|
22
|
+
assert.equal(providerHealth.isProviderCoolingDown('fresh-provider-xyz'), false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('markProviderFailure puts provider into cooldown', () => {
|
|
26
|
+
providerHealth.markProviderFailure('test-fail-1', 'connection refused')
|
|
27
|
+
assert.equal(providerHealth.isProviderCoolingDown('test-fail-1'), true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('markProviderSuccess clears cooldown', () => {
|
|
31
|
+
providerHealth.markProviderFailure('test-recover-1', 'timeout')
|
|
32
|
+
assert.equal(providerHealth.isProviderCoolingDown('test-recover-1'), true)
|
|
33
|
+
|
|
34
|
+
providerHealth.markProviderSuccess('test-recover-1')
|
|
35
|
+
assert.equal(providerHealth.isProviderCoolingDown('test-recover-1'), false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('multiple failures increase cooldown (exponential backoff)', () => {
|
|
39
|
+
const id = 'test-backoff-1'
|
|
40
|
+
providerHealth.markProviderFailure(id, 'err')
|
|
41
|
+
const snap1 = providerHealth.getProviderHealthSnapshot()[id]
|
|
42
|
+
|
|
43
|
+
providerHealth.markProviderFailure(id, 'err')
|
|
44
|
+
const snap2 = providerHealth.getProviderHealthSnapshot()[id]
|
|
45
|
+
|
|
46
|
+
providerHealth.markProviderFailure(id, 'err')
|
|
47
|
+
const snap3 = providerHealth.getProviderHealthSnapshot()[id]
|
|
48
|
+
|
|
49
|
+
assert.equal(snap1.failures, 1)
|
|
50
|
+
assert.equal(snap2.failures, 2)
|
|
51
|
+
assert.equal(snap3.failures, 3)
|
|
52
|
+
|
|
53
|
+
// Cooldown should increase with more failures
|
|
54
|
+
const cooldown1 = (snap1.cooldownUntil ?? 0) - (snap1.lastFailureAt ?? 0)
|
|
55
|
+
const cooldown2 = (snap2.cooldownUntil ?? 0) - (snap2.lastFailureAt ?? 0)
|
|
56
|
+
const cooldown3 = (snap3.cooldownUntil ?? 0) - (snap3.lastFailureAt ?? 0)
|
|
57
|
+
assert.ok(cooldown2 > cooldown1, 'cooldown2 > cooldown1')
|
|
58
|
+
assert.ok(cooldown3 > cooldown2, 'cooldown3 > cooldown2')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('failure count is capped at 50', () => {
|
|
62
|
+
const id = 'test-cap-1'
|
|
63
|
+
for (let i = 0; i < 60; i++) {
|
|
64
|
+
providerHealth.markProviderFailure(id, `err-${i}`)
|
|
65
|
+
}
|
|
66
|
+
const snap = providerHealth.getProviderHealthSnapshot()[id]
|
|
67
|
+
assert.equal(snap.failures, 50)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('error message is truncated to 500 chars', () => {
|
|
71
|
+
const id = 'test-trunc-1'
|
|
72
|
+
const longError = 'x'.repeat(1000)
|
|
73
|
+
providerHealth.markProviderFailure(id, longError)
|
|
74
|
+
const snap = providerHealth.getProviderHealthSnapshot()[id]
|
|
75
|
+
assert.equal(snap.lastError?.length, 500)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('success resets failure count to 0', () => {
|
|
79
|
+
const id = 'test-reset-1'
|
|
80
|
+
providerHealth.markProviderFailure(id, 'err')
|
|
81
|
+
providerHealth.markProviderFailure(id, 'err')
|
|
82
|
+
providerHealth.markProviderFailure(id, 'err')
|
|
83
|
+
providerHealth.markProviderSuccess(id)
|
|
84
|
+
const snap = providerHealth.getProviderHealthSnapshot()[id]
|
|
85
|
+
assert.equal(snap.failures, 0)
|
|
86
|
+
assert.equal(snap.cooldownUntil, undefined)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('success preserves lastError and lastFailureAt from previous failures', () => {
|
|
90
|
+
const id = 'test-preserve-1'
|
|
91
|
+
providerHealth.markProviderFailure(id, 'original error')
|
|
92
|
+
const afterFail = providerHealth.getProviderHealthSnapshot()[id]
|
|
93
|
+
providerHealth.markProviderSuccess(id)
|
|
94
|
+
const afterSuccess = providerHealth.getProviderHealthSnapshot()[id]
|
|
95
|
+
|
|
96
|
+
assert.equal(afterSuccess.lastError, 'original error')
|
|
97
|
+
assert.equal(afterSuccess.lastFailureAt, afterFail.lastFailureAt)
|
|
98
|
+
assert.ok(afterSuccess.lastSuccessAt! > 0)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// -------------------------------------------------------------------------
|
|
102
|
+
// getProviderHealthSnapshot
|
|
103
|
+
// -------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
it('snapshot includes coolingDown boolean', () => {
|
|
106
|
+
const id = 'test-snapshot-cool'
|
|
107
|
+
providerHealth.markProviderFailure(id, 'err')
|
|
108
|
+
const snap = providerHealth.getProviderHealthSnapshot()
|
|
109
|
+
assert.equal(snap[id].coolingDown, true)
|
|
110
|
+
|
|
111
|
+
providerHealth.markProviderSuccess(id)
|
|
112
|
+
const snap2 = providerHealth.getProviderHealthSnapshot()
|
|
113
|
+
assert.equal(snap2[id].coolingDown, false)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
// OPENAI_COMPATIBLE_DEFAULTS
|
|
118
|
+
// -------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
it('OPENAI_COMPATIBLE_DEFAULTS has expected providers', () => {
|
|
121
|
+
const defaults = providerHealth.OPENAI_COMPATIBLE_DEFAULTS
|
|
122
|
+
assert.ok(defaults.openai)
|
|
123
|
+
assert.ok(defaults.google)
|
|
124
|
+
assert.ok(defaults.deepseek)
|
|
125
|
+
assert.ok(defaults.groq)
|
|
126
|
+
assert.ok(defaults.together)
|
|
127
|
+
assert.ok(defaults.mistral)
|
|
128
|
+
assert.ok(defaults.xai)
|
|
129
|
+
assert.ok(defaults.fireworks)
|
|
130
|
+
|
|
131
|
+
// Each entry has name and defaultEndpoint
|
|
132
|
+
for (const [, val] of Object.entries(defaults)) {
|
|
133
|
+
assert.ok(typeof val.name === 'string' && val.name.length > 0)
|
|
134
|
+
assert.ok(typeof val.defaultEndpoint === 'string' && val.defaultEndpoint.startsWith('https://'))
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'child_process'
|
|
2
|
+
import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
|
|
2
3
|
|
|
3
4
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
4
5
|
|
|
@@ -10,9 +11,8 @@ interface ProviderHealthState {
|
|
|
10
11
|
cooldownUntil?: number
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const gk = '__swarmclaw_provider_health__' as const
|
|
14
14
|
const states: Map<string, ProviderHealthState> =
|
|
15
|
-
(
|
|
15
|
+
hmrSingleton('__swarmclaw_provider_health__', () => new Map<string, ProviderHealthState>())
|
|
16
16
|
|
|
17
17
|
const cliCheckCache = new Map<string, { at: number; ok: boolean }>()
|
|
18
18
|
const delegateReadyCache = new Map<string, { at: number; ok: boolean }>()
|
|
@@ -264,7 +264,7 @@ export async function pingProvider(
|
|
|
264
264
|
} catch (err: unknown) {
|
|
265
265
|
const msg = err instanceof Error && err.name === 'TimeoutError'
|
|
266
266
|
? 'Connection timed out.'
|
|
267
|
-
: (err
|
|
267
|
+
: errorMessage(err)
|
|
268
268
|
return { ok: false, message: msg }
|
|
269
269
|
}
|
|
270
270
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'crypto'
|
|
2
|
+
import { hmrSingleton } from '@/lib/shared-utils'
|
|
2
3
|
import { getProviderList } from '@/lib/providers'
|
|
3
4
|
import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
4
5
|
import { decryptKey, loadCredentials } from '@/lib/server/storage'
|
|
@@ -33,20 +34,10 @@ const CLOUD_CACHE_TTL_MS = 15 * 60_000
|
|
|
33
34
|
const LOCAL_CACHE_TTL_MS = 60_000
|
|
34
35
|
const ERROR_CACHE_TTL_MS = 30_000
|
|
35
36
|
const DISCOVERY_TIMEOUT_MS = 10_000
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
type DiscoveryGlobals = typeof globalThis & {
|
|
39
|
-
[gk]?: {
|
|
40
|
-
cache: Map<string, DiscoveryCacheEntry>
|
|
41
|
-
pending: Map<string, Promise<ProviderModelDiscoveryResult>>
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const discoveryGlobals = globalThis as DiscoveryGlobals
|
|
46
|
-
const discoveryState = discoveryGlobals[gk] ?? (discoveryGlobals[gk] = {
|
|
37
|
+
const discoveryState = hmrSingleton('__swarmclaw_provider_model_discovery__', () => ({
|
|
47
38
|
cache: new Map<string, DiscoveryCacheEntry>(),
|
|
48
39
|
pending: new Map<string, Promise<ProviderModelDiscoveryResult>>(),
|
|
49
|
-
})
|
|
40
|
+
}))
|
|
50
41
|
|
|
51
42
|
function clean(value: string | null | undefined): string {
|
|
52
43
|
return typeof value === 'string' ? value.trim() : ''
|
|
@@ -82,7 +82,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
|
82
82
|
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
83
83
|
task,
|
|
84
84
|
sessions: sessions as SessionFixtureMap,
|
|
85
|
-
connectors,
|
|
85
|
+
connectors: connectors as any,
|
|
86
86
|
running,
|
|
87
87
|
})
|
|
88
88
|
|
|
@@ -129,7 +129,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
|
129
129
|
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
130
130
|
task,
|
|
131
131
|
sessions: sessions as SessionFixtureMap,
|
|
132
|
-
connectors,
|
|
132
|
+
connectors: connectors as any,
|
|
133
133
|
running,
|
|
134
134
|
})
|
|
135
135
|
|
|
@@ -177,7 +177,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
|
177
177
|
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
178
178
|
task,
|
|
179
179
|
sessions: sessions as SessionFixtureMap,
|
|
180
|
-
connectors,
|
|
180
|
+
connectors: connectors as any,
|
|
181
181
|
running,
|
|
182
182
|
})
|
|
183
183
|
|
|
@@ -226,7 +226,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
|
226
226
|
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
227
227
|
task,
|
|
228
228
|
sessions: sessions as SessionFixtureMap,
|
|
229
|
-
connectors,
|
|
229
|
+
connectors: connectors as any,
|
|
230
230
|
running,
|
|
231
231
|
})
|
|
232
232
|
|
|
@@ -280,7 +280,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
|
280
280
|
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
281
281
|
task,
|
|
282
282
|
sessions: sessions as SessionFixtureMap,
|
|
283
|
-
connectors,
|
|
283
|
+
connectors: connectors as any,
|
|
284
284
|
running,
|
|
285
285
|
})
|
|
286
286
|
|
|
@@ -341,7 +341,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
|
341
341
|
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
342
342
|
task,
|
|
343
343
|
sessions: sessions as SessionFixtureMap,
|
|
344
|
-
connectors,
|
|
344
|
+
connectors: connectors as any,
|
|
345
345
|
running,
|
|
346
346
|
})
|
|
347
347
|
|
|
@@ -391,7 +391,7 @@ describe('collectTaskConnectorFollowupTargets', () => {
|
|
|
391
391
|
const targets = collectTaskConnectorFollowupTargets({
|
|
392
392
|
task,
|
|
393
393
|
sessions: sessions as SessionFixtureMap,
|
|
394
|
-
connectors,
|
|
394
|
+
connectors: connectors as any,
|
|
395
395
|
running,
|
|
396
396
|
})
|
|
397
397
|
|
|
@@ -434,7 +434,7 @@ describe('collectTaskConnectorFollowupTargets', () => {
|
|
|
434
434
|
const targets = collectTaskConnectorFollowupTargets({
|
|
435
435
|
task,
|
|
436
436
|
sessions: sessions as SessionFixtureMap,
|
|
437
|
-
connectors,
|
|
437
|
+
connectors: connectors as any,
|
|
438
438
|
running,
|
|
439
439
|
})
|
|
440
440
|
|
|
@@ -478,7 +478,7 @@ describe('collectTaskConnectorFollowupTargets', () => {
|
|
|
478
478
|
const targets = collectTaskConnectorFollowupTargets({
|
|
479
479
|
task,
|
|
480
480
|
sessions: sessions as SessionFixtureMap,
|
|
481
|
-
connectors,
|
|
481
|
+
connectors: connectors as any,
|
|
482
482
|
running,
|
|
483
483
|
})
|
|
484
484
|
|
|
@@ -36,8 +36,8 @@ function runWithTempDataDir(script: string) {
|
|
|
36
36
|
describe('reconcileFinishedRunningTasks', () => {
|
|
37
37
|
it('finalizes a completed one-off scheduled task from its finished session and deletes the schedule', () => {
|
|
38
38
|
const output = runWithTempDataDir(`
|
|
39
|
-
const storageMod = await import('./src/lib/server/storage
|
|
40
|
-
const queueMod = await import('./src/lib/server/queue
|
|
39
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
40
|
+
const queueMod = await import('./src/lib/server/queue')
|
|
41
41
|
const storage = storageMod.default || storageMod
|
|
42
42
|
const queue = queueMod.default || queueMod
|
|
43
43
|
|
|
@@ -0,0 +1,269 @@
|
|
|
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 { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-queue-recovery-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
SWARMCLAW_BUILD_MODE: '1',
|
|
20
|
+
},
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
timeout: 15000,
|
|
23
|
+
})
|
|
24
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
25
|
+
const lines = (result.stdout || '')
|
|
26
|
+
.trim()
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((line) => line.trim())
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
31
|
+
return JSON.parse(jsonLine || '{}') as Record<string, any>
|
|
32
|
+
} finally {
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('queue recovery', () => {
|
|
38
|
+
it('processNext recovers orphaned queued tasks and defers them when the agent is disabled', () => {
|
|
39
|
+
const output = runWithTempDataDir(`
|
|
40
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
41
|
+
const queueMod = await import('./src/lib/server/queue')
|
|
42
|
+
const storage = storageMod.default || storageMod
|
|
43
|
+
const queue = queueMod.default || queueMod
|
|
44
|
+
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
storage.saveAgents({
|
|
47
|
+
'agent-disabled': {
|
|
48
|
+
id: 'agent-disabled',
|
|
49
|
+
name: 'Disabled Agent',
|
|
50
|
+
provider: 'openai',
|
|
51
|
+
model: 'gpt-test',
|
|
52
|
+
disabled: true,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
storage.saveTasks({
|
|
58
|
+
orphaned: {
|
|
59
|
+
id: 'orphaned',
|
|
60
|
+
title: 'Recover me',
|
|
61
|
+
description: 'Queued task missing from the queue array',
|
|
62
|
+
status: 'queued',
|
|
63
|
+
agentId: 'agent-disabled',
|
|
64
|
+
createdAt: now - 5_000,
|
|
65
|
+
updatedAt: now - 5_000,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
storage.saveQueue([])
|
|
69
|
+
|
|
70
|
+
await queue.processNext()
|
|
71
|
+
|
|
72
|
+
const task = storage.loadTasks().orphaned
|
|
73
|
+
const queueItems = storage.loadQueue()
|
|
74
|
+
console.log(JSON.stringify({
|
|
75
|
+
status: task?.status ?? null,
|
|
76
|
+
queued: queueItems,
|
|
77
|
+
retryDelayMs: typeof task?.retryScheduledAt === 'number' ? task.retryScheduledAt - now : null,
|
|
78
|
+
error: task?.error ?? null,
|
|
79
|
+
}))
|
|
80
|
+
`)
|
|
81
|
+
|
|
82
|
+
assert.equal(output.status, 'queued')
|
|
83
|
+
assert.deepEqual(output.queued, ['orphaned'])
|
|
84
|
+
assert.equal(typeof output.retryDelayMs, 'number')
|
|
85
|
+
assert.ok(output.retryDelayMs >= 55_000 && output.retryDelayMs <= 65_000)
|
|
86
|
+
assert.match(output.error, /disabled/i)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('recoverStalledRunningTasks requeues tasks missing startedAt and records the recovery', () => {
|
|
90
|
+
const output = runWithTempDataDir(`
|
|
91
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
92
|
+
const queueMod = await import('./src/lib/server/queue')
|
|
93
|
+
const storage = storageMod.default || storageMod
|
|
94
|
+
const queue = queueMod.default || queueMod
|
|
95
|
+
|
|
96
|
+
const now = Date.now()
|
|
97
|
+
storage.saveTasks({
|
|
98
|
+
broken: {
|
|
99
|
+
id: 'broken',
|
|
100
|
+
title: 'Broken running task',
|
|
101
|
+
description: 'Missing startedAt should be recovered',
|
|
102
|
+
status: 'running',
|
|
103
|
+
agentId: 'agent-a',
|
|
104
|
+
createdAt: now - 20_000,
|
|
105
|
+
updatedAt: now - 15_000,
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
storage.saveQueue([])
|
|
109
|
+
|
|
110
|
+
const originalSetTimeout = globalThis.setTimeout
|
|
111
|
+
const scheduled = []
|
|
112
|
+
globalThis.setTimeout = (fn, delay, ...args) => {
|
|
113
|
+
scheduled.push(delay)
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const result = queue.recoverStalledRunningTasks()
|
|
118
|
+
const task = storage.loadTasks().broken
|
|
119
|
+
console.log(JSON.stringify({
|
|
120
|
+
result,
|
|
121
|
+
status: task?.status ?? null,
|
|
122
|
+
queued: storage.loadQueue(),
|
|
123
|
+
retryDelayMs: typeof task?.retryScheduledAt === 'number' ? task.retryScheduledAt - now : null,
|
|
124
|
+
error: task?.error ?? null,
|
|
125
|
+
comment: task?.comments?.at(-1)?.text ?? null,
|
|
126
|
+
scheduledCalls: scheduled.length,
|
|
127
|
+
}))
|
|
128
|
+
} finally {
|
|
129
|
+
globalThis.setTimeout = originalSetTimeout
|
|
130
|
+
}
|
|
131
|
+
`)
|
|
132
|
+
|
|
133
|
+
assert.equal(output.result.recovered, 1)
|
|
134
|
+
assert.equal(output.result.deadLettered, 0)
|
|
135
|
+
assert.equal(output.status, 'queued')
|
|
136
|
+
assert.deepEqual(output.queued, ['broken'])
|
|
137
|
+
assert.equal(typeof output.retryDelayMs, 'number')
|
|
138
|
+
assert.ok(output.retryDelayMs >= 25_000 && output.retryDelayMs <= 35_000)
|
|
139
|
+
assert.match(output.error, /missing startedAt/i)
|
|
140
|
+
assert.match(output.comment, /missing startedAt/i)
|
|
141
|
+
assert.equal(output.scheduledCalls, 1)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('recoverStalledRunningTasks preserves retry policy backoff for stalled tasks', () => {
|
|
145
|
+
const output = runWithTempDataDir(`
|
|
146
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
147
|
+
const queueMod = await import('./src/lib/server/queue')
|
|
148
|
+
const storage = storageMod.default || storageMod
|
|
149
|
+
const queue = queueMod.default || queueMod
|
|
150
|
+
|
|
151
|
+
const now = Date.now()
|
|
152
|
+
storage.saveSettings({
|
|
153
|
+
...storage.loadSettings(),
|
|
154
|
+
taskStallTimeoutMin: 5,
|
|
155
|
+
taskRetryBackoffSec: 90,
|
|
156
|
+
})
|
|
157
|
+
storage.saveSessions({
|
|
158
|
+
'sess-stalled': {
|
|
159
|
+
id: 'sess-stalled',
|
|
160
|
+
agentId: 'agent-a',
|
|
161
|
+
messages: [],
|
|
162
|
+
createdAt: now - 100_000,
|
|
163
|
+
lastActiveAt: now - 5_000,
|
|
164
|
+
heartbeatEnabled: true,
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
storage.saveTasks({
|
|
168
|
+
stalled: {
|
|
169
|
+
id: 'stalled',
|
|
170
|
+
title: 'Stalled task',
|
|
171
|
+
description: 'Should use configured backoff when recovered',
|
|
172
|
+
status: 'running',
|
|
173
|
+
agentId: 'agent-a',
|
|
174
|
+
sessionId: 'sess-stalled',
|
|
175
|
+
createdAt: now - 200_000,
|
|
176
|
+
updatedAt: now - 420_000,
|
|
177
|
+
startedAt: now - 420_000,
|
|
178
|
+
maxAttempts: 3,
|
|
179
|
+
attempts: 0,
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
storage.saveQueue([])
|
|
183
|
+
|
|
184
|
+
const originalSetTimeout = globalThis.setTimeout
|
|
185
|
+
const scheduled = []
|
|
186
|
+
globalThis.setTimeout = (fn, delay, ...args) => {
|
|
187
|
+
scheduled.push(delay)
|
|
188
|
+
return 0
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const result = queue.recoverStalledRunningTasks()
|
|
192
|
+
const task = storage.loadTasks().stalled
|
|
193
|
+
const session = storage.loadSessions()['sess-stalled']
|
|
194
|
+
console.log(JSON.stringify({
|
|
195
|
+
result,
|
|
196
|
+
status: task?.status ?? null,
|
|
197
|
+
attempts: task?.attempts ?? null,
|
|
198
|
+
queued: storage.loadQueue(),
|
|
199
|
+
retryDelayMs: typeof task?.retryScheduledAt === 'number' ? task.retryScheduledAt - now : null,
|
|
200
|
+
error: task?.error ?? null,
|
|
201
|
+
heartbeatEnabled: session?.heartbeatEnabled ?? null,
|
|
202
|
+
scheduledCalls: scheduled.length,
|
|
203
|
+
}))
|
|
204
|
+
} finally {
|
|
205
|
+
globalThis.setTimeout = originalSetTimeout
|
|
206
|
+
}
|
|
207
|
+
`)
|
|
208
|
+
|
|
209
|
+
assert.equal(output.result.recovered, 1)
|
|
210
|
+
assert.equal(output.result.deadLettered, 0)
|
|
211
|
+
assert.equal(output.status, 'queued')
|
|
212
|
+
assert.equal(output.attempts, 1)
|
|
213
|
+
assert.deepEqual(output.queued, ['stalled'])
|
|
214
|
+
assert.equal(typeof output.retryDelayMs, 'number')
|
|
215
|
+
assert.ok(output.retryDelayMs >= 85_000 && output.retryDelayMs <= 95_000)
|
|
216
|
+
assert.match(output.error, /Retry scheduled after failure/i)
|
|
217
|
+
assert.equal(output.heartbeatEnabled, false)
|
|
218
|
+
assert.equal(output.scheduledCalls, 1)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('resumeQueue restores blocked queued tasks without clobbering their queuedAt timestamp', () => {
|
|
222
|
+
const output = runWithTempDataDir(`
|
|
223
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
224
|
+
const queueMod = await import('./src/lib/server/queue')
|
|
225
|
+
const storage = storageMod.default || storageMod
|
|
226
|
+
const queue = queueMod.default || queueMod
|
|
227
|
+
|
|
228
|
+
const originalQueuedAt = Date.now() - 45_000
|
|
229
|
+
storage.saveTasks({
|
|
230
|
+
dep: {
|
|
231
|
+
id: 'dep',
|
|
232
|
+
title: 'Dependency',
|
|
233
|
+
description: 'Still running',
|
|
234
|
+
status: 'running',
|
|
235
|
+
agentId: 'agent-a',
|
|
236
|
+
createdAt: originalQueuedAt - 10_000,
|
|
237
|
+
updatedAt: originalQueuedAt - 10_000,
|
|
238
|
+
startedAt: originalQueuedAt - 10_000,
|
|
239
|
+
},
|
|
240
|
+
blocked: {
|
|
241
|
+
id: 'blocked',
|
|
242
|
+
title: 'Blocked task',
|
|
243
|
+
description: 'Should be re-added to the queue on boot',
|
|
244
|
+
status: 'queued',
|
|
245
|
+
agentId: 'agent-a',
|
|
246
|
+
blockedBy: ['dep'],
|
|
247
|
+
queuedAt: originalQueuedAt,
|
|
248
|
+
createdAt: originalQueuedAt - 20_000,
|
|
249
|
+
updatedAt: originalQueuedAt - 5_000,
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
storage.saveQueue([])
|
|
253
|
+
|
|
254
|
+
queue.resumeQueue()
|
|
255
|
+
|
|
256
|
+
const task = storage.loadTasks().blocked
|
|
257
|
+
console.log(JSON.stringify({
|
|
258
|
+
queued: storage.loadQueue(),
|
|
259
|
+
queuedAt: task?.queuedAt ?? null,
|
|
260
|
+
status: task?.status ?? null,
|
|
261
|
+
}))
|
|
262
|
+
`)
|
|
263
|
+
|
|
264
|
+
assert.deepEqual(output.queued, ['blocked'])
|
|
265
|
+
assert.equal(output.status, 'queued')
|
|
266
|
+
assert.equal(typeof output.queuedAt, 'number')
|
|
267
|
+
assert.ok(output.queuedAt < Date.now() - 30_000)
|
|
268
|
+
})
|
|
269
|
+
})
|