@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,30 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { UPLOAD_DIR } from './storage'
|
|
4
|
+
|
|
5
|
+
const UPLOAD_URL_PREFIX = '/api/uploads/'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve an image to a valid filesystem path.
|
|
9
|
+
*
|
|
10
|
+
* Tries, in order:
|
|
11
|
+
* 1. `imagePath` (the absolute filesystem path returned by the upload API)
|
|
12
|
+
* 2. `imageUrl` mapped back to the uploads dir (e.g. `/api/uploads/foo.jpeg` → `UPLOAD_DIR/foo.jpeg`)
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` if neither resolves to an existing file.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveImagePath(imagePath?: string, imageUrl?: string): string | null {
|
|
17
|
+
if (imagePath && fs.existsSync(imagePath)) return imagePath
|
|
18
|
+
|
|
19
|
+
// Fall back: resolve relative API URL to filesystem
|
|
20
|
+
if (imageUrl?.startsWith(UPLOAD_URL_PREFIX)) {
|
|
21
|
+
const filename = imageUrl.slice(UPLOAD_URL_PREFIX.length)
|
|
22
|
+
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
23
|
+
if (safeName) {
|
|
24
|
+
const resolved = path.join(UPLOAD_DIR, safeName)
|
|
25
|
+
if (fs.existsSync(resolved)) return resolved
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
|
|
|
35
35
|
describe('runtime settings defaults', () => {
|
|
36
36
|
it('backfills explicit runtime defaults for clean installs', () => {
|
|
37
37
|
const output = runWithTempDataDir(`
|
|
38
|
-
const storageMod = await import('./src/lib/server/storage
|
|
39
|
-
const runtimeMod = await import('./src/lib/server/runtime-settings
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
39
|
+
const runtimeMod = await import('./src/lib/server/runtime-settings')
|
|
40
40
|
const storage = storageMod.default || storageMod
|
|
41
41
|
const runtime = runtimeMod.default || runtimeMod
|
|
42
42
|
console.log(JSON.stringify({
|
|
@@ -68,8 +68,8 @@ describe('runtime settings defaults', () => {
|
|
|
68
68
|
|
|
69
69
|
it('clamps invalid persisted runtime settings into the supported range', () => {
|
|
70
70
|
const output = runWithTempDataDir(`
|
|
71
|
-
const storageMod = await import('./src/lib/server/storage
|
|
72
|
-
const runtimeMod = await import('./src/lib/server/runtime-settings
|
|
71
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
72
|
+
const runtimeMod = await import('./src/lib/server/runtime-settings')
|
|
73
73
|
const storage = storageMod.default || storageMod
|
|
74
74
|
const runtime = runtimeMod.default || runtimeMod
|
|
75
75
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import test from 'node:test'
|
|
5
|
+
|
|
6
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
7
|
+
|
|
8
|
+
function readRepoSource(relativePath: string): string {
|
|
9
|
+
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf-8')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test('runtime hot paths use row-level task, schedule, and agent writes', () => {
|
|
13
|
+
const expectations = [
|
|
14
|
+
{
|
|
15
|
+
file: 'src/lib/server/queue',
|
|
16
|
+
required: ['patchTask(', 'upsertTask(', 'upsertTasks(', 'upsertSchedule(', 'deleteSchedule('],
|
|
17
|
+
forbidden: ['saveTasks(', 'saveSchedules('],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
file: 'src/lib/server/scheduler',
|
|
21
|
+
required: ['upsertTask(', 'upsertSchedule(', 'upsertSchedules('],
|
|
22
|
+
forbidden: ['saveTasks(', 'saveSchedules('],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
file: 'src/lib/server/orchestrator-lg',
|
|
26
|
+
required: ['patchTask(', 'upsertTask('],
|
|
27
|
+
forbidden: ['saveTasks('],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
file: 'src/app/api/orchestrator/run/route',
|
|
31
|
+
required: ['upsertTask('],
|
|
32
|
+
forbidden: ['saveTasks('],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
file: 'src/app/api/schedules/[id]/run/route',
|
|
36
|
+
required: ['upsertTask(', 'upsertSchedule('],
|
|
37
|
+
forbidden: ['saveTasks(', 'saveSchedules('],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
file: 'src/app/api/tasks/[id]/approve/route',
|
|
41
|
+
required: ['patchTask('],
|
|
42
|
+
forbidden: ['saveTasks('],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
file: 'src/lib/server/wallet-service',
|
|
46
|
+
required: ['loadAgent(', 'upsertAgent('],
|
|
47
|
+
forbidden: ['saveAgents('],
|
|
48
|
+
},
|
|
49
|
+
] as const
|
|
50
|
+
|
|
51
|
+
for (const expectation of expectations) {
|
|
52
|
+
const src = readRepoSource(expectation.file)
|
|
53
|
+
for (const token of expectation.required) {
|
|
54
|
+
assert.equal(src.includes(token), true, `${expectation.file} should use ${token}`)
|
|
55
|
+
}
|
|
56
|
+
for (const token of expectation.forbidden) {
|
|
57
|
+
assert.equal(src.includes(token), false, `${expectation.file} should not use ${token}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let workspaceDir = ''
|
|
15
|
+
let mod: typeof import('./schedule-normalization')
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-schedule-norm-'))
|
|
19
|
+
workspaceDir = path.join(tempDir, 'workspace')
|
|
20
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
21
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
22
|
+
process.env.WORKSPACE_DIR = workspaceDir
|
|
23
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
24
|
+
mod = await import('./schedule-normalization')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
after(() => {
|
|
28
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
29
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
30
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
31
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
32
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
33
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
34
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('extractScheduleCommandScriptPath', () => {
|
|
38
|
+
it('extracts script path from python command', () => {
|
|
39
|
+
assert.equal(mod.extractScheduleCommandScriptPath('python3 scripts/run.py'), 'scripts/run.py')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('extracts script path from node command', () => {
|
|
43
|
+
assert.equal(mod.extractScheduleCommandScriptPath('node ./build/index.js'), './build/index.js')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('extracts script path from bash command', () => {
|
|
47
|
+
assert.equal(mod.extractScheduleCommandScriptPath('bash deploy.sh'), 'deploy.sh')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('extracts script path from npx tsx command', () => {
|
|
51
|
+
assert.equal(mod.extractScheduleCommandScriptPath('npx tsx src/worker.ts'), 'src/worker.ts')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('extracts script path from deno run command', () => {
|
|
55
|
+
assert.equal(mod.extractScheduleCommandScriptPath('deno run main.ts'), 'main.ts')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('skips flags before finding path', () => {
|
|
59
|
+
assert.equal(mod.extractScheduleCommandScriptPath('python3 -u scripts/run.py'), 'scripts/run.py')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns null for bare command with no script-like argument', () => {
|
|
63
|
+
assert.equal(mod.extractScheduleCommandScriptPath('echo hello'), null)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('returns null for empty command', () => {
|
|
67
|
+
assert.equal(mod.extractScheduleCommandScriptPath(''), null)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('handles quoted paths', () => {
|
|
71
|
+
assert.equal(mod.extractScheduleCommandScriptPath('python3 "my script.py"'), 'my script.py')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('normalizeSchedulePayload', () => {
|
|
76
|
+
it('rejects missing agentId', () => {
|
|
77
|
+
const result = mod.normalizeSchedulePayload({ taskPrompt: 'do stuff' })
|
|
78
|
+
assert.equal(result.ok, false)
|
|
79
|
+
if (!result.ok) assert.match(result.error, /agentId/)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('rejects missing taskPrompt/command/path', () => {
|
|
83
|
+
const result = mod.normalizeSchedulePayload({ agentId: 'agent-1' })
|
|
84
|
+
assert.equal(result.ok, false)
|
|
85
|
+
if (!result.ok) assert.match(result.error, /taskPrompt/)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('accepts valid payload with taskPrompt', () => {
|
|
89
|
+
const result = mod.normalizeSchedulePayload({
|
|
90
|
+
agentId: 'agent-1',
|
|
91
|
+
taskPrompt: 'Run the daily report',
|
|
92
|
+
})
|
|
93
|
+
assert.equal(result.ok, true)
|
|
94
|
+
if (result.ok) {
|
|
95
|
+
assert.equal(result.value.agentId, 'agent-1')
|
|
96
|
+
assert.equal(result.value.taskPrompt, 'Run the daily report')
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('derives taskPrompt from command when not explicit', () => {
|
|
101
|
+
const result = mod.normalizeSchedulePayload({
|
|
102
|
+
agentId: 'agent-1',
|
|
103
|
+
command: 'echo hello',
|
|
104
|
+
})
|
|
105
|
+
assert.equal(result.ok, true)
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
assert.match(result.value.taskPrompt as string, /echo hello/)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('normalizes scheduleType to interval by default', () => {
|
|
112
|
+
const result = mod.normalizeSchedulePayload({
|
|
113
|
+
agentId: 'agent-1',
|
|
114
|
+
taskPrompt: 'test',
|
|
115
|
+
})
|
|
116
|
+
assert.equal(result.ok, true)
|
|
117
|
+
if (result.ok) assert.equal(result.value.scheduleType, 'interval')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('preserves valid scheduleType values', () => {
|
|
121
|
+
for (const t of ['cron', 'interval', 'once'] as const) {
|
|
122
|
+
const result = mod.normalizeSchedulePayload({
|
|
123
|
+
agentId: 'agent-1',
|
|
124
|
+
taskPrompt: 'test',
|
|
125
|
+
scheduleType: t,
|
|
126
|
+
})
|
|
127
|
+
assert.equal(result.ok, true)
|
|
128
|
+
if (result.ok) assert.equal(result.value.scheduleType, t)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('falls back invalid scheduleType to interval', () => {
|
|
133
|
+
const result = mod.normalizeSchedulePayload({
|
|
134
|
+
agentId: 'agent-1',
|
|
135
|
+
taskPrompt: 'test',
|
|
136
|
+
scheduleType: 'bogus',
|
|
137
|
+
})
|
|
138
|
+
assert.equal(result.ok, true)
|
|
139
|
+
if (result.ok) assert.equal(result.value.scheduleType, 'interval')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('normalizes status to active for unknown values', () => {
|
|
143
|
+
const result = mod.normalizeSchedulePayload({
|
|
144
|
+
agentId: 'agent-1',
|
|
145
|
+
taskPrompt: 'test',
|
|
146
|
+
status: 'invalid-status',
|
|
147
|
+
})
|
|
148
|
+
assert.equal(result.ok, true)
|
|
149
|
+
if (result.ok) assert.equal(result.value.status, 'active')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('preserves valid status values', () => {
|
|
153
|
+
for (const s of ['active', 'paused', 'completed', 'failed']) {
|
|
154
|
+
const result = mod.normalizeSchedulePayload({
|
|
155
|
+
agentId: 'agent-1',
|
|
156
|
+
taskPrompt: 'test',
|
|
157
|
+
status: s,
|
|
158
|
+
})
|
|
159
|
+
assert.equal(result.ok, true)
|
|
160
|
+
if (result.ok) assert.equal(result.value.status, s)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('calculates nextRunAt for interval type when intervalMs is set', () => {
|
|
165
|
+
const now = 1_000_000
|
|
166
|
+
const result = mod.normalizeSchedulePayload(
|
|
167
|
+
{ agentId: 'agent-1', taskPrompt: 'test', scheduleType: 'interval', intervalMs: 5000 },
|
|
168
|
+
{ now },
|
|
169
|
+
)
|
|
170
|
+
assert.equal(result.ok, true)
|
|
171
|
+
if (result.ok) assert.equal(result.value.nextRunAt, 1_005_000)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('uses runAt for once type', () => {
|
|
175
|
+
const result = mod.normalizeSchedulePayload(
|
|
176
|
+
{ agentId: 'agent-1', taskPrompt: 'test', scheduleType: 'once', runAt: 9_999_999 },
|
|
177
|
+
{ now: 1_000_000 },
|
|
178
|
+
)
|
|
179
|
+
assert.equal(result.ok, true)
|
|
180
|
+
if (result.ok) assert.equal(result.value.nextRunAt, 9_999_999)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('does not overwrite existing nextRunAt', () => {
|
|
184
|
+
const result = mod.normalizeSchedulePayload(
|
|
185
|
+
{ agentId: 'agent-1', taskPrompt: 'test', scheduleType: 'interval', intervalMs: 5000, nextRunAt: 42 },
|
|
186
|
+
{ now: 1_000_000 },
|
|
187
|
+
)
|
|
188
|
+
assert.equal(result.ok, true)
|
|
189
|
+
if (result.ok) assert.equal(result.value.nextRunAt, 42)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('trims whitespace from agentId', () => {
|
|
193
|
+
const result = mod.normalizeSchedulePayload({
|
|
194
|
+
agentId: ' agent-1 ',
|
|
195
|
+
taskPrompt: 'test',
|
|
196
|
+
})
|
|
197
|
+
assert.equal(result.ok, true)
|
|
198
|
+
if (result.ok) assert.equal(result.value.agentId, 'agent-1')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('rejects run_script action without path', () => {
|
|
202
|
+
const result = mod.normalizeSchedulePayload({
|
|
203
|
+
agentId: 'agent-1',
|
|
204
|
+
taskPrompt: 'test',
|
|
205
|
+
action: 'run_script',
|
|
206
|
+
})
|
|
207
|
+
assert.equal(result.ok, false)
|
|
208
|
+
if (!result.ok) assert.match(result.error, /run_script/)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('rejects path outside workspace', () => {
|
|
212
|
+
const result = mod.normalizeSchedulePayload(
|
|
213
|
+
{ agentId: 'agent-1', taskPrompt: 'test', path: '/etc/passwd' },
|
|
214
|
+
{ cwd: workspaceDir },
|
|
215
|
+
)
|
|
216
|
+
assert.equal(result.ok, false)
|
|
217
|
+
if (!result.ok) assert.match(result.error, /must stay inside/)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('validates path exists for file-based schedules', () => {
|
|
221
|
+
const result = mod.normalizeSchedulePayload(
|
|
222
|
+
{ agentId: 'agent-1', taskPrompt: 'test', path: 'nonexistent.py' },
|
|
223
|
+
{ cwd: workspaceDir },
|
|
224
|
+
)
|
|
225
|
+
assert.equal(result.ok, false)
|
|
226
|
+
if (!result.ok) assert.match(result.error, /not found/)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('accepts path that exists inside workspace', () => {
|
|
230
|
+
const scriptPath = path.join(workspaceDir, 'test-script.py')
|
|
231
|
+
fs.writeFileSync(scriptPath, '#!/usr/bin/env python3\nprint("ok")')
|
|
232
|
+
const result = mod.normalizeSchedulePayload(
|
|
233
|
+
{ agentId: 'agent-1', taskPrompt: 'test', path: 'test-script.py' },
|
|
234
|
+
{ cwd: workspaceDir },
|
|
235
|
+
)
|
|
236
|
+
assert.equal(result.ok, true)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('derives taskPrompt from path with run_script action', () => {
|
|
240
|
+
const scriptPath = path.join(workspaceDir, 'runner.sh')
|
|
241
|
+
fs.writeFileSync(scriptPath, '#!/bin/bash\necho ok')
|
|
242
|
+
const result = mod.normalizeSchedulePayload(
|
|
243
|
+
{ agentId: 'agent-1', action: 'run_script', path: 'runner.sh' },
|
|
244
|
+
{ cwd: workspaceDir },
|
|
245
|
+
)
|
|
246
|
+
assert.equal(result.ok, true)
|
|
247
|
+
if (result.ok) assert.match(result.value.taskPrompt as string, /Run the script/)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('derives taskPrompt from path without specific action', () => {
|
|
251
|
+
const filePath = path.join(workspaceDir, 'data.csv')
|
|
252
|
+
fs.writeFileSync(filePath, 'a,b\n1,2')
|
|
253
|
+
const result = mod.normalizeSchedulePayload(
|
|
254
|
+
{ agentId: 'agent-1', path: 'data.csv' },
|
|
255
|
+
{ cwd: workspaceDir },
|
|
256
|
+
)
|
|
257
|
+
assert.equal(result.ok, true)
|
|
258
|
+
if (result.ok) assert.match(result.value.taskPrompt as string, /Use the file/)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('rejects command referencing a missing script file', () => {
|
|
262
|
+
const result = mod.normalizeSchedulePayload(
|
|
263
|
+
{ agentId: 'agent-1', taskPrompt: 'test', command: 'python3 missing_script.py' },
|
|
264
|
+
{ cwd: workspaceDir },
|
|
265
|
+
)
|
|
266
|
+
assert.equal(result.ok, false)
|
|
267
|
+
if (!result.ok) assert.match(result.error, /missing file/)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('accepts command referencing an existing script file', () => {
|
|
271
|
+
const scriptPath = path.join(workspaceDir, 'existing.py')
|
|
272
|
+
fs.writeFileSync(scriptPath, 'print("ok")')
|
|
273
|
+
const result = mod.normalizeSchedulePayload(
|
|
274
|
+
{ agentId: 'agent-1', taskPrompt: 'test', command: 'python3 existing.py' },
|
|
275
|
+
{ cwd: workspaceDir },
|
|
276
|
+
)
|
|
277
|
+
assert.equal(result.ok, true)
|
|
278
|
+
})
|
|
279
|
+
})
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import {
|
|
3
|
+
findDuplicateSchedule,
|
|
4
|
+
findEquivalentSchedules,
|
|
5
|
+
type ScheduleLike,
|
|
6
|
+
} from '@/lib/schedule-dedupe'
|
|
7
|
+
import { resolveScheduleName } from '@/lib/schedule-name'
|
|
8
|
+
import type { Schedule, ScheduleStatus } from '@/types'
|
|
9
|
+
import { dedup } from '@/lib/shared-utils'
|
|
10
|
+
|
|
11
|
+
import { normalizeSchedulePayload } from './schedule-normalization'
|
|
12
|
+
|
|
13
|
+
export interface ScheduleCreatorScope {
|
|
14
|
+
agentId?: string | null
|
|
15
|
+
sessionId?: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildScheduleCreatorScope(schedule: Record<string, unknown> | null | undefined): ScheduleCreatorScope | null {
|
|
19
|
+
if (!schedule || typeof schedule !== 'object') return null
|
|
20
|
+
const agentId = typeof schedule.createdByAgentId === 'string' && schedule.createdByAgentId.trim()
|
|
21
|
+
? schedule.createdByAgentId.trim()
|
|
22
|
+
: null
|
|
23
|
+
const sessionId = typeof schedule.createdInSessionId === 'string' && schedule.createdInSessionId.trim()
|
|
24
|
+
? schedule.createdInSessionId.trim()
|
|
25
|
+
: null
|
|
26
|
+
if (!agentId && !sessionId) return null
|
|
27
|
+
return { agentId, sessionId }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function findRelatedScheduleIds(
|
|
31
|
+
schedules: Record<string, ScheduleLike>,
|
|
32
|
+
schedule: Record<string, unknown> | null | undefined,
|
|
33
|
+
opts: { ignoreId?: string | null } = {},
|
|
34
|
+
): string[] {
|
|
35
|
+
if (!schedule || typeof schedule !== 'object') return []
|
|
36
|
+
const scope = buildScheduleCreatorScope(schedule)
|
|
37
|
+
if (!scope?.sessionId) return []
|
|
38
|
+
const matches = findEquivalentSchedules(schedules, {
|
|
39
|
+
id: typeof schedule.id === 'string' ? schedule.id : null,
|
|
40
|
+
agentId: typeof schedule.agentId === 'string' ? schedule.agentId : null,
|
|
41
|
+
taskPrompt: typeof schedule.taskPrompt === 'string' ? schedule.taskPrompt : null,
|
|
42
|
+
scheduleType: typeof schedule.scheduleType === 'string' ? schedule.scheduleType : null,
|
|
43
|
+
cron: typeof schedule.cron === 'string' ? schedule.cron : null,
|
|
44
|
+
intervalMs: typeof schedule.intervalMs === 'number' ? schedule.intervalMs : null,
|
|
45
|
+
runAt: typeof schedule.runAt === 'number' ? schedule.runAt : null,
|
|
46
|
+
createdByAgentId: scope.agentId,
|
|
47
|
+
createdInSessionId: scope.sessionId,
|
|
48
|
+
}, {
|
|
49
|
+
ignoreId: opts.ignoreId || (typeof schedule.id === 'string' ? schedule.id : null),
|
|
50
|
+
creatorScope: scope,
|
|
51
|
+
})
|
|
52
|
+
return dedup(matches
|
|
53
|
+
.map((entry) => (typeof entry.id === 'string' ? entry.id : ''))
|
|
54
|
+
.filter(Boolean))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getScheduleClusterIds(
|
|
58
|
+
schedules: Record<string, ScheduleLike>,
|
|
59
|
+
schedule: Record<string, unknown> | null | undefined,
|
|
60
|
+
opts: { ignoreId?: string | null } = {},
|
|
61
|
+
): string[] {
|
|
62
|
+
const id = typeof schedule?.id === 'string' ? schedule.id : ''
|
|
63
|
+
const relatedIds = findRelatedScheduleIds(schedules, schedule, {
|
|
64
|
+
ignoreId: opts.ignoreId || id || null,
|
|
65
|
+
})
|
|
66
|
+
const ids = [
|
|
67
|
+
...(!opts.ignoreId && id ? [id] : []),
|
|
68
|
+
...relatedIds,
|
|
69
|
+
]
|
|
70
|
+
return dedup(ids.filter(Boolean))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeScheduleStatus(value: unknown): ScheduleStatus | '' {
|
|
74
|
+
if (typeof value !== 'string') return ''
|
|
75
|
+
const normalized = value.trim().toLowerCase()
|
|
76
|
+
return normalized === 'active'
|
|
77
|
+
|| normalized === 'paused'
|
|
78
|
+
|| normalized === 'completed'
|
|
79
|
+
|| normalized === 'failed'
|
|
80
|
+
? normalized
|
|
81
|
+
: ''
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PrepareScheduleCreateOptions {
|
|
85
|
+
input: Record<string, unknown>
|
|
86
|
+
schedules: Record<string, ScheduleLike>
|
|
87
|
+
now: number
|
|
88
|
+
cwd?: string | null
|
|
89
|
+
creatorScope?: ScheduleCreatorScope | null
|
|
90
|
+
dedupeCreatorScope?: ScheduleCreatorScope | null
|
|
91
|
+
followupTarget?: Partial<Schedule>
|
|
92
|
+
createId?: () => string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type PrepareScheduleCreateResult =
|
|
96
|
+
| { ok: false; error: string }
|
|
97
|
+
| {
|
|
98
|
+
ok: true
|
|
99
|
+
kind: 'duplicate'
|
|
100
|
+
scheduleId: string
|
|
101
|
+
schedule: ScheduleLike
|
|
102
|
+
changed: boolean
|
|
103
|
+
entries: Array<[string, ScheduleLike]>
|
|
104
|
+
}
|
|
105
|
+
| {
|
|
106
|
+
ok: true
|
|
107
|
+
kind: 'created'
|
|
108
|
+
scheduleId: string
|
|
109
|
+
schedule: Schedule
|
|
110
|
+
entries: Array<[string, Schedule]>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function prepareScheduleCreate(options: PrepareScheduleCreateOptions): PrepareScheduleCreateResult {
|
|
114
|
+
const normalized = normalizeSchedulePayload(options.input, {
|
|
115
|
+
cwd: options.cwd,
|
|
116
|
+
now: options.now,
|
|
117
|
+
})
|
|
118
|
+
if (!normalized.ok) return { ok: false, error: normalized.error }
|
|
119
|
+
|
|
120
|
+
const candidate = normalized.value
|
|
121
|
+
const dedupeScope = options.dedupeCreatorScope || null
|
|
122
|
+
const duplicate = findDuplicateSchedule(options.schedules, {
|
|
123
|
+
agentId: typeof candidate.agentId === 'string' ? candidate.agentId : null,
|
|
124
|
+
taskPrompt: typeof candidate.taskPrompt === 'string' ? candidate.taskPrompt : '',
|
|
125
|
+
scheduleType: typeof candidate.scheduleType === 'string' ? candidate.scheduleType : 'interval',
|
|
126
|
+
cron: typeof candidate.cron === 'string' ? candidate.cron : null,
|
|
127
|
+
intervalMs: typeof candidate.intervalMs === 'number' ? candidate.intervalMs : null,
|
|
128
|
+
runAt: typeof candidate.runAt === 'number' ? candidate.runAt : null,
|
|
129
|
+
createdByAgentId: dedupeScope?.agentId || null,
|
|
130
|
+
createdInSessionId: dedupeScope?.sessionId || null,
|
|
131
|
+
}, dedupeScope
|
|
132
|
+
? { creatorScope: dedupeScope }
|
|
133
|
+
: undefined)
|
|
134
|
+
|
|
135
|
+
if (duplicate) {
|
|
136
|
+
const duplicateId = typeof duplicate.id === 'string' ? duplicate.id : ''
|
|
137
|
+
const nextSchedule = { ...duplicate }
|
|
138
|
+
let changed = false
|
|
139
|
+
const nextName = resolveScheduleName({
|
|
140
|
+
name: candidate.name ?? nextSchedule.name,
|
|
141
|
+
taskPrompt: candidate.taskPrompt ?? nextSchedule.taskPrompt,
|
|
142
|
+
})
|
|
143
|
+
if (nextName && nextName !== nextSchedule.name) {
|
|
144
|
+
nextSchedule.name = nextName
|
|
145
|
+
changed = true
|
|
146
|
+
}
|
|
147
|
+
const nextStatus = normalizeScheduleStatus(candidate.status)
|
|
148
|
+
if ((nextStatus === 'active' || nextStatus === 'paused') && nextSchedule.status !== nextStatus) {
|
|
149
|
+
nextSchedule.status = nextStatus
|
|
150
|
+
changed = true
|
|
151
|
+
}
|
|
152
|
+
if (changed) nextSchedule.updatedAt = options.now
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
kind: 'duplicate',
|
|
156
|
+
scheduleId: duplicateId,
|
|
157
|
+
schedule: nextSchedule,
|
|
158
|
+
changed,
|
|
159
|
+
entries: changed && duplicateId ? [[duplicateId, nextSchedule]] : [],
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const id = options.createId ? options.createId() : genId()
|
|
164
|
+
const creatorFields = options.creatorScope
|
|
165
|
+
? {
|
|
166
|
+
createdByAgentId: options.creatorScope.agentId || null,
|
|
167
|
+
createdInSessionId: options.creatorScope.sessionId || null,
|
|
168
|
+
}
|
|
169
|
+
: {}
|
|
170
|
+
const schedule = {
|
|
171
|
+
id,
|
|
172
|
+
...candidate,
|
|
173
|
+
...creatorFields,
|
|
174
|
+
...(options.followupTarget || {}),
|
|
175
|
+
name: resolveScheduleName({ name: candidate.name, taskPrompt: candidate.taskPrompt }),
|
|
176
|
+
scheduleType: candidate.scheduleType === 'cron' || candidate.scheduleType === 'once' ? candidate.scheduleType : 'interval',
|
|
177
|
+
lastRunAt: undefined,
|
|
178
|
+
createdAt: options.now,
|
|
179
|
+
updatedAt: options.now,
|
|
180
|
+
} as Schedule
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
kind: 'created',
|
|
185
|
+
scheduleId: id,
|
|
186
|
+
schedule,
|
|
187
|
+
entries: [[id, schedule]],
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface PrepareScheduleUpdateOptions {
|
|
192
|
+
id: string
|
|
193
|
+
current: ScheduleLike
|
|
194
|
+
patch: Record<string, unknown>
|
|
195
|
+
schedules: Record<string, ScheduleLike>
|
|
196
|
+
now: number
|
|
197
|
+
cwd?: string | null
|
|
198
|
+
agentExists?: (agentId: string) => boolean
|
|
199
|
+
propagateEquivalentStatuses?: boolean
|
|
200
|
+
propagationSource?: Record<string, unknown> | null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type PrepareScheduleUpdateResult =
|
|
204
|
+
| { ok: false; error: string }
|
|
205
|
+
| {
|
|
206
|
+
ok: true
|
|
207
|
+
schedule: ScheduleLike
|
|
208
|
+
entries: Array<[string, ScheduleLike]>
|
|
209
|
+
affectedScheduleIds: string[]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function prepareScheduleUpdate(options: PrepareScheduleUpdateOptions): PrepareScheduleUpdateResult {
|
|
213
|
+
const normalized = normalizeSchedulePayload({
|
|
214
|
+
...options.current,
|
|
215
|
+
...options.patch,
|
|
216
|
+
id: options.id,
|
|
217
|
+
}, {
|
|
218
|
+
cwd: options.cwd,
|
|
219
|
+
now: options.now,
|
|
220
|
+
})
|
|
221
|
+
if (!normalized.ok) return { ok: false, error: normalized.error }
|
|
222
|
+
|
|
223
|
+
const nextSchedule = {
|
|
224
|
+
...options.current,
|
|
225
|
+
...normalized.value,
|
|
226
|
+
id: options.id,
|
|
227
|
+
updatedAt: options.now,
|
|
228
|
+
}
|
|
229
|
+
const agentId = typeof nextSchedule.agentId === 'string' ? nextSchedule.agentId : ''
|
|
230
|
+
if (options.agentExists && (!agentId || !options.agentExists(agentId))) {
|
|
231
|
+
return { ok: false, error: `Agent not found: ${String(nextSchedule.agentId)}` }
|
|
232
|
+
}
|
|
233
|
+
nextSchedule.name = resolveScheduleName({
|
|
234
|
+
name: nextSchedule.name,
|
|
235
|
+
taskPrompt: nextSchedule.taskPrompt,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const entries: Array<[string, ScheduleLike]> = [[options.id, nextSchedule]]
|
|
239
|
+
const nextStatus = normalizeScheduleStatus(nextSchedule.status)
|
|
240
|
+
if (options.propagateEquivalentStatuses && (nextStatus === 'paused' || nextStatus === 'completed' || nextStatus === 'failed')) {
|
|
241
|
+
const relatedIds = findRelatedScheduleIds(
|
|
242
|
+
options.schedules,
|
|
243
|
+
options.propagationSource || options.current,
|
|
244
|
+
{ ignoreId: options.id },
|
|
245
|
+
)
|
|
246
|
+
for (const relatedId of relatedIds) {
|
|
247
|
+
const related = options.schedules[relatedId]
|
|
248
|
+
if (!related) continue
|
|
249
|
+
entries.push([relatedId, {
|
|
250
|
+
...related,
|
|
251
|
+
status: nextStatus,
|
|
252
|
+
updatedAt: options.now,
|
|
253
|
+
}])
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
schedule: nextSchedule,
|
|
260
|
+
entries,
|
|
261
|
+
affectedScheduleIds: dedup(entries.map(([id]) => id)),
|
|
262
|
+
}
|
|
263
|
+
}
|