@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,640 @@
|
|
|
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, afterEach, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
// Suppress unhandled rejections from background drainExecution() calls
|
|
8
|
+
// that fail because executeSessionChatTurn has no real LLM provider.
|
|
9
|
+
const _suppressedErrors: unknown[] = []
|
|
10
|
+
function suppressionHandler(err: unknown) { _suppressedErrors.push(err) }
|
|
11
|
+
process.on('unhandledRejection', suppressionHandler)
|
|
12
|
+
|
|
13
|
+
const originalEnv = {
|
|
14
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
15
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
16
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let tempDir = ''
|
|
20
|
+
let mgr: typeof import('./session-run-manager')
|
|
21
|
+
let storage: typeof import('./storage')
|
|
22
|
+
|
|
23
|
+
const globalKey = '__swarmclaw_session_run_manager__' as const
|
|
24
|
+
type RuntimeState = {
|
|
25
|
+
runningByExecution: Map<string, unknown>
|
|
26
|
+
queueByExecution: Map<string, unknown[]>
|
|
27
|
+
runs: Map<string, unknown>
|
|
28
|
+
recentRunIds: string[]
|
|
29
|
+
promises: Map<string, unknown>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Pending promises from fire-and-forget drain calls. We suppress their
|
|
33
|
+
* rejections and await them in afterEach so node:test doesn't see
|
|
34
|
+
* "asynchronous activity after the test ended" warnings. */
|
|
35
|
+
const pendingPromises: Promise<unknown>[] = []
|
|
36
|
+
|
|
37
|
+
function resetState() {
|
|
38
|
+
const state = (globalThis as Record<string, unknown>)[globalKey] as RuntimeState | undefined
|
|
39
|
+
if (state) {
|
|
40
|
+
state.runningByExecution.clear()
|
|
41
|
+
state.queueByExecution.clear()
|
|
42
|
+
state.runs.clear()
|
|
43
|
+
state.recentRunIds.length = 0
|
|
44
|
+
state.promises.clear()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Wrapper around enqueueSessionRun that captures the run promise to
|
|
49
|
+
* prevent async-after-test warnings from node:test. */
|
|
50
|
+
function enqueue(input: Parameters<typeof mgr.enqueueSessionRun>[0]) {
|
|
51
|
+
const result = mgr.enqueueSessionRun(input)
|
|
52
|
+
const suppressed = result.promise.catch(() => {})
|
|
53
|
+
pendingPromises.push(suppressed)
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
before(async () => {
|
|
58
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-session-run-mgr-'))
|
|
59
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
60
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
61
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
62
|
+
|
|
63
|
+
storage = await import('./storage')
|
|
64
|
+
mgr = await import('./session-run-manager')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function seedSession(id: string) {
|
|
68
|
+
const sessions = storage.loadSessions()
|
|
69
|
+
sessions[id] = {
|
|
70
|
+
id,
|
|
71
|
+
agentId: 'test-agent',
|
|
72
|
+
messages: [],
|
|
73
|
+
createdAt: Date.now(),
|
|
74
|
+
lastActiveAt: Date.now(),
|
|
75
|
+
}
|
|
76
|
+
storage.saveSessions(sessions)
|
|
77
|
+
const agents = storage.loadAgents()
|
|
78
|
+
if (!agents['test-agent']) {
|
|
79
|
+
agents['test-agent'] = {
|
|
80
|
+
id: 'test-agent',
|
|
81
|
+
name: 'Test Agent',
|
|
82
|
+
provider: 'anthropic',
|
|
83
|
+
model: 'claude-sonnet-4-20250514',
|
|
84
|
+
systemPrompt: 'You are a test agent.',
|
|
85
|
+
}
|
|
86
|
+
storage.saveAgents(agents)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
after(() => {
|
|
91
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
92
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
93
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
94
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
95
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
96
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
97
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
98
|
+
process.removeListener('unhandledRejection', suppressionHandler)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
afterEach(async () => {
|
|
102
|
+
// Wait for all background drain activity to settle before resetting state
|
|
103
|
+
await Promise.allSettled(pendingPromises)
|
|
104
|
+
pendingPromises.length = 0
|
|
105
|
+
resetState()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('session-run-manager', () => {
|
|
109
|
+
describe('enqueueSessionRun', () => {
|
|
110
|
+
it('returns a run ID and queued position', () => {
|
|
111
|
+
const result = enqueue({
|
|
112
|
+
sessionId: 'sess-1',
|
|
113
|
+
message: 'Hello world',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
assert.ok(result.runId, 'should have a run ID')
|
|
117
|
+
assert.equal(typeof result.runId, 'string')
|
|
118
|
+
assert.equal(typeof result.position, 'number')
|
|
119
|
+
assert.ok(result.promise instanceof Promise, 'should return a promise')
|
|
120
|
+
assert.equal(typeof result.abort, 'function')
|
|
121
|
+
assert.equal(typeof result.unsubscribe, 'function')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('registers the run record accessible via getRunById', () => {
|
|
125
|
+
const result = enqueue({
|
|
126
|
+
sessionId: 'sess-2',
|
|
127
|
+
message: 'Test message',
|
|
128
|
+
source: 'chat',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const run = mgr.getRunById(result.runId)
|
|
132
|
+
assert.ok(run, 'run should exist')
|
|
133
|
+
assert.equal(run.sessionId, 'sess-2')
|
|
134
|
+
assert.equal(run.source, 'chat')
|
|
135
|
+
assert.equal(run.messagePreview, 'Test message')
|
|
136
|
+
assert.ok(run.queuedAt > 0)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('truncates message preview to 140 chars', () => {
|
|
140
|
+
const longMessage = 'A'.repeat(200)
|
|
141
|
+
const result = enqueue({
|
|
142
|
+
sessionId: 'sess-trunc',
|
|
143
|
+
message: longMessage,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const run = mgr.getRunById(result.runId)
|
|
147
|
+
assert.ok(run)
|
|
148
|
+
assert.equal(run.messagePreview.length, 140)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('defaults internal to false and source to chat', () => {
|
|
152
|
+
const result = enqueue({
|
|
153
|
+
sessionId: 'sess-defaults',
|
|
154
|
+
message: 'test',
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const run = mgr.getRunById(result.runId)
|
|
158
|
+
assert.ok(run)
|
|
159
|
+
assert.equal(run.internal, false)
|
|
160
|
+
assert.equal(run.source, 'chat')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('normalizes mode to followup for non-internal runs', () => {
|
|
164
|
+
const result = enqueue({
|
|
165
|
+
sessionId: 'sess-mode',
|
|
166
|
+
message: 'test',
|
|
167
|
+
internal: false,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const run = mgr.getRunById(result.runId)
|
|
171
|
+
assert.ok(run)
|
|
172
|
+
assert.equal(run.mode, 'followup')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('normalizes mode to collect for internal runs without explicit mode', () => {
|
|
176
|
+
const result = enqueue({
|
|
177
|
+
sessionId: 'sess-mode-int',
|
|
178
|
+
message: 'test',
|
|
179
|
+
internal: true,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const run = mgr.getRunById(result.runId)
|
|
183
|
+
assert.ok(run)
|
|
184
|
+
assert.equal(run.mode, 'collect')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('preserves explicit mode when provided', () => {
|
|
188
|
+
const result = enqueue({
|
|
189
|
+
sessionId: 'sess-explicit-mode',
|
|
190
|
+
message: 'test',
|
|
191
|
+
mode: 'steer',
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const run = mgr.getRunById(result.runId)
|
|
195
|
+
assert.ok(run)
|
|
196
|
+
assert.equal(run.mode, 'steer')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('deduplication', () => {
|
|
201
|
+
it('deduplicates queued runs with the same dedupeKey', () => {
|
|
202
|
+
const first = enqueue({
|
|
203
|
+
sessionId: 'sess-dedup',
|
|
204
|
+
message: 'first run',
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
const run1 = enqueue({
|
|
208
|
+
sessionId: 'sess-dedup',
|
|
209
|
+
message: 'deduped message',
|
|
210
|
+
dedupeKey: 'key-1',
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const run2 = enqueue({
|
|
214
|
+
sessionId: 'sess-dedup',
|
|
215
|
+
message: 'duplicate message',
|
|
216
|
+
dedupeKey: 'key-1',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
assert.equal(run2.deduped, true, 'second run should be deduped')
|
|
220
|
+
assert.equal(run2.runId, run1.runId, 'deduped run should share the same run ID')
|
|
221
|
+
assert.ok(first.runId !== run1.runId, 'first run should be different from deduped runs')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('does not deduplicate runs without dedupeKey', () => {
|
|
225
|
+
enqueue({ sessionId: 'sess-no-dedup', message: 'occupier' })
|
|
226
|
+
|
|
227
|
+
const run1 = enqueue({ sessionId: 'sess-no-dedup', message: 'msg1' })
|
|
228
|
+
const run2 = enqueue({ sessionId: 'sess-no-dedup', message: 'msg2' })
|
|
229
|
+
|
|
230
|
+
assert.ok(run1.runId !== run2.runId, 'runs without dedupeKey should have different IDs')
|
|
231
|
+
assert.equal(run2.deduped, undefined)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('does not deduplicate runs with different dedupeKeys', () => {
|
|
235
|
+
enqueue({ sessionId: 'sess-diff-keys', message: 'occupier' })
|
|
236
|
+
|
|
237
|
+
const run1 = enqueue({
|
|
238
|
+
sessionId: 'sess-diff-keys',
|
|
239
|
+
message: 'msg1',
|
|
240
|
+
dedupeKey: 'alpha',
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const run2 = enqueue({
|
|
244
|
+
sessionId: 'sess-diff-keys',
|
|
245
|
+
message: 'msg2',
|
|
246
|
+
dedupeKey: 'beta',
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
assert.ok(run1.runId !== run2.runId)
|
|
250
|
+
assert.equal(run2.deduped, undefined)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('collect mode coalescing', () => {
|
|
255
|
+
it('coalesces internal collect-mode messages within the time window', () => {
|
|
256
|
+
enqueue({ sessionId: 'sess-coalesce', message: 'occupier' })
|
|
257
|
+
|
|
258
|
+
const run1 = enqueue({
|
|
259
|
+
sessionId: 'sess-coalesce',
|
|
260
|
+
message: 'first collect',
|
|
261
|
+
internal: true,
|
|
262
|
+
source: 'heartbeat',
|
|
263
|
+
mode: 'collect',
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const run2 = enqueue({
|
|
267
|
+
sessionId: 'sess-coalesce',
|
|
268
|
+
message: 'second collect',
|
|
269
|
+
internal: true,
|
|
270
|
+
source: 'heartbeat',
|
|
271
|
+
mode: 'collect',
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
assert.equal(run2.coalesced, true, 'second collect should be coalesced')
|
|
275
|
+
assert.equal(run2.runId, run1.runId, 'coalesced run should share the same run ID')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('does not coalesce messages with different sources', () => {
|
|
279
|
+
enqueue({ sessionId: 'sess-no-coalesce-src', message: 'occupier' })
|
|
280
|
+
|
|
281
|
+
const run1 = enqueue({
|
|
282
|
+
sessionId: 'sess-no-coalesce-src',
|
|
283
|
+
message: 'first',
|
|
284
|
+
internal: true,
|
|
285
|
+
source: 'heartbeat',
|
|
286
|
+
mode: 'collect',
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
const run2 = enqueue({
|
|
290
|
+
sessionId: 'sess-no-coalesce-src',
|
|
291
|
+
message: 'second',
|
|
292
|
+
internal: true,
|
|
293
|
+
source: 'other-source',
|
|
294
|
+
mode: 'collect',
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
assert.ok(run1.runId !== run2.runId, 'different sources should not coalesce')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('does not coalesce when there are image attachments', () => {
|
|
301
|
+
enqueue({ sessionId: 'sess-no-coalesce-img', message: 'occupier' })
|
|
302
|
+
|
|
303
|
+
const run1 = enqueue({
|
|
304
|
+
sessionId: 'sess-no-coalesce-img',
|
|
305
|
+
message: 'first',
|
|
306
|
+
internal: true,
|
|
307
|
+
source: 'heartbeat',
|
|
308
|
+
mode: 'collect',
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const run2 = enqueue({
|
|
312
|
+
sessionId: 'sess-no-coalesce-img',
|
|
313
|
+
message: 'second with image',
|
|
314
|
+
internal: true,
|
|
315
|
+
source: 'heartbeat',
|
|
316
|
+
mode: 'collect',
|
|
317
|
+
imagePath: '/path/to/image.png',
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
assert.ok(run1.runId !== run2.runId, 'image attachments should prevent coalescing')
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
describe('getSessionRunState / getSessionExecutionState', () => {
|
|
325
|
+
it('returns empty state for unknown session', () => {
|
|
326
|
+
const state = mgr.getSessionRunState('unknown-session')
|
|
327
|
+
assert.equal(state.runningRunId, undefined)
|
|
328
|
+
assert.equal(state.queueLength, 0)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('returns execution state with queue info', () => {
|
|
332
|
+
enqueue({ sessionId: 'sess-state', message: 'running' })
|
|
333
|
+
enqueue({ sessionId: 'sess-state', message: 'queued 1' })
|
|
334
|
+
|
|
335
|
+
const state = mgr.getSessionExecutionState('sess-state')
|
|
336
|
+
assert.equal(state.hasQueued, true)
|
|
337
|
+
assert.ok(state.queueLength >= 1)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('reports heartbeat vs non-heartbeat queued runs', () => {
|
|
341
|
+
enqueue({ sessionId: 'sess-hb-state', message: 'occupier' })
|
|
342
|
+
enqueue({
|
|
343
|
+
sessionId: 'sess-hb-state',
|
|
344
|
+
message: 'hb',
|
|
345
|
+
internal: true,
|
|
346
|
+
source: 'heartbeat',
|
|
347
|
+
})
|
|
348
|
+
enqueue({
|
|
349
|
+
sessionId: 'sess-hb-state',
|
|
350
|
+
message: 'user',
|
|
351
|
+
internal: false,
|
|
352
|
+
source: 'chat',
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const state = mgr.getSessionExecutionState('sess-hb-state')
|
|
356
|
+
assert.equal(state.hasQueuedHeartbeat, true)
|
|
357
|
+
assert.equal(state.hasQueuedNonHeartbeat, true)
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('listRuns', () => {
|
|
362
|
+
it('lists all runs in reverse chronological order', () => {
|
|
363
|
+
enqueue({ sessionId: 'sess-list-a', message: 'msg a' })
|
|
364
|
+
enqueue({ sessionId: 'sess-list-b', message: 'msg b' })
|
|
365
|
+
|
|
366
|
+
const runs = mgr.listRuns()
|
|
367
|
+
assert.ok(runs.length >= 2)
|
|
368
|
+
const idxA = runs.findIndex(r => r.sessionId === 'sess-list-a')
|
|
369
|
+
const idxB = runs.findIndex(r => r.sessionId === 'sess-list-b')
|
|
370
|
+
assert.ok(idxB < idxA, 'more recent run should be first')
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('filters by sessionId', () => {
|
|
374
|
+
enqueue({ sessionId: 'sess-filter-a', message: 'a' })
|
|
375
|
+
enqueue({ sessionId: 'sess-filter-b', message: 'b' })
|
|
376
|
+
|
|
377
|
+
const runs = mgr.listRuns({ sessionId: 'sess-filter-a' })
|
|
378
|
+
assert.ok(runs.length >= 1)
|
|
379
|
+
for (const run of runs) {
|
|
380
|
+
assert.equal(run.sessionId, 'sess-filter-a')
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('filters by status', () => {
|
|
385
|
+
enqueue({ sessionId: 'sess-status-a', message: 'a' })
|
|
386
|
+
enqueue({ sessionId: 'sess-status-b', message: 'b' })
|
|
387
|
+
|
|
388
|
+
// At least one should be queued synchronously
|
|
389
|
+
const queued = mgr.listRuns({ status: 'queued' })
|
|
390
|
+
// We just verify the filter doesn't crash and returns consistent data
|
|
391
|
+
for (const run of queued) {
|
|
392
|
+
assert.equal(run.status, 'queued')
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('respects limit parameter', () => {
|
|
397
|
+
for (let i = 0; i < 5; i++) {
|
|
398
|
+
enqueue({ sessionId: `sess-limit-${i}`, message: `msg ${i}` })
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const runs = mgr.listRuns({ limit: 2 })
|
|
402
|
+
assert.equal(runs.length, 2)
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
describe('cancelSessionRuns', () => {
|
|
407
|
+
it('cancels queued runs for a session', () => {
|
|
408
|
+
enqueue({ sessionId: 'sess-cancel', message: 'running' })
|
|
409
|
+
enqueue({ sessionId: 'sess-cancel', message: 'queued 1' })
|
|
410
|
+
enqueue({ sessionId: 'sess-cancel', message: 'queued 2' })
|
|
411
|
+
|
|
412
|
+
const result = mgr.cancelSessionRuns('sess-cancel', 'User cancelled')
|
|
413
|
+
assert.ok(result.cancelledQueued >= 2, `should cancel at least 2 queued runs, got ${result.cancelledQueued}`)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('returns zero when no runs exist for session', () => {
|
|
417
|
+
const result = mgr.cancelSessionRuns('nonexistent-session')
|
|
418
|
+
assert.equal(result.cancelledQueued, 0)
|
|
419
|
+
assert.equal(result.cancelledRunning, false)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('does not cancel runs for other sessions', () => {
|
|
423
|
+
enqueue({ sessionId: 'sess-keep', message: 'keep me' })
|
|
424
|
+
enqueue({ sessionId: 'sess-cancel-other', message: 'cancel me' })
|
|
425
|
+
|
|
426
|
+
mgr.cancelSessionRuns('sess-cancel-other', 'cancelled')
|
|
427
|
+
|
|
428
|
+
const keepRuns = mgr.listRuns({ sessionId: 'sess-keep' })
|
|
429
|
+
assert.ok(keepRuns.length >= 1, 'runs for other session should be preserved')
|
|
430
|
+
const keptRun = keepRuns.find(r => r.status !== 'cancelled')
|
|
431
|
+
assert.ok(keptRun, 'kept session run should not be cancelled')
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
describe('steer mode', () => {
|
|
436
|
+
it('cancels pending queued runs when steer mode is used', () => {
|
|
437
|
+
enqueue({ sessionId: 'sess-steer', message: 'running' })
|
|
438
|
+
enqueue({ sessionId: 'sess-steer', message: 'queued 1' })
|
|
439
|
+
enqueue({ sessionId: 'sess-steer', message: 'queued 2' })
|
|
440
|
+
|
|
441
|
+
const steer = enqueue({
|
|
442
|
+
sessionId: 'sess-steer',
|
|
443
|
+
message: 'steer message',
|
|
444
|
+
mode: 'steer',
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
assert.ok(steer.runId)
|
|
448
|
+
const steerRun = mgr.getRunById(steer.runId)
|
|
449
|
+
assert.ok(steerRun)
|
|
450
|
+
assert.notEqual(steerRun.status, 'cancelled')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('steer marks previously queued runs as cancelled', () => {
|
|
454
|
+
enqueue({ sessionId: 'sess-steer-verify', message: 'occupier' })
|
|
455
|
+
const q1 = enqueue({ sessionId: 'sess-steer-verify', message: 'will be cancelled' })
|
|
456
|
+
const q2 = enqueue({ sessionId: 'sess-steer-verify', message: 'also cancelled' })
|
|
457
|
+
|
|
458
|
+
enqueue({
|
|
459
|
+
sessionId: 'sess-steer-verify',
|
|
460
|
+
message: 'steer',
|
|
461
|
+
mode: 'steer',
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const run1 = mgr.getRunById(q1.runId)
|
|
465
|
+
const run2 = mgr.getRunById(q2.runId)
|
|
466
|
+
assert.ok(run1)
|
|
467
|
+
assert.ok(run2)
|
|
468
|
+
assert.equal(run1.status, 'cancelled')
|
|
469
|
+
assert.equal(run2.status, 'cancelled')
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
describe('abort and unsubscribe', () => {
|
|
474
|
+
it('abort function is callable without error', () => {
|
|
475
|
+
const result = enqueue({
|
|
476
|
+
sessionId: 'sess-abort',
|
|
477
|
+
message: 'abort me',
|
|
478
|
+
})
|
|
479
|
+
assert.doesNotThrow(() => result.abort())
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('unsubscribe removes the event listener', () => {
|
|
483
|
+
const events: unknown[] = []
|
|
484
|
+
const result = enqueue({
|
|
485
|
+
sessionId: 'sess-unsub',
|
|
486
|
+
message: 'test',
|
|
487
|
+
onEvent: (event) => events.push(event),
|
|
488
|
+
})
|
|
489
|
+
assert.doesNotThrow(() => result.unsubscribe())
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
describe('callerSignal chaining', () => {
|
|
494
|
+
it('propagates an already-aborted callerSignal', () => {
|
|
495
|
+
const controller = new AbortController()
|
|
496
|
+
controller.abort()
|
|
497
|
+
|
|
498
|
+
const result = enqueue({
|
|
499
|
+
sessionId: 'sess-pre-aborted',
|
|
500
|
+
message: 'test',
|
|
501
|
+
callerSignal: controller.signal,
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
assert.doesNotThrow(() => result.abort())
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('chains a live callerSignal to the run', () => {
|
|
508
|
+
const controller = new AbortController()
|
|
509
|
+
|
|
510
|
+
const result = enqueue({
|
|
511
|
+
sessionId: 'sess-live-signal',
|
|
512
|
+
message: 'test',
|
|
513
|
+
callerSignal: controller.signal,
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
// Aborting the caller should work without throwing
|
|
517
|
+
assert.doesNotThrow(() => controller.abort())
|
|
518
|
+
assert.ok(result.runId)
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe('run completion and drain', () => {
|
|
523
|
+
it('run eventually transitions from queued to a terminal state', async () => {
|
|
524
|
+
seedSession('sess-terminal')
|
|
525
|
+
const result = enqueue({
|
|
526
|
+
sessionId: 'sess-terminal',
|
|
527
|
+
message: 'will fail in drain',
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
// Wait for drain to process
|
|
531
|
+
await result.promise.catch(() => {})
|
|
532
|
+
|
|
533
|
+
const run = mgr.getRunById(result.runId)
|
|
534
|
+
assert.ok(run, 'run should still exist')
|
|
535
|
+
const terminal = ['completed', 'failed', 'cancelled']
|
|
536
|
+
assert.ok(
|
|
537
|
+
terminal.includes(run.status),
|
|
538
|
+
`run status should be terminal, got: ${run.status}`,
|
|
539
|
+
)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('drain processes next queued run after current completes', async () => {
|
|
543
|
+
seedSession('sess-drain-chain')
|
|
544
|
+
const result1 = enqueue({
|
|
545
|
+
sessionId: 'sess-drain-chain',
|
|
546
|
+
message: 'first',
|
|
547
|
+
})
|
|
548
|
+
const result2 = enqueue({
|
|
549
|
+
sessionId: 'sess-drain-chain',
|
|
550
|
+
message: 'second',
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
// Wait for both drains to process
|
|
554
|
+
await Promise.allSettled([result1.promise, result2.promise])
|
|
555
|
+
|
|
556
|
+
const run1 = mgr.getRunById(result1.runId)
|
|
557
|
+
const run2 = mgr.getRunById(result2.runId)
|
|
558
|
+
assert.ok(run1)
|
|
559
|
+
assert.ok(run2)
|
|
560
|
+
assert.notEqual(run1.status, 'queued', 'first run should not still be queued')
|
|
561
|
+
assert.notEqual(run2.status, 'queued', 'second run should not still be queued')
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('failed run records error message', async () => {
|
|
565
|
+
seedSession('sess-fail-error')
|
|
566
|
+
const result = enqueue({
|
|
567
|
+
sessionId: 'sess-fail-error',
|
|
568
|
+
message: 'will error',
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
await result.promise.catch(() => {})
|
|
572
|
+
|
|
573
|
+
const run = mgr.getRunById(result.runId)
|
|
574
|
+
assert.ok(run)
|
|
575
|
+
if (run.status === 'failed') {
|
|
576
|
+
assert.ok(run.error, 'failed run should have an error message')
|
|
577
|
+
assert.ok(run.endedAt, 'failed run should have endedAt timestamp')
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
describe('cancelAllHeartbeatRuns', () => {
|
|
583
|
+
it('cancels queued heartbeat runs but keeps non-heartbeat runs', () => {
|
|
584
|
+
enqueue({ sessionId: 'sess-hb-cancel', message: 'occupier' })
|
|
585
|
+
|
|
586
|
+
enqueue({
|
|
587
|
+
sessionId: 'sess-hb-cancel',
|
|
588
|
+
message: 'heartbeat msg',
|
|
589
|
+
internal: true,
|
|
590
|
+
source: 'heartbeat',
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
enqueue({
|
|
594
|
+
sessionId: 'sess-hb-cancel',
|
|
595
|
+
message: 'user msg',
|
|
596
|
+
internal: false,
|
|
597
|
+
source: 'chat',
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
const result = mgr.cancelAllHeartbeatRuns('Test cancellation')
|
|
601
|
+
assert.ok(result.cancelledQueued >= 1, 'should cancel at least 1 queued heartbeat')
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('returns zeros when no heartbeat runs exist', () => {
|
|
605
|
+
const result = mgr.cancelAllHeartbeatRuns()
|
|
606
|
+
assert.equal(result.cancelledQueued, 0)
|
|
607
|
+
assert.equal(result.abortedRunning, 0)
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
it('preserves non-heartbeat queued runs', () => {
|
|
611
|
+
enqueue({ sessionId: 'sess-hb-keep', message: 'occupier' })
|
|
612
|
+
|
|
613
|
+
enqueue({
|
|
614
|
+
sessionId: 'sess-hb-keep',
|
|
615
|
+
message: 'heartbeat',
|
|
616
|
+
internal: true,
|
|
617
|
+
source: 'heartbeat',
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
const userRun = enqueue({
|
|
621
|
+
sessionId: 'sess-hb-keep',
|
|
622
|
+
message: 'user chat',
|
|
623
|
+
internal: false,
|
|
624
|
+
source: 'chat',
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
mgr.cancelAllHeartbeatRuns()
|
|
628
|
+
|
|
629
|
+
const userRunRecord = mgr.getRunById(userRun.runId)
|
|
630
|
+
assert.ok(userRunRecord)
|
|
631
|
+
assert.notEqual(userRunRecord.status, 'cancelled', 'non-heartbeat run should not be cancelled')
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
describe('getRunById', () => {
|
|
636
|
+
it('returns null for non-existent run', () => {
|
|
637
|
+
assert.equal(mgr.getRunById('nonexistent'), null)
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
})
|