@swarmclawai/swarmclaw 0.2.0
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 +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Message, ProviderType } from '@/types'
|
|
2
|
+
import { getMemoryDb } from './memory-db'
|
|
3
|
+
|
|
4
|
+
// --- Context window sizes (tokens) per provider/model ---
|
|
5
|
+
|
|
6
|
+
const PROVIDER_CONTEXT_WINDOWS: Record<string, number> = {
|
|
7
|
+
// Anthropic
|
|
8
|
+
'claude-opus-4-6': 200_000,
|
|
9
|
+
'claude-sonnet-4-6': 200_000,
|
|
10
|
+
'claude-haiku-4-5-20251001': 200_000,
|
|
11
|
+
'claude-sonnet-4-5-20250514': 200_000,
|
|
12
|
+
// OpenAI
|
|
13
|
+
'gpt-4o': 128_000,
|
|
14
|
+
'gpt-4o-mini': 128_000,
|
|
15
|
+
'gpt-4.1': 1_047_576,
|
|
16
|
+
'gpt-4.1-mini': 1_047_576,
|
|
17
|
+
'gpt-4.1-nano': 1_047_576,
|
|
18
|
+
'o3': 200_000,
|
|
19
|
+
'o3-mini': 128_000,
|
|
20
|
+
'o4-mini': 200_000,
|
|
21
|
+
// Codex CLI
|
|
22
|
+
'gpt-5.3-codex': 1_047_576,
|
|
23
|
+
'gpt-5.2-codex': 1_047_576,
|
|
24
|
+
'gpt-5.1-codex': 1_047_576,
|
|
25
|
+
'gpt-5-codex': 1_047_576,
|
|
26
|
+
'gpt-5-codex-mini': 1_047_576,
|
|
27
|
+
// Google Gemini
|
|
28
|
+
'gemini-2.5-pro': 1_048_576,
|
|
29
|
+
'gemini-2.5-flash': 1_048_576,
|
|
30
|
+
'gemini-2.5-flash-lite': 1_048_576,
|
|
31
|
+
// DeepSeek
|
|
32
|
+
'deepseek-chat': 64_000,
|
|
33
|
+
'deepseek-reasoner': 64_000,
|
|
34
|
+
// Mistral
|
|
35
|
+
'mistral-large-latest': 128_000,
|
|
36
|
+
'mistral-small-latest': 128_000,
|
|
37
|
+
'magistral-medium-2506': 128_000,
|
|
38
|
+
'devstral-small-latest': 128_000,
|
|
39
|
+
// xAI
|
|
40
|
+
'grok-3': 131_072,
|
|
41
|
+
'grok-3-fast': 131_072,
|
|
42
|
+
'grok-3-mini': 131_072,
|
|
43
|
+
'grok-3-mini-fast': 131_072,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PROVIDER_DEFAULT_WINDOWS: Record<string, number> = {
|
|
47
|
+
anthropic: 200_000,
|
|
48
|
+
'claude-cli': 200_000,
|
|
49
|
+
openai: 128_000,
|
|
50
|
+
'codex-cli': 1_047_576,
|
|
51
|
+
'opencode-cli': 200_000,
|
|
52
|
+
google: 1_048_576,
|
|
53
|
+
deepseek: 64_000,
|
|
54
|
+
groq: 32_768,
|
|
55
|
+
together: 32_768,
|
|
56
|
+
mistral: 128_000,
|
|
57
|
+
xai: 131_072,
|
|
58
|
+
fireworks: 32_768,
|
|
59
|
+
ollama: 8_192,
|
|
60
|
+
openclaw: 128_000,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get context window size for a model, falling back to provider default */
|
|
64
|
+
export function getContextWindowSize(provider: string, model: string): number {
|
|
65
|
+
return PROVIDER_CONTEXT_WINDOWS[model]
|
|
66
|
+
|| PROVIDER_DEFAULT_WINDOWS[provider]
|
|
67
|
+
|| 8_192
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Token estimation ---
|
|
71
|
+
|
|
72
|
+
/** Rough token estimate: ~4 chars per token for English text */
|
|
73
|
+
export function estimateTokens(text: string): number {
|
|
74
|
+
if (!text) return 0
|
|
75
|
+
return Math.ceil(text.length / 4)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Estimate total tokens for a message array */
|
|
79
|
+
export function estimateMessagesTokens(messages: Message[]): number {
|
|
80
|
+
let total = 0
|
|
81
|
+
for (const m of messages) {
|
|
82
|
+
// Role + overhead per message (~4 tokens)
|
|
83
|
+
total += 4
|
|
84
|
+
total += estimateTokens(m.text)
|
|
85
|
+
if (m.toolEvents) {
|
|
86
|
+
for (const te of m.toolEvents) {
|
|
87
|
+
total += estimateTokens(te.name) + estimateTokens(te.input)
|
|
88
|
+
if (te.output) total += estimateTokens(te.output)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return total
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Context status ---
|
|
96
|
+
|
|
97
|
+
export interface ContextStatus {
|
|
98
|
+
estimatedTokens: number
|
|
99
|
+
contextWindow: number
|
|
100
|
+
percentUsed: number
|
|
101
|
+
messageCount: number
|
|
102
|
+
strategy: 'ok' | 'warning' | 'critical'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getContextStatus(
|
|
106
|
+
messages: Message[],
|
|
107
|
+
systemPromptTokens: number,
|
|
108
|
+
provider: string,
|
|
109
|
+
model: string,
|
|
110
|
+
): ContextStatus {
|
|
111
|
+
const contextWindow = getContextWindowSize(provider, model)
|
|
112
|
+
const messageTokens = estimateMessagesTokens(messages)
|
|
113
|
+
const estimatedTokens = messageTokens + systemPromptTokens
|
|
114
|
+
const percentUsed = Math.round((estimatedTokens / contextWindow) * 100)
|
|
115
|
+
return {
|
|
116
|
+
estimatedTokens,
|
|
117
|
+
contextWindow,
|
|
118
|
+
percentUsed,
|
|
119
|
+
messageCount: messages.length,
|
|
120
|
+
strategy: percentUsed >= 90 ? 'critical' : percentUsed >= 70 ? 'warning' : 'ok',
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Memory consolidation ---
|
|
125
|
+
|
|
126
|
+
/** Extract important facts from old messages before pruning */
|
|
127
|
+
export function consolidateToMemory(
|
|
128
|
+
messages: Message[],
|
|
129
|
+
agentId: string | null,
|
|
130
|
+
sessionId: string,
|
|
131
|
+
): number {
|
|
132
|
+
if (!agentId) return 0
|
|
133
|
+
const db = getMemoryDb()
|
|
134
|
+
let stored = 0
|
|
135
|
+
|
|
136
|
+
for (const m of messages) {
|
|
137
|
+
if (m.role !== 'assistant' || !m.text) continue
|
|
138
|
+
// Look for decisions, commitments, key facts
|
|
139
|
+
const text = m.text
|
|
140
|
+
const hasDecision = /\b(decided|decision|agreed|committed|will do|plan is|approach is|chosen|selected)\b/i.test(text)
|
|
141
|
+
const hasKeyFact = /\b(important|critical|note|remember|key point|constraint|requirement|deadline)\b/i.test(text)
|
|
142
|
+
const hasResult = /\b(result|found|discovered|concluded|completed|built|created|deployed)\b/i.test(text)
|
|
143
|
+
|
|
144
|
+
if (hasDecision || hasKeyFact || hasResult) {
|
|
145
|
+
// Create a concise summary (first 500 chars)
|
|
146
|
+
const summary = text.length > 500 ? text.slice(0, 500) + '...' : text
|
|
147
|
+
const category = hasDecision ? 'decision' : hasResult ? 'result' : 'note'
|
|
148
|
+
const title = `[auto-consolidated] ${text.slice(0, 60).replace(/\n/g, ' ')}`
|
|
149
|
+
|
|
150
|
+
db.add({
|
|
151
|
+
agentId,
|
|
152
|
+
sessionId,
|
|
153
|
+
category,
|
|
154
|
+
title,
|
|
155
|
+
content: summary,
|
|
156
|
+
})
|
|
157
|
+
stored++
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return stored
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Compaction strategies ---
|
|
164
|
+
|
|
165
|
+
export interface CompactionResult {
|
|
166
|
+
messages: Message[]
|
|
167
|
+
prunedCount: number
|
|
168
|
+
memoriesStored: number
|
|
169
|
+
summaryAdded: boolean
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Sliding window: keep last N messages */
|
|
173
|
+
export function slidingWindowCompact(
|
|
174
|
+
messages: Message[],
|
|
175
|
+
keepLastN: number,
|
|
176
|
+
): Message[] {
|
|
177
|
+
if (messages.length <= keepLastN) return messages
|
|
178
|
+
return messages.slice(-keepLastN)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Summarize old messages, keep recent ones */
|
|
182
|
+
export async function summarizeAndCompact(opts: {
|
|
183
|
+
messages: Message[]
|
|
184
|
+
keepLastN: number
|
|
185
|
+
agentId: string | null
|
|
186
|
+
sessionId: string
|
|
187
|
+
generateSummary: (text: string) => Promise<string>
|
|
188
|
+
}): Promise<CompactionResult> {
|
|
189
|
+
const { messages, keepLastN, agentId, sessionId, generateSummary } = opts
|
|
190
|
+
if (messages.length <= keepLastN) {
|
|
191
|
+
return { messages, prunedCount: 0, memoriesStored: 0, summaryAdded: false }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const oldMessages = messages.slice(0, -keepLastN)
|
|
195
|
+
const recentMessages = messages.slice(-keepLastN)
|
|
196
|
+
|
|
197
|
+
// Consolidate important info to memory before pruning
|
|
198
|
+
const memoriesStored = consolidateToMemory(oldMessages, agentId, sessionId)
|
|
199
|
+
|
|
200
|
+
// Build text for summarization
|
|
201
|
+
const conversationText = oldMessages
|
|
202
|
+
.map((m) => `${m.role}: ${m.text}`)
|
|
203
|
+
.join('\n\n')
|
|
204
|
+
|
|
205
|
+
const summary = await generateSummary(conversationText)
|
|
206
|
+
|
|
207
|
+
const summaryMessage: Message = {
|
|
208
|
+
role: 'assistant',
|
|
209
|
+
text: `[Context Summary]\n${summary}`,
|
|
210
|
+
time: Date.now(),
|
|
211
|
+
kind: 'system',
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
messages: [summaryMessage, ...recentMessages],
|
|
216
|
+
prunedCount: oldMessages.length,
|
|
217
|
+
memoriesStored,
|
|
218
|
+
summaryAdded: true,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Auto-compact: triggers when estimated tokens exceed threshold */
|
|
223
|
+
export function shouldAutoCompact(
|
|
224
|
+
messages: Message[],
|
|
225
|
+
systemPromptTokens: number,
|
|
226
|
+
provider: string,
|
|
227
|
+
model: string,
|
|
228
|
+
triggerPercent = 80,
|
|
229
|
+
): boolean {
|
|
230
|
+
const status = getContextStatus(messages, systemPromptTokens, provider, model)
|
|
231
|
+
return status.percentUsed >= triggerPercent
|
|
232
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Model cost table: [inputCostPer1M, outputCostPer1M] in USD
|
|
2
|
+
const MODEL_COSTS: Record<string, [number, number]> = {
|
|
3
|
+
// Anthropic
|
|
4
|
+
'claude-opus-4-6': [15, 75],
|
|
5
|
+
'claude-sonnet-4-6': [3, 15],
|
|
6
|
+
'claude-haiku-4-5-20251001': [0.8, 4],
|
|
7
|
+
'claude-sonnet-4-5-20250514': [3, 15],
|
|
8
|
+
// OpenAI
|
|
9
|
+
'gpt-4o': [2.5, 10],
|
|
10
|
+
'gpt-4o-mini': [0.15, 0.6],
|
|
11
|
+
'gpt-4.1': [2, 8],
|
|
12
|
+
'gpt-4.1-mini': [0.4, 1.6],
|
|
13
|
+
'gpt-4.1-nano': [0.1, 0.4],
|
|
14
|
+
'o3': [10, 40],
|
|
15
|
+
'o3-mini': [1.1, 4.4],
|
|
16
|
+
'o4-mini': [1.1, 4.4],
|
|
17
|
+
// OpenAI embeddings
|
|
18
|
+
'text-embedding-3-small': [0.02, 0],
|
|
19
|
+
'text-embedding-3-large': [0.13, 0],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function estimateCost(model: string, inputTokens: number, outputTokens: number): number {
|
|
23
|
+
const costs = MODEL_COSTS[model]
|
|
24
|
+
if (!costs) return 0
|
|
25
|
+
const [inputRate, outputRate] = costs
|
|
26
|
+
return (inputTokens * inputRate + outputTokens * outputRate) / 1_000_000
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getModelCosts(): Record<string, [number, number]> {
|
|
30
|
+
return { ...MODEL_COSTS }
|
|
31
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { loadQueue, loadSchedules, loadSessions, saveSessions, loadConnectors } from './storage'
|
|
2
|
+
import { processNext, cleanupFinishedTaskSessions, validateCompletedTasksQueue, recoverStalledRunningTasks } from './queue'
|
|
3
|
+
import { startScheduler, stopScheduler } from './scheduler'
|
|
4
|
+
import { sweepOrphanedBrowsers, getActiveBrowserCount } from './session-tools'
|
|
5
|
+
import {
|
|
6
|
+
autoStartConnectors,
|
|
7
|
+
stopAllConnectors,
|
|
8
|
+
listRunningConnectors,
|
|
9
|
+
sendConnectorMessage,
|
|
10
|
+
startConnector,
|
|
11
|
+
getConnectorStatus,
|
|
12
|
+
} from './connectors/manager'
|
|
13
|
+
import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus } from './heartbeat-service'
|
|
14
|
+
|
|
15
|
+
const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
|
|
16
|
+
const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
|
|
17
|
+
const BROWSER_MAX_AGE = 10 * 60 * 1000 // 10 minutes idle = orphaned
|
|
18
|
+
const HEALTH_CHECK_INTERVAL = 120_000 // 2 minutes
|
|
19
|
+
const STALE_MULTIPLIER = 4 // session is stale after N × heartbeat interval
|
|
20
|
+
const STALE_MIN_MS = 4 * 60 * 1000 // minimum 4 minutes regardless of interval
|
|
21
|
+
const STALE_AUTO_DISABLE_MULTIPLIER = 16 // auto-disable after much longer sustained staleness
|
|
22
|
+
const STALE_AUTO_DISABLE_MIN_MS = 45 * 60 * 1000 // never auto-disable before 45 minutes
|
|
23
|
+
const CONNECTOR_RESTART_BASE_MS = 30_000
|
|
24
|
+
const CONNECTOR_RESTART_MAX_MS = 15 * 60 * 1000
|
|
25
|
+
|
|
26
|
+
function parseBoolish(value: unknown, fallback: boolean): boolean {
|
|
27
|
+
if (typeof value === 'boolean') return value
|
|
28
|
+
if (typeof value !== 'string') return fallback
|
|
29
|
+
const normalized = value.trim().toLowerCase()
|
|
30
|
+
if (!normalized) return fallback
|
|
31
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
|
|
32
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false
|
|
33
|
+
return fallback
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function daemonAutostartEnvEnabled(): boolean {
|
|
37
|
+
return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, true)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseHeartbeatIntervalSec(value: unknown, fallback = 120): number {
|
|
41
|
+
const parsed = typeof value === 'number'
|
|
42
|
+
? value
|
|
43
|
+
: typeof value === 'string'
|
|
44
|
+
? Number.parseInt(value, 10)
|
|
45
|
+
: Number.NaN
|
|
46
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
47
|
+
return Math.max(0, Math.min(3600, Math.trunc(parsed)))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeWhatsappTarget(raw?: string | null): string | null {
|
|
51
|
+
const input = (raw || '').trim()
|
|
52
|
+
if (!input) return null
|
|
53
|
+
if (input.includes('@')) return input
|
|
54
|
+
let digits = input.replace(/[^\d+]/g, '')
|
|
55
|
+
if (digits.startsWith('+')) digits = digits.slice(1)
|
|
56
|
+
if (digits.startsWith('0') && digits.length >= 10) {
|
|
57
|
+
digits = `44${digits.slice(1)}`
|
|
58
|
+
}
|
|
59
|
+
digits = digits.replace(/[^\d]/g, '')
|
|
60
|
+
return digits ? `${digits}@s.whatsapp.net` : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Store daemon state on globalThis to survive HMR reloads
|
|
64
|
+
const gk = '__swarmclaw_daemon__' as const
|
|
65
|
+
const ds: {
|
|
66
|
+
queueIntervalId: ReturnType<typeof setInterval> | null
|
|
67
|
+
browserSweepId: ReturnType<typeof setInterval> | null
|
|
68
|
+
healthIntervalId: ReturnType<typeof setInterval> | null
|
|
69
|
+
/** Session IDs we've already alerted as stale (alert-once semantics). */
|
|
70
|
+
staleSessionIds: Set<string>
|
|
71
|
+
connectorRestartState: Map<string, { lastAttemptAt: number; failCount: number }>
|
|
72
|
+
manualStopRequested: boolean
|
|
73
|
+
running: boolean
|
|
74
|
+
lastProcessedAt: number | null
|
|
75
|
+
} = (globalThis as any)[gk] ?? ((globalThis as any)[gk] = {
|
|
76
|
+
queueIntervalId: null,
|
|
77
|
+
browserSweepId: null,
|
|
78
|
+
healthIntervalId: null,
|
|
79
|
+
staleSessionIds: new Set<string>(),
|
|
80
|
+
connectorRestartState: new Map<string, { lastAttemptAt: number; failCount: number }>(),
|
|
81
|
+
manualStopRequested: false,
|
|
82
|
+
running: false,
|
|
83
|
+
lastProcessedAt: null,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Backfill fields for hot-reloaded daemon state objects from older code versions.
|
|
87
|
+
if (!ds.staleSessionIds) ds.staleSessionIds = new Set<string>()
|
|
88
|
+
if (!ds.connectorRestartState) ds.connectorRestartState = new Map<string, { lastAttemptAt: number; failCount: number }>()
|
|
89
|
+
// Migrate from old issueLastAlertAt map if present (HMR across code versions)
|
|
90
|
+
if ((ds as any).issueLastAlertAt) delete (ds as any).issueLastAlertAt
|
|
91
|
+
if (ds.healthIntervalId === undefined) ds.healthIntervalId = null
|
|
92
|
+
if (ds.manualStopRequested === undefined) ds.manualStopRequested = false
|
|
93
|
+
|
|
94
|
+
export function ensureDaemonStarted(source = 'unknown'): boolean {
|
|
95
|
+
if (ds.running) return false
|
|
96
|
+
if (!daemonAutostartEnvEnabled()) return false
|
|
97
|
+
if (ds.manualStopRequested) return false
|
|
98
|
+
startDaemon({ source, manualStart: false })
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function startDaemon(options?: { source?: string; manualStart?: boolean }) {
|
|
103
|
+
const source = options?.source || 'unknown'
|
|
104
|
+
const manualStart = options?.manualStart === true
|
|
105
|
+
if (manualStart) ds.manualStopRequested = false
|
|
106
|
+
|
|
107
|
+
if (ds.running) {
|
|
108
|
+
// In dev/HMR, daemon can already be flagged running while new interval types
|
|
109
|
+
// (for example health monitor) were introduced in newer code.
|
|
110
|
+
startQueueProcessor()
|
|
111
|
+
startBrowserSweep()
|
|
112
|
+
startHealthMonitor()
|
|
113
|
+
startHeartbeatService()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
ds.running = true
|
|
117
|
+
console.log(`[daemon] Starting daemon (source=${source}, scheduler + queue processor + heartbeat)`)
|
|
118
|
+
|
|
119
|
+
validateCompletedTasksQueue()
|
|
120
|
+
cleanupFinishedTaskSessions()
|
|
121
|
+
startScheduler()
|
|
122
|
+
startQueueProcessor()
|
|
123
|
+
startBrowserSweep()
|
|
124
|
+
startHealthMonitor()
|
|
125
|
+
startHeartbeatService()
|
|
126
|
+
|
|
127
|
+
// Auto-start enabled connectors
|
|
128
|
+
autoStartConnectors().catch((err) => {
|
|
129
|
+
console.error('[daemon] Error auto-starting connectors:', err.message)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function stopDaemon(options?: { source?: string; manualStop?: boolean }) {
|
|
134
|
+
const source = options?.source || 'unknown'
|
|
135
|
+
if (options?.manualStop === true) ds.manualStopRequested = true
|
|
136
|
+
if (!ds.running) return
|
|
137
|
+
ds.running = false
|
|
138
|
+
console.log(`[daemon] Stopping daemon (source=${source})`)
|
|
139
|
+
|
|
140
|
+
stopScheduler()
|
|
141
|
+
stopQueueProcessor()
|
|
142
|
+
stopBrowserSweep()
|
|
143
|
+
stopHealthMonitor()
|
|
144
|
+
stopHeartbeatService()
|
|
145
|
+
stopAllConnectors().catch(() => {})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function startBrowserSweep() {
|
|
149
|
+
if (ds.browserSweepId) return
|
|
150
|
+
ds.browserSweepId = setInterval(() => {
|
|
151
|
+
const count = getActiveBrowserCount()
|
|
152
|
+
if (count > 0) {
|
|
153
|
+
const cleaned = sweepOrphanedBrowsers(BROWSER_MAX_AGE)
|
|
154
|
+
if (cleaned > 0) {
|
|
155
|
+
console.log(`[daemon] Cleaned ${cleaned} orphaned browser(s), ${getActiveBrowserCount()} still active`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, BROWSER_SWEEP_INTERVAL)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function stopBrowserSweep() {
|
|
162
|
+
if (ds.browserSweepId) {
|
|
163
|
+
clearInterval(ds.browserSweepId)
|
|
164
|
+
ds.browserSweepId = null
|
|
165
|
+
}
|
|
166
|
+
// Kill all remaining browsers on shutdown
|
|
167
|
+
sweepOrphanedBrowsers(0)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function startQueueProcessor() {
|
|
171
|
+
if (ds.queueIntervalId) return
|
|
172
|
+
ds.queueIntervalId = setInterval(async () => {
|
|
173
|
+
const queue = loadQueue()
|
|
174
|
+
if (queue.length > 0) {
|
|
175
|
+
console.log(`[daemon] Processing ${queue.length} queued task(s)`)
|
|
176
|
+
await processNext()
|
|
177
|
+
ds.lastProcessedAt = Date.now()
|
|
178
|
+
}
|
|
179
|
+
}, QUEUE_CHECK_INTERVAL)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function stopQueueProcessor() {
|
|
183
|
+
if (ds.queueIntervalId) {
|
|
184
|
+
clearInterval(ds.queueIntervalId)
|
|
185
|
+
ds.queueIntervalId = null
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function sendHealthAlert(text: string) {
|
|
190
|
+
console.warn(`[health] ${text}`)
|
|
191
|
+
try {
|
|
192
|
+
const running = listRunningConnectors('whatsapp')
|
|
193
|
+
if (!running.length) return
|
|
194
|
+
const candidate = running[0]
|
|
195
|
+
const target = candidate.recentChannelId
|
|
196
|
+
|| normalizeWhatsappTarget(candidate.configuredTargets[0] || null)
|
|
197
|
+
if (!target) return
|
|
198
|
+
await sendConnectorMessage({
|
|
199
|
+
connectorId: candidate.id,
|
|
200
|
+
channelId: target,
|
|
201
|
+
text: `⚠️ SwarmClaw health alert: ${text}`,
|
|
202
|
+
})
|
|
203
|
+
} catch {
|
|
204
|
+
// alerts are best effort; log-only fallback is acceptable
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runConnectorHealthChecks(now: number) {
|
|
209
|
+
const connectors = loadConnectors()
|
|
210
|
+
for (const connector of Object.values(connectors) as any[]) {
|
|
211
|
+
if (!connector?.id) continue
|
|
212
|
+
if (connector.isEnabled !== true) {
|
|
213
|
+
ds.connectorRestartState.delete(connector.id)
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const runtimeStatus = getConnectorStatus(connector.id)
|
|
218
|
+
if (runtimeStatus === 'running') {
|
|
219
|
+
ds.connectorRestartState.delete(connector.id)
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const current = ds.connectorRestartState.get(connector.id) || { lastAttemptAt: 0, failCount: 0 }
|
|
224
|
+
const backoffMs = Math.min(
|
|
225
|
+
CONNECTOR_RESTART_MAX_MS,
|
|
226
|
+
CONNECTOR_RESTART_BASE_MS * (2 ** Math.min(6, current.failCount)),
|
|
227
|
+
)
|
|
228
|
+
if ((now - current.lastAttemptAt) < backoffMs) continue
|
|
229
|
+
|
|
230
|
+
current.lastAttemptAt = now
|
|
231
|
+
ds.connectorRestartState.set(connector.id, current)
|
|
232
|
+
try {
|
|
233
|
+
await startConnector(connector.id)
|
|
234
|
+
ds.connectorRestartState.delete(connector.id)
|
|
235
|
+
await sendHealthAlert(`Connector "${connector.name}" (${connector.platform}) was down and has been auto-restarted.`)
|
|
236
|
+
} catch (err: any) {
|
|
237
|
+
current.failCount += 1
|
|
238
|
+
ds.connectorRestartState.set(connector.id, current)
|
|
239
|
+
console.warn(`[health] Connector auto-restart failed for ${connector.name}: ${err?.message || String(err)}`)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function runHealthChecks() {
|
|
245
|
+
// Continuously keep the completed queue honest.
|
|
246
|
+
validateCompletedTasksQueue()
|
|
247
|
+
recoverStalledRunningTasks()
|
|
248
|
+
|
|
249
|
+
// Keep heartbeat state in sync with task terminal states even without daemon restarts.
|
|
250
|
+
cleanupFinishedTaskSessions()
|
|
251
|
+
|
|
252
|
+
const sessions = loadSessions()
|
|
253
|
+
const now = Date.now()
|
|
254
|
+
const currentlyStale = new Set<string>()
|
|
255
|
+
let sessionsDirty = false
|
|
256
|
+
|
|
257
|
+
for (const session of Object.values(sessions) as any[]) {
|
|
258
|
+
if (!session?.id) continue
|
|
259
|
+
if (session.heartbeatEnabled !== true) continue
|
|
260
|
+
|
|
261
|
+
const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec, 120)
|
|
262
|
+
if (intervalSec <= 0) continue
|
|
263
|
+
const staleAfter = Math.max(intervalSec * STALE_MULTIPLIER * 1000, STALE_MIN_MS)
|
|
264
|
+
const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
|
|
265
|
+
if (lastActive <= 0) continue
|
|
266
|
+
|
|
267
|
+
const staleForMs = now - lastActive
|
|
268
|
+
if (staleForMs > staleAfter) {
|
|
269
|
+
const autoDisableAfter = Math.max(intervalSec * STALE_AUTO_DISABLE_MULTIPLIER * 1000, STALE_AUTO_DISABLE_MIN_MS)
|
|
270
|
+
if (staleForMs > autoDisableAfter) {
|
|
271
|
+
session.heartbeatEnabled = false
|
|
272
|
+
session.lastActiveAt = now
|
|
273
|
+
sessionsDirty = true
|
|
274
|
+
ds.staleSessionIds.delete(session.id)
|
|
275
|
+
await sendHealthAlert(
|
|
276
|
+
`Auto-disabled heartbeat for stale session "${session.name || session.id}" after ${Math.round(staleForMs / 60_000)}m of inactivity.`,
|
|
277
|
+
)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
currentlyStale.add(session.id)
|
|
282
|
+
// Only alert on transition from healthy → stale (once per stale episode)
|
|
283
|
+
if (!ds.staleSessionIds.has(session.id)) {
|
|
284
|
+
ds.staleSessionIds.add(session.id)
|
|
285
|
+
await sendHealthAlert(
|
|
286
|
+
`Session "${session.name || session.id}" heartbeat appears stale (last active ${(Math.round(staleForMs / 1000))}s ago, interval ${intervalSec}s).`,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Clear recovered sessions so they can re-alert if they go stale again later
|
|
293
|
+
for (const id of ds.staleSessionIds) {
|
|
294
|
+
if (!currentlyStale.has(id)) {
|
|
295
|
+
ds.staleSessionIds.delete(id)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (sessionsDirty) saveSessions(sessions)
|
|
300
|
+
|
|
301
|
+
await runConnectorHealthChecks(now)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function startHealthMonitor() {
|
|
305
|
+
if (ds.healthIntervalId) return
|
|
306
|
+
ds.healthIntervalId = setInterval(() => {
|
|
307
|
+
runHealthChecks().catch((err) => {
|
|
308
|
+
console.error('[daemon] Health monitor tick failed:', err?.message || String(err))
|
|
309
|
+
})
|
|
310
|
+
}, HEALTH_CHECK_INTERVAL)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function stopHealthMonitor() {
|
|
314
|
+
if (ds.healthIntervalId) {
|
|
315
|
+
clearInterval(ds.healthIntervalId)
|
|
316
|
+
ds.healthIntervalId = null
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function runDaemonHealthCheckNow() {
|
|
321
|
+
await runHealthChecks()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function getDaemonStatus() {
|
|
325
|
+
const queue = loadQueue()
|
|
326
|
+
const schedules = loadSchedules()
|
|
327
|
+
|
|
328
|
+
// Find next scheduled task
|
|
329
|
+
let nextScheduled: number | null = null
|
|
330
|
+
for (const s of Object.values(schedules) as any[]) {
|
|
331
|
+
if (s.status === 'active' && s.nextRunAt) {
|
|
332
|
+
if (!nextScheduled || s.nextRunAt < nextScheduled) {
|
|
333
|
+
nextScheduled = s.nextRunAt
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
running: ds.running,
|
|
340
|
+
schedulerActive: ds.running,
|
|
341
|
+
autostartEnabled: daemonAutostartEnvEnabled(),
|
|
342
|
+
manualStopRequested: ds.manualStopRequested,
|
|
343
|
+
queueLength: queue.length,
|
|
344
|
+
lastProcessed: ds.lastProcessedAt,
|
|
345
|
+
nextScheduled,
|
|
346
|
+
heartbeat: getHeartbeatServiceStatus(),
|
|
347
|
+
health: {
|
|
348
|
+
monitorActive: !!ds.healthIntervalId,
|
|
349
|
+
staleSessions: ds.staleSessionIds.size,
|
|
350
|
+
connectorsInBackoff: ds.connectorRestartState.size,
|
|
351
|
+
checkIntervalSec: Math.trunc(HEALTH_CHECK_INTERVAL / 1000),
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
}
|