@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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wake Dispatcher — central routing for wake requests by WakeMode.
|
|
3
|
+
*
|
|
4
|
+
* Instead of callers directly choosing between `requestHeartbeatNow()` (immediate)
|
|
5
|
+
* and the heartbeat-service timer (deferred), they call `dispatchWake()` with an
|
|
6
|
+
* explicit WakeMode. The dispatcher routes to the correct execution path.
|
|
7
|
+
*
|
|
8
|
+
* This replaces the pattern where scheduler.ts both creates a task AND calls
|
|
9
|
+
* requestHeartbeatNow() — the dispatcher handles that routing centrally.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { genId } from '@/lib/id'
|
|
13
|
+
import { log } from './logger'
|
|
14
|
+
import type { WakeModeRequest, JobContext } from './wake-mode'
|
|
15
|
+
import {
|
|
16
|
+
computeWakePriority,
|
|
17
|
+
resolveRunAt,
|
|
18
|
+
wakeModeToSource,
|
|
19
|
+
createJobContext,
|
|
20
|
+
} from './wake-mode'
|
|
21
|
+
import type { WakeRequestInput } from './heartbeat-wake'
|
|
22
|
+
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
23
|
+
import { enqueueSystemEvent } from './system-events'
|
|
24
|
+
import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
|
|
25
|
+
|
|
26
|
+
// ── Deferred queue for `next_heartbeat` mode ────────────────────────────
|
|
27
|
+
|
|
28
|
+
interface DeferredWake {
|
|
29
|
+
request: WakeModeRequest
|
|
30
|
+
priority: number
|
|
31
|
+
enqueuedAt: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const state = hmrSingleton('__swarmclaw_wake_dispatcher__', () => ({
|
|
35
|
+
deferredQueue: new Map<string, DeferredWake[]>(),
|
|
36
|
+
scheduledTimers: new Map<string, ReturnType<typeof setTimeout>>(),
|
|
37
|
+
activeJobs: new Map<string, JobContext>(),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
function deferredKey(request: WakeModeRequest): string {
|
|
41
|
+
return `${request.agentId || ''}::${request.sessionId || ''}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Dispatch a wake request through the appropriate execution path.
|
|
48
|
+
*
|
|
49
|
+
* - `immediate`: forwards to requestHeartbeatNow with coalesce
|
|
50
|
+
* - `next_heartbeat`: queues for the next heartbeat-service tick to drain
|
|
51
|
+
* - `scheduled`: sets a timer to fire requestHeartbeatNow at the target time
|
|
52
|
+
*/
|
|
53
|
+
export function dispatchWake(request: WakeModeRequest): {
|
|
54
|
+
mode: string
|
|
55
|
+
priority: number
|
|
56
|
+
runAt: number | null
|
|
57
|
+
jobId: string
|
|
58
|
+
} {
|
|
59
|
+
const priority = computeWakePriority(request)
|
|
60
|
+
const runAt = resolveRunAt(request)
|
|
61
|
+
const jobId = genId(8)
|
|
62
|
+
|
|
63
|
+
switch (request.mode) {
|
|
64
|
+
case 'immediate':
|
|
65
|
+
dispatchImmediate(request, priority, jobId)
|
|
66
|
+
break
|
|
67
|
+
case 'next_heartbeat':
|
|
68
|
+
dispatchDeferred(request, priority, jobId)
|
|
69
|
+
break
|
|
70
|
+
case 'scheduled':
|
|
71
|
+
dispatchScheduled(request, priority, runAt, jobId)
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { mode: request.mode, priority, runAt, jobId }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dispatchImmediate(request: WakeModeRequest, priority: number, jobId: string): void {
|
|
79
|
+
const wakeInput: WakeRequestInput = {
|
|
80
|
+
eventId: request.eventId || jobId,
|
|
81
|
+
agentId: request.agentId,
|
|
82
|
+
sessionId: request.sessionId,
|
|
83
|
+
reason: request.reason || 'immediate-wake',
|
|
84
|
+
source: request.source,
|
|
85
|
+
resumeMessage: request.resumeMessage,
|
|
86
|
+
detail: request.detail,
|
|
87
|
+
priority,
|
|
88
|
+
}
|
|
89
|
+
requestHeartbeatNow(wakeInput)
|
|
90
|
+
log.info('wake-dispatcher', `Dispatched immediate wake ${jobId}`, {
|
|
91
|
+
agentId: request.agentId,
|
|
92
|
+
sessionId: request.sessionId,
|
|
93
|
+
reason: request.reason,
|
|
94
|
+
priority,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function dispatchDeferred(request: WakeModeRequest, priority: number, jobId: string): void {
|
|
99
|
+
const key = deferredKey(request)
|
|
100
|
+
const queue = state.deferredQueue.get(key) || []
|
|
101
|
+
|
|
102
|
+
// Deduplicate by eventId or reason+source
|
|
103
|
+
const existingIndex = queue.findIndex((entry: any) => {
|
|
104
|
+
if (request.eventId && entry.request.eventId) {
|
|
105
|
+
return request.eventId === entry.request.eventId
|
|
106
|
+
}
|
|
107
|
+
return entry.request.reason === request.reason
|
|
108
|
+
&& entry.request.source === request.source
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const entry: DeferredWake = { request, priority, enqueuedAt: Date.now() }
|
|
112
|
+
|
|
113
|
+
if (existingIndex >= 0) {
|
|
114
|
+
// Replace with higher-priority version
|
|
115
|
+
if (priority >= queue[existingIndex].priority) {
|
|
116
|
+
queue[existingIndex] = entry
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
queue.push(entry)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Keep sorted by priority (highest first)
|
|
123
|
+
queue.sort((a: any, b: any) => b.priority - a.priority)
|
|
124
|
+
state.deferredQueue.set(key, queue)
|
|
125
|
+
|
|
126
|
+
log.info('wake-dispatcher', `Deferred wake ${jobId} queued for next heartbeat`, {
|
|
127
|
+
agentId: request.agentId,
|
|
128
|
+
sessionId: request.sessionId,
|
|
129
|
+
reason: request.reason,
|
|
130
|
+
queueDepth: queue.length,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function dispatchScheduled(
|
|
135
|
+
request: WakeModeRequest,
|
|
136
|
+
priority: number,
|
|
137
|
+
runAt: number | null,
|
|
138
|
+
jobId: string,
|
|
139
|
+
): void {
|
|
140
|
+
const now = Date.now()
|
|
141
|
+
const targetTime = runAt ?? now
|
|
142
|
+
const delayMs = Math.max(0, targetTime - now)
|
|
143
|
+
|
|
144
|
+
// If the target time is now or in the past, dispatch immediately
|
|
145
|
+
if (delayMs <= 0) {
|
|
146
|
+
dispatchImmediate(request, priority, jobId)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Set a timer to fire at the target time
|
|
151
|
+
const timerId = setTimeout(() => {
|
|
152
|
+
state.scheduledTimers.delete(jobId)
|
|
153
|
+
try {
|
|
154
|
+
dispatchImmediate(request, priority, jobId)
|
|
155
|
+
} catch (err: unknown) {
|
|
156
|
+
log.error('wake-dispatcher', `Scheduled wake ${jobId} failed, retrying once`, {
|
|
157
|
+
error: errorMessage(err),
|
|
158
|
+
agentId: request.agentId,
|
|
159
|
+
sessionId: request.sessionId,
|
|
160
|
+
})
|
|
161
|
+
// Single retry after 5s — if this also fails, the wake is lost (logged above)
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
try { dispatchImmediate(request, priority, jobId) } catch { /* give up */ }
|
|
164
|
+
}, 5000)
|
|
165
|
+
}
|
|
166
|
+
log.info('wake-dispatcher', `Scheduled wake ${jobId} fired after ${delayMs}ms delay`, {
|
|
167
|
+
agentId: request.agentId,
|
|
168
|
+
sessionId: request.sessionId,
|
|
169
|
+
reason: request.reason,
|
|
170
|
+
})
|
|
171
|
+
}, delayMs)
|
|
172
|
+
|
|
173
|
+
state.scheduledTimers.set(jobId, timerId)
|
|
174
|
+
|
|
175
|
+
log.info('wake-dispatcher', `Scheduled wake ${jobId} for ${new Date(targetTime).toISOString()}`, {
|
|
176
|
+
agentId: request.agentId,
|
|
177
|
+
sessionId: request.sessionId,
|
|
178
|
+
reason: request.reason,
|
|
179
|
+
delayMs,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Also enqueue a system event so the agent knows something is scheduled
|
|
183
|
+
if (request.sessionId && request.resumeMessage) {
|
|
184
|
+
enqueueSystemEvent(
|
|
185
|
+
request.sessionId,
|
|
186
|
+
`[Scheduled] ${request.resumeMessage} (fires at ${new Date(targetTime).toISOString()})`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Deferred queue drain (called by heartbeat-service on each tick) ─────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Drain all deferred wakes for a given session/agent. Called by heartbeat-service
|
|
195
|
+
* during its periodic tick to pick up `next_heartbeat` mode requests.
|
|
196
|
+
*
|
|
197
|
+
* Returns the deferred events so the heartbeat-service can include them in
|
|
198
|
+
* the prompt context alongside normal heartbeat content.
|
|
199
|
+
*/
|
|
200
|
+
export function drainDeferredWakes(agentId?: string, sessionId?: string): WakeModeRequest[] {
|
|
201
|
+
const key = `${agentId || ''}::${sessionId || ''}`
|
|
202
|
+
const queue = state.deferredQueue.get(key)
|
|
203
|
+
if (!queue || queue.length === 0) return []
|
|
204
|
+
|
|
205
|
+
const drained = queue.map((entry: any) => entry.request)
|
|
206
|
+
state.deferredQueue.delete(key)
|
|
207
|
+
|
|
208
|
+
log.info('wake-dispatcher', `Drained ${drained.length} deferred wakes`, {
|
|
209
|
+
agentId,
|
|
210
|
+
sessionId,
|
|
211
|
+
reasons: drained.map((r: any) => r.reason),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
return drained
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if there are pending deferred wakes for a target.
|
|
219
|
+
*/
|
|
220
|
+
export function hasDeferredWakes(agentId?: string, sessionId?: string): boolean {
|
|
221
|
+
const key = `${agentId || ''}::${sessionId || ''}`
|
|
222
|
+
const queue = state.deferredQueue.get(key)
|
|
223
|
+
return !!queue && queue.length > 0
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Job context management ──────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create and register an isolated job context for a wake execution.
|
|
230
|
+
* The job context provides per-job scratchpad, abort signal, and
|
|
231
|
+
* heartbeat snapshot isolation.
|
|
232
|
+
*/
|
|
233
|
+
export function startJobExecution(params: {
|
|
234
|
+
sessionId: string
|
|
235
|
+
agentId?: string
|
|
236
|
+
mode: WakeModeRequest['mode']
|
|
237
|
+
signal: AbortSignal
|
|
238
|
+
source?: string
|
|
239
|
+
reason?: string
|
|
240
|
+
heartbeatSnapshot?: string
|
|
241
|
+
}): JobContext {
|
|
242
|
+
const jobId = genId(8)
|
|
243
|
+
const ctx = createJobContext({
|
|
244
|
+
jobId,
|
|
245
|
+
sessionId: params.sessionId,
|
|
246
|
+
agentId: params.agentId,
|
|
247
|
+
mode: params.mode,
|
|
248
|
+
signal: params.signal,
|
|
249
|
+
source: params.source,
|
|
250
|
+
reason: params.reason,
|
|
251
|
+
heartbeatSnapshot: params.heartbeatSnapshot,
|
|
252
|
+
})
|
|
253
|
+
ctx.startedAt = Date.now()
|
|
254
|
+
state.activeJobs.set(jobId, ctx)
|
|
255
|
+
return ctx
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Mark a job as completed and remove from active tracking.
|
|
260
|
+
*/
|
|
261
|
+
export function endJobExecution(jobId: string): JobContext | null {
|
|
262
|
+
const ctx = state.activeJobs.get(jobId)
|
|
263
|
+
if (!ctx) return null
|
|
264
|
+
ctx.endedAt = Date.now()
|
|
265
|
+
state.activeJobs.delete(jobId)
|
|
266
|
+
return ctx
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get the active job context by ID.
|
|
271
|
+
*/
|
|
272
|
+
export function getActiveJob(jobId: string): JobContext | null {
|
|
273
|
+
return state.activeJobs.get(jobId) || null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* List all active job contexts for a session.
|
|
278
|
+
*/
|
|
279
|
+
export function getActiveJobsForSession(sessionId: string): JobContext[] {
|
|
280
|
+
return [...state.activeJobs.values()].filter((ctx) => ctx.sessionId === sessionId)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Cleanup ─────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
export function cancelScheduledWake(jobId: string): boolean {
|
|
286
|
+
const timer = state.scheduledTimers.get(jobId)
|
|
287
|
+
if (!timer) return false
|
|
288
|
+
clearTimeout(timer)
|
|
289
|
+
state.scheduledTimers.delete(jobId)
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function resetWakeDispatcherForTests(): void {
|
|
294
|
+
for (const timer of state.scheduledTimers.values()) {
|
|
295
|
+
clearTimeout(timer)
|
|
296
|
+
}
|
|
297
|
+
state.deferredQueue.clear()
|
|
298
|
+
state.scheduledTimers.clear()
|
|
299
|
+
state.activeJobs.clear()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Diagnostics ─────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
export function getWakeDispatcherStatus(): {
|
|
305
|
+
deferredQueueCount: number
|
|
306
|
+
scheduledTimerCount: number
|
|
307
|
+
activeJobCount: number
|
|
308
|
+
} {
|
|
309
|
+
let deferredCount = 0
|
|
310
|
+
for (const queue of state.deferredQueue.values()) {
|
|
311
|
+
deferredCount += queue.length
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
deferredQueueCount: deferredCount,
|
|
315
|
+
scheduledTimerCount: state.scheduledTimers.size,
|
|
316
|
+
activeJobCount: state.activeJobs.size,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
computeWakePriority,
|
|
6
|
+
createJobContext,
|
|
7
|
+
resolveRunAt,
|
|
8
|
+
sourceToWakeMode,
|
|
9
|
+
wakeModeToSource,
|
|
10
|
+
} from './wake-mode'
|
|
11
|
+
import type { WakeModeRequest } from './wake-mode'
|
|
12
|
+
|
|
13
|
+
describe('WakeMode', () => {
|
|
14
|
+
describe('computeWakePriority', () => {
|
|
15
|
+
it('returns mode-based default priority when none specified', () => {
|
|
16
|
+
assert.equal(computeWakePriority({ mode: 'immediate' }), 80)
|
|
17
|
+
assert.equal(computeWakePriority({ mode: 'next_heartbeat' }), 40)
|
|
18
|
+
assert.equal(computeWakePriority({ mode: 'scheduled' }), 60)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('uses explicit priority when provided', () => {
|
|
22
|
+
assert.equal(computeWakePriority({ mode: 'immediate', priority: 95 }), 95)
|
|
23
|
+
assert.equal(computeWakePriority({ mode: 'next_heartbeat', priority: 10 }), 10)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('clamps priority to [0, 100]', () => {
|
|
27
|
+
assert.equal(computeWakePriority({ mode: 'immediate', priority: 150 }), 100)
|
|
28
|
+
assert.equal(computeWakePriority({ mode: 'immediate', priority: -5 }), 0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('ignores non-finite priority values', () => {
|
|
32
|
+
assert.equal(computeWakePriority({ mode: 'immediate', priority: NaN }), 80)
|
|
33
|
+
assert.equal(computeWakePriority({ mode: 'immediate', priority: Infinity }), 80)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('resolveRunAt', () => {
|
|
38
|
+
const NOW = 1_700_000_000_000
|
|
39
|
+
|
|
40
|
+
it('returns now for immediate mode', () => {
|
|
41
|
+
assert.equal(resolveRunAt({ mode: 'immediate' }, NOW), NOW)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns null for next_heartbeat mode (deferred)', () => {
|
|
45
|
+
assert.equal(resolveRunAt({ mode: 'next_heartbeat' }, NOW), null)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns absolute runAt for scheduled mode', () => {
|
|
49
|
+
const target = NOW + 60_000
|
|
50
|
+
assert.equal(resolveRunAt({ mode: 'scheduled', runAt: target }, NOW), target)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('computes runAt from delayMs for scheduled mode', () => {
|
|
54
|
+
assert.equal(resolveRunAt({ mode: 'scheduled', delayMs: 5_000 }, NOW), NOW + 5_000)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('clamps scheduled runAt to at least now', () => {
|
|
58
|
+
const pastTime = NOW - 10_000
|
|
59
|
+
assert.equal(resolveRunAt({ mode: 'scheduled', runAt: pastTime }, NOW), NOW)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('falls back to now for scheduled mode without runAt or delayMs', () => {
|
|
63
|
+
assert.equal(resolveRunAt({ mode: 'scheduled' }, NOW), NOW)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('wakeModeToSource (backward compat)', () => {
|
|
68
|
+
it('maps immediate to heartbeat-wake', () => {
|
|
69
|
+
assert.equal(wakeModeToSource('immediate'), 'heartbeat-wake')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('maps next_heartbeat to heartbeat', () => {
|
|
73
|
+
assert.equal(wakeModeToSource('next_heartbeat'), 'heartbeat')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('maps scheduled to heartbeat-wake', () => {
|
|
77
|
+
assert.equal(wakeModeToSource('scheduled'), 'heartbeat-wake')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('sourceToWakeMode (legacy migration)', () => {
|
|
82
|
+
it('infers next_heartbeat from heartbeat source', () => {
|
|
83
|
+
assert.equal(sourceToWakeMode('heartbeat'), 'next_heartbeat')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('infers immediate from heartbeat-wake source', () => {
|
|
87
|
+
assert.equal(sourceToWakeMode('heartbeat-wake'), 'immediate')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('infers scheduled from schedule-prefixed source', () => {
|
|
91
|
+
assert.equal(sourceToWakeMode('schedule:nightly'), 'scheduled')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('defaults to immediate for unknown sources', () => {
|
|
95
|
+
assert.equal(sourceToWakeMode('connector:slack'), 'immediate')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('createJobContext', () => {
|
|
100
|
+
it('creates an isolated context with scratchpad', () => {
|
|
101
|
+
const controller = new AbortController()
|
|
102
|
+
const ctx = createJobContext({
|
|
103
|
+
jobId: 'job-1',
|
|
104
|
+
sessionId: 'sess-1',
|
|
105
|
+
agentId: 'agent-1',
|
|
106
|
+
mode: 'immediate',
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
source: 'connector:slack',
|
|
109
|
+
reason: 'New message arrived',
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
assert.equal(ctx.jobId, 'job-1')
|
|
113
|
+
assert.equal(ctx.sessionId, 'sess-1')
|
|
114
|
+
assert.equal(ctx.agentId, 'agent-1')
|
|
115
|
+
assert.equal(ctx.mode, 'immediate')
|
|
116
|
+
assert.equal(ctx.source, 'connector:slack')
|
|
117
|
+
assert.equal(ctx.reason, 'New message arrived')
|
|
118
|
+
assert.ok(ctx.createdAt > 0)
|
|
119
|
+
assert.equal(ctx.startedAt, undefined)
|
|
120
|
+
assert.equal(ctx.endedAt, undefined)
|
|
121
|
+
assert.ok(ctx.scratchpad instanceof Map)
|
|
122
|
+
assert.equal(ctx.scratchpad.size, 0)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('scratchpad isolates state between jobs', () => {
|
|
126
|
+
const controller = new AbortController()
|
|
127
|
+
const ctx1 = createJobContext({
|
|
128
|
+
jobId: 'job-a',
|
|
129
|
+
sessionId: 'sess-1',
|
|
130
|
+
mode: 'immediate',
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
})
|
|
133
|
+
const ctx2 = createJobContext({
|
|
134
|
+
jobId: 'job-b',
|
|
135
|
+
sessionId: 'sess-1',
|
|
136
|
+
mode: 'next_heartbeat',
|
|
137
|
+
signal: controller.signal,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
ctx1.scratchpad.set('key', 'value-a')
|
|
141
|
+
ctx2.scratchpad.set('key', 'value-b')
|
|
142
|
+
|
|
143
|
+
assert.equal(ctx1.scratchpad.get('key'), 'value-a')
|
|
144
|
+
assert.equal(ctx2.scratchpad.get('key'), 'value-b')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('captures heartbeat snapshot for isolation', () => {
|
|
148
|
+
const controller = new AbortController()
|
|
149
|
+
const snapshot = '# Heartbeat Tasks\n## Active\n- [ ] Send report'
|
|
150
|
+
const ctx = createJobContext({
|
|
151
|
+
jobId: 'job-snap',
|
|
152
|
+
sessionId: 'sess-1',
|
|
153
|
+
mode: 'next_heartbeat',
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
heartbeatSnapshot: snapshot,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
assert.equal(ctx.heartbeatSnapshot, snapshot)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WakeMode — explicit scheduling semantics for heartbeat and task execution.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the implicit `source: 'heartbeat' | 'heartbeat-wake'` convention
|
|
5
|
+
* with a formal enum that determines routing, priority, and isolation behavior.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by OpenClaw's separation of "run now" vs "queue next heartbeat" vs
|
|
8
|
+
* scheduled execution with proper isolation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ── WakeMode enum ───────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export type WakeMode = 'immediate' | 'next_heartbeat' | 'scheduled'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `immediate` — Run now. Coalesced within a short window (250ms default),
|
|
17
|
+
* then dispatched. Used for connector events, watch-job
|
|
18
|
+
* triggers, approvals, webhooks.
|
|
19
|
+
*
|
|
20
|
+
* `next_heartbeat` — Queue for the next periodic heartbeat tick. No coalesce
|
|
21
|
+
* window; the job waits until the heartbeat-service timer
|
|
22
|
+
* fires. Used for low-urgency background polling, system
|
|
23
|
+
* events that don't need instant reaction.
|
|
24
|
+
*
|
|
25
|
+
* `scheduled` — Run at a specific future time (absolute or relative).
|
|
26
|
+
* Managed by the scheduler tick. Used for cron jobs,
|
|
27
|
+
* interval schedules, one-shot delayed wakes.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ── Job Context ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Isolated execution context for a wake job. Each job gets its own context
|
|
34
|
+
* so that failures, side effects, and state are contained.
|
|
35
|
+
*/
|
|
36
|
+
export interface JobContext {
|
|
37
|
+
/** Unique job execution ID. */
|
|
38
|
+
jobId: string
|
|
39
|
+
/** Which session this job targets. */
|
|
40
|
+
sessionId: string
|
|
41
|
+
/** Which agent (if any) owns this job. */
|
|
42
|
+
agentId?: string
|
|
43
|
+
/** The wake mode that created this job. */
|
|
44
|
+
mode: WakeMode
|
|
45
|
+
/** When the job was created/requested. */
|
|
46
|
+
createdAt: number
|
|
47
|
+
/** When the job actually started executing. */
|
|
48
|
+
startedAt?: number
|
|
49
|
+
/** When the job finished (success or failure). */
|
|
50
|
+
endedAt?: number
|
|
51
|
+
/** Abort controller for this specific job. */
|
|
52
|
+
signal: AbortSignal
|
|
53
|
+
/** Source identifier (e.g. 'connector:slack', 'schedule:nightly'). */
|
|
54
|
+
source?: string
|
|
55
|
+
/** Human-readable reason for this wake. */
|
|
56
|
+
reason?: string
|
|
57
|
+
/** Snapshot of HEARTBEAT.md at job start (for isolation). */
|
|
58
|
+
heartbeatSnapshot?: string
|
|
59
|
+
/** Job-scoped metadata accumulator — tools can stash results here without
|
|
60
|
+
* polluting session state until the job completes successfully. */
|
|
61
|
+
scratchpad: Map<string, unknown>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createJobContext(params: {
|
|
65
|
+
jobId: string
|
|
66
|
+
sessionId: string
|
|
67
|
+
agentId?: string
|
|
68
|
+
mode: WakeMode
|
|
69
|
+
signal: AbortSignal
|
|
70
|
+
source?: string
|
|
71
|
+
reason?: string
|
|
72
|
+
heartbeatSnapshot?: string
|
|
73
|
+
}): JobContext {
|
|
74
|
+
return {
|
|
75
|
+
jobId: params.jobId,
|
|
76
|
+
sessionId: params.sessionId,
|
|
77
|
+
agentId: params.agentId,
|
|
78
|
+
mode: params.mode,
|
|
79
|
+
createdAt: Date.now(),
|
|
80
|
+
signal: params.signal,
|
|
81
|
+
source: params.source,
|
|
82
|
+
reason: params.reason,
|
|
83
|
+
heartbeatSnapshot: params.heartbeatSnapshot,
|
|
84
|
+
scratchpad: new Map(),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Wake Request with explicit mode ─────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export interface WakeModeRequest {
|
|
91
|
+
mode: WakeMode
|
|
92
|
+
agentId?: string
|
|
93
|
+
sessionId?: string
|
|
94
|
+
reason?: string
|
|
95
|
+
source?: string
|
|
96
|
+
resumeMessage?: string
|
|
97
|
+
detail?: string
|
|
98
|
+
priority?: number
|
|
99
|
+
/** For `scheduled` mode: absolute timestamp (ms) to execute at. */
|
|
100
|
+
runAt?: number
|
|
101
|
+
/** For `scheduled` mode: relative delay (ms) from now. */
|
|
102
|
+
delayMs?: number
|
|
103
|
+
/** Event ID for deduplication. */
|
|
104
|
+
eventId?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Priority mapping per mode ───────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
const MODE_BASE_PRIORITY: Record<WakeMode, number> = {
|
|
110
|
+
immediate: 80,
|
|
111
|
+
scheduled: 60,
|
|
112
|
+
next_heartbeat: 40,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compute effective priority for a wake request. Explicit priority overrides
|
|
117
|
+
* the mode-based default; otherwise the mode determines the base.
|
|
118
|
+
*/
|
|
119
|
+
export function computeWakePriority(request: WakeModeRequest): number {
|
|
120
|
+
if (typeof request.priority === 'number' && Number.isFinite(request.priority)) {
|
|
121
|
+
return Math.max(0, Math.min(100, Math.trunc(request.priority)))
|
|
122
|
+
}
|
|
123
|
+
return MODE_BASE_PRIORITY[request.mode]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the target execution time for a wake request.
|
|
128
|
+
* - `immediate`: now (or within coalesce window)
|
|
129
|
+
* - `next_heartbeat`: null (deferred to next tick)
|
|
130
|
+
* - `scheduled`: absolute time from runAt or now + delayMs
|
|
131
|
+
*/
|
|
132
|
+
export function resolveRunAt(request: WakeModeRequest, now = Date.now()): number | null {
|
|
133
|
+
switch (request.mode) {
|
|
134
|
+
case 'immediate':
|
|
135
|
+
return now
|
|
136
|
+
case 'next_heartbeat':
|
|
137
|
+
return null
|
|
138
|
+
case 'scheduled': {
|
|
139
|
+
if (typeof request.runAt === 'number' && Number.isFinite(request.runAt)) {
|
|
140
|
+
return Math.max(now, Math.trunc(request.runAt))
|
|
141
|
+
}
|
|
142
|
+
if (typeof request.delayMs === 'number' && Number.isFinite(request.delayMs)) {
|
|
143
|
+
return now + Math.max(0, Math.trunc(request.delayMs))
|
|
144
|
+
}
|
|
145
|
+
return now
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Map a WakeMode to the session-run-manager source string.
|
|
152
|
+
* Maintains backward compatibility with existing heartbeat-source checks.
|
|
153
|
+
*/
|
|
154
|
+
export function wakeModeToSource(mode: WakeMode): string {
|
|
155
|
+
switch (mode) {
|
|
156
|
+
case 'immediate':
|
|
157
|
+
return 'heartbeat-wake'
|
|
158
|
+
case 'next_heartbeat':
|
|
159
|
+
return 'heartbeat'
|
|
160
|
+
case 'scheduled':
|
|
161
|
+
return 'heartbeat-wake'
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Infer WakeMode from a legacy source string.
|
|
167
|
+
* Used during migration to preserve backward compat with existing callers.
|
|
168
|
+
*/
|
|
169
|
+
export function sourceToWakeMode(source: string): WakeMode {
|
|
170
|
+
if (source === 'heartbeat') return 'next_heartbeat'
|
|
171
|
+
if (source === 'heartbeat-wake') return 'immediate'
|
|
172
|
+
if (source.startsWith('schedule')) return 'scheduled'
|
|
173
|
+
return 'immediate'
|
|
174
|
+
}
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
normalizeAtomicString,
|
|
10
10
|
} from '@/lib/wallet'
|
|
11
11
|
import type { Agent, AgentWallet, WalletChain, WalletTransaction } from '@/types'
|
|
12
|
-
import {
|
|
12
|
+
import { dedup } from '@/lib/shared-utils'
|
|
13
|
+
import { loadAgent, loadAgents, loadWalletTransactions, loadWallets, upsertAgent, upsertWallet } from './storage'
|
|
13
14
|
import { generateEthereumWallet, isValidEthereumAddress, sendEth } from './ethereum'
|
|
14
15
|
import { generateSolanaKeypair, isValidSolanaAddress, sendSol } from './solana'
|
|
15
16
|
import { notify } from './ws-hub'
|
|
@@ -31,7 +32,7 @@ export function getAgentWalletIds(agent: Pick<Agent, 'walletIds' | 'walletId'> |
|
|
|
31
32
|
const legacy = typeof agent?.walletId === 'string' && agent.walletId.trim()
|
|
32
33
|
? [agent.walletId.trim()]
|
|
33
34
|
: []
|
|
34
|
-
return
|
|
35
|
+
return dedup([...ids, ...legacy])
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function getAgentActiveWalletId(
|
|
@@ -44,7 +45,7 @@ export function getAgentActiveWalletId(
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function syncAgentWalletPointers(agent: Agent, walletIds: string[], activeWalletId?: string | null): Agent {
|
|
47
|
-
const normalizedIds =
|
|
48
|
+
const normalizedIds = dedup(walletIds.filter(Boolean))
|
|
48
49
|
const normalizedActive = activeWalletId && normalizedIds.includes(activeWalletId)
|
|
49
50
|
? activeWalletId
|
|
50
51
|
: normalizedIds[0] || null
|
|
@@ -102,8 +103,8 @@ export function createAgentWallet(input: {
|
|
|
102
103
|
const agentId = String(input.agentId || '').trim()
|
|
103
104
|
if (!agentId) throw new Error('agentId is required')
|
|
104
105
|
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
106
|
+
const agent = loadAgent(agentId)
|
|
107
|
+
if (!agent) throw new Error('Agent not found')
|
|
107
108
|
|
|
108
109
|
const chain = getWalletChainOrDefault(input.chain ?? input.provider, 'solana')
|
|
109
110
|
const existing = getWalletByAgentId(agentId, chain)
|
|
@@ -128,11 +129,9 @@ export function createAgentWallet(input: {
|
|
|
128
129
|
upsertWallet(id, wallet)
|
|
129
130
|
clearWalletPortfolioCache(id)
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
linkWalletToAgent(agent, id, getAgentActiveWalletId(agent) == null)
|
|
132
|
+
linkWalletToAgent(agent as any, id, getAgentActiveWalletId(agent as any) == null)
|
|
133
133
|
agent.updatedAt = now
|
|
134
|
-
|
|
135
|
-
saveAgents(agents)
|
|
134
|
+
upsertAgent(agentId, agent)
|
|
136
135
|
|
|
137
136
|
notify('wallets')
|
|
138
137
|
notify('agents')
|