@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.
Files changed (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,1017 @@
1
+ import crypto from 'crypto'
2
+ import type { GoalContract, MessageToolEvent } from '@/types'
3
+ import { loadSessions, saveSessions, loadTasks, saveTasks } from './storage'
4
+ import { log } from './logger'
5
+ import { getMemoryDb } from './memory-db'
6
+ import {
7
+ mergeGoalContracts,
8
+ parseGoalContractFromText,
9
+ parseMainLoopPlan,
10
+ parseMainLoopReview,
11
+ } from './autonomy-contract'
12
+
13
+ const MAIN_SESSION_NAME = '__main__'
14
+ const MAX_PENDING_EVENTS = 40
15
+ const MAX_TIMELINE_EVENTS = 80
16
+ const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
17
+ const MEMORY_NOTE_MIN_INTERVAL_MS = 90 * 60 * 1000
18
+ const DEFAULT_FOLLOWUP_DELAY_SEC = 45
19
+ const MAX_FOLLOWUP_CHAIN = 6
20
+ const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
21
+ const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
22
+ const DELIVERY_GOAL_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
23
+ const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recurr)\b/i
24
+ const UPLOAD_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
25
+ const SENT_ARTIFACT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image|file)\b/i
26
+
27
+ interface MainLoopSessionMessageLike {
28
+ text?: string
29
+ }
30
+
31
+ interface MainLoopSessionEvidenceLike {
32
+ messages?: MainLoopSessionMessageLike[]
33
+ }
34
+
35
+ export interface MainLoopEvent {
36
+ id: string
37
+ type: string
38
+ text: string
39
+ createdAt: number
40
+ }
41
+
42
+ export interface MainLoopTimelineEntry {
43
+ id: string
44
+ at: number
45
+ source: string
46
+ note: string
47
+ status?: 'idle' | 'progress' | 'blocked' | 'ok'
48
+ }
49
+
50
+ export interface MainLoopState {
51
+ goal: string | null
52
+ goalContract: GoalContract | null
53
+ status: 'idle' | 'progress' | 'blocked' | 'ok'
54
+ summary: string | null
55
+ nextAction: string | null
56
+ planSteps: string[]
57
+ currentPlanStep: string | null
58
+ reviewNote: string | null
59
+ reviewConfidence: number | null
60
+ missionTaskId: string | null
61
+ momentumScore: number
62
+ paused: boolean
63
+ autonomyMode: 'assist' | 'autonomous'
64
+ pendingEvents: MainLoopEvent[]
65
+ timeline: MainLoopTimelineEntry[]
66
+ followupChainCount: number
67
+ metaMissCount: number
68
+ workingMemoryNotes: string[]
69
+ lastMemoryNoteAt: number | null
70
+ lastPlannedAt: number | null
71
+ lastReviewedAt: number | null
72
+ lastTickAt: number | null
73
+ updatedAt: number
74
+ }
75
+
76
+ interface MainLoopMeta {
77
+ status?: 'idle' | 'progress' | 'blocked' | 'ok'
78
+ summary?: string
79
+ next_action?: string
80
+ follow_up?: boolean
81
+ delay_sec?: number
82
+ goal?: string
83
+ consume_event_ids?: string[]
84
+ }
85
+
86
+ export interface MainLoopFollowupRequest {
87
+ message: string
88
+ delayMs: number
89
+ dedupeKey: string
90
+ }
91
+
92
+ export interface PushMainLoopEventInput {
93
+ type: string
94
+ text: string
95
+ user?: string | null
96
+ }
97
+
98
+ export interface HandleMainLoopRunResultInput {
99
+ sessionId: string
100
+ message: string
101
+ internal: boolean
102
+ source: string
103
+ resultText: string
104
+ error?: string
105
+ toolEvents?: MessageToolEvent[]
106
+ }
107
+
108
+ function toOneLine(value: string, max = 240): string {
109
+ return (value || '').replace(/\s+/g, ' ').trim().slice(0, max)
110
+ }
111
+
112
+ function normalizeMemoryText(value: string): string {
113
+ return (value || '').replace(/\s+/g, ' ').trim()
114
+ }
115
+
116
+ function clampInt(value: unknown, fallback: number, min: number, max: number): number {
117
+ const parsed = typeof value === 'number'
118
+ ? value
119
+ : typeof value === 'string'
120
+ ? Number.parseInt(value, 10)
121
+ : Number.NaN
122
+ if (!Number.isFinite(parsed)) return fallback
123
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
124
+ }
125
+
126
+ function pruneEvents(events: MainLoopEvent[], now = Date.now()): MainLoopEvent[] {
127
+ const minTs = now - EVENT_TTL_MS
128
+ const fresh = events.filter((e) => e && typeof e.createdAt === 'number' && e.createdAt >= minTs)
129
+ if (fresh.length <= MAX_PENDING_EVENTS) return fresh
130
+ return fresh.slice(fresh.length - MAX_PENDING_EVENTS)
131
+ }
132
+
133
+ function pruneTimeline(entries: MainLoopTimelineEntry[], now = Date.now()): MainLoopTimelineEntry[] {
134
+ const minTs = now - EVENT_TTL_MS
135
+ const fresh = entries.filter((e) => e && typeof e.at === 'number' && e.at >= minTs && typeof e.note === 'string' && e.note.trim())
136
+ if (fresh.length <= MAX_TIMELINE_EVENTS) return fresh
137
+ return fresh.slice(fresh.length - MAX_TIMELINE_EVENTS)
138
+ }
139
+
140
+ function appendTimeline(
141
+ state: MainLoopState,
142
+ source: string,
143
+ note: string,
144
+ now = Date.now(),
145
+ status?: 'idle' | 'progress' | 'blocked' | 'ok',
146
+ ) {
147
+ const normalizedNote = toOneLine(note, 400)
148
+ if (!normalizedNote) return
149
+ const recent = state.timeline.at(-1)
150
+ if (recent && recent.source === source && recent.note === normalizedNote && now - recent.at < 45_000) return
151
+ state.timeline.push({
152
+ id: `tl_${crypto.randomBytes(4).toString('hex')}`,
153
+ at: now,
154
+ source,
155
+ note: normalizedNote,
156
+ status,
157
+ })
158
+ state.timeline = pruneTimeline(state.timeline, now)
159
+ }
160
+
161
+ function computeMomentumScore(state: MainLoopState): number {
162
+ const baseByStatus = {
163
+ idle: 40,
164
+ progress: 72,
165
+ blocked: 20,
166
+ ok: 94,
167
+ } as const
168
+ let score: number = baseByStatus[state.status]
169
+ score -= Math.min(20, state.metaMissCount * 3)
170
+ score -= Math.min(12, Math.max(0, state.pendingEvents.length - 4) * 2)
171
+ if (state.paused) score = Math.min(score, 35)
172
+ return clampInt(score, 0, 0, 100)
173
+ }
174
+
175
+ function normalizeStringList(input: unknown, maxItems: number, maxChars: number): string[] {
176
+ if (!Array.isArray(input)) return []
177
+ const seen = new Set<string>()
178
+ const out: string[] = []
179
+ for (const raw of input) {
180
+ if (typeof raw !== 'string') continue
181
+ const value = raw.replace(/\s+/g, ' ').trim().slice(0, maxChars)
182
+ if (!value) continue
183
+ const key = value.toLowerCase()
184
+ if (seen.has(key)) continue
185
+ seen.add(key)
186
+ out.push(value)
187
+ if (out.length >= maxItems) break
188
+ }
189
+ return out
190
+ }
191
+
192
+ function normalizeGoalContract(raw: any): GoalContract | null {
193
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
194
+ const objective = typeof raw.objective === 'string' ? raw.objective.trim().slice(0, 300) : ''
195
+ if (!objective) return null
196
+ const constraints = normalizeStringList(raw.constraints, 10, 220)
197
+ const budgetUsd = typeof raw.budgetUsd === 'number'
198
+ ? Math.max(0, Math.min(1_000_000, raw.budgetUsd))
199
+ : null
200
+ const deadlineAt = typeof raw.deadlineAt === 'number' && Number.isFinite(raw.deadlineAt)
201
+ ? Math.trunc(raw.deadlineAt)
202
+ : null
203
+ const successMetric = typeof raw.successMetric === 'string'
204
+ ? raw.successMetric.trim().slice(0, 220) || null
205
+ : null
206
+ return {
207
+ objective,
208
+ constraints: constraints.length ? constraints : undefined,
209
+ budgetUsd,
210
+ deadlineAt,
211
+ successMetric,
212
+ }
213
+ }
214
+
215
+ function normalizeState(raw: any, now = Date.now()): MainLoopState {
216
+ const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
217
+ ? raw.status
218
+ : 'idle'
219
+
220
+ const pendingRaw = Array.isArray(raw?.pendingEvents) ? raw.pendingEvents : []
221
+ const pendingEvents = pruneEvents(
222
+ pendingRaw
223
+ .map((e: any) => {
224
+ const text = toOneLine(typeof e?.text === 'string' ? e.text : '')
225
+ if (!text) return null
226
+ return {
227
+ id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${crypto.randomBytes(3).toString('hex')}`,
228
+ type: typeof e?.type === 'string' && e.type.trim() ? e.type.trim() : 'event',
229
+ text,
230
+ createdAt: typeof e?.createdAt === 'number' ? e.createdAt : now,
231
+ } as MainLoopEvent
232
+ })
233
+ .filter(Boolean) as MainLoopEvent[],
234
+ now,
235
+ )
236
+
237
+ const timelineRaw = Array.isArray(raw?.timeline) ? raw.timeline : []
238
+ const timeline = pruneTimeline(
239
+ timelineRaw
240
+ .map((entry: any) => {
241
+ const note = toOneLine(typeof entry?.note === 'string' ? entry.note : '', 400)
242
+ if (!note) return null
243
+ const status = entry?.status === 'blocked' || entry?.status === 'ok' || entry?.status === 'progress' || entry?.status === 'idle'
244
+ ? entry.status
245
+ : undefined
246
+ return {
247
+ id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${crypto.randomBytes(3).toString('hex')}`,
248
+ at: typeof entry?.at === 'number' ? entry.at : now,
249
+ source: typeof entry?.source === 'string' && entry.source.trim() ? entry.source.trim() : 'event',
250
+ note,
251
+ status,
252
+ } as MainLoopTimelineEntry
253
+ })
254
+ .filter(Boolean) as MainLoopTimelineEntry[],
255
+ now,
256
+ )
257
+
258
+ const normalized: MainLoopState = {
259
+ goal: typeof raw?.goal === 'string' && raw.goal.trim() ? raw.goal.trim().slice(0, 600) : null,
260
+ goalContract: normalizeGoalContract(raw?.goalContract),
261
+ status,
262
+ summary: typeof raw?.summary === 'string' && raw.summary.trim() ? raw.summary.trim().slice(0, 800) : null,
263
+ nextAction: typeof raw?.nextAction === 'string' && raw.nextAction.trim() ? raw.nextAction.trim().slice(0, 600) : null,
264
+ planSteps: normalizeStringList(raw?.planSteps, 10, 220),
265
+ currentPlanStep: typeof raw?.currentPlanStep === 'string' && raw.currentPlanStep.trim()
266
+ ? raw.currentPlanStep.trim().slice(0, 220)
267
+ : null,
268
+ reviewNote: typeof raw?.reviewNote === 'string' && raw.reviewNote.trim()
269
+ ? raw.reviewNote.trim().slice(0, 320)
270
+ : null,
271
+ reviewConfidence: typeof raw?.reviewConfidence === 'number' && Number.isFinite(raw.reviewConfidence)
272
+ ? Math.max(0, Math.min(1, raw.reviewConfidence))
273
+ : null,
274
+ missionTaskId: typeof raw?.missionTaskId === 'string' && raw.missionTaskId.trim() ? raw.missionTaskId.trim() : null,
275
+ momentumScore: clampInt(raw?.momentumScore, 40, 0, 100),
276
+ paused: raw?.paused === true,
277
+ autonomyMode: raw?.autonomyMode === 'assist' ? 'assist' : 'autonomous',
278
+ pendingEvents,
279
+ timeline,
280
+ followupChainCount: clampInt(raw?.followupChainCount, 0, 0, 100),
281
+ metaMissCount: clampInt(raw?.metaMissCount, 0, 0, 100),
282
+ workingMemoryNotes: normalizeStringList(raw?.workingMemoryNotes, 24, 260),
283
+ lastMemoryNoteAt: typeof raw?.lastMemoryNoteAt === 'number' ? raw.lastMemoryNoteAt : null,
284
+ lastPlannedAt: typeof raw?.lastPlannedAt === 'number' ? raw.lastPlannedAt : null,
285
+ lastReviewedAt: typeof raw?.lastReviewedAt === 'number' ? raw.lastReviewedAt : null,
286
+ lastTickAt: typeof raw?.lastTickAt === 'number' ? raw.lastTickAt : null,
287
+ updatedAt: typeof raw?.updatedAt === 'number' ? raw.updatedAt : now,
288
+ }
289
+ if (!normalized.goal && normalized.goalContract?.objective) {
290
+ normalized.goal = normalized.goalContract.objective
291
+ }
292
+ normalized.momentumScore = computeMomentumScore(normalized)
293
+ return normalized
294
+ }
295
+
296
+ function appendEvent(state: MainLoopState, type: string, text: string, now = Date.now()): boolean {
297
+ const normalizedText = toOneLine(text)
298
+ if (!normalizedText) return false
299
+ const recent = state.pendingEvents.at(-1)
300
+ if (recent && recent.type === type && recent.text === normalizedText && now - recent.createdAt < 60_000) {
301
+ return false
302
+ }
303
+ state.pendingEvents.push({
304
+ id: `evt_${crypto.randomBytes(4).toString('hex')}`,
305
+ type,
306
+ text: normalizedText,
307
+ createdAt: now,
308
+ })
309
+ state.pendingEvents = pruneEvents(state.pendingEvents, now)
310
+ return true
311
+ }
312
+
313
+ function appendWorkingMemoryNote(state: MainLoopState, note: string) {
314
+ const value = toOneLine(note, 260)
315
+ if (!value) return
316
+ const existing = state.workingMemoryNotes || []
317
+ if (existing.length && existing[existing.length - 1] === value) return
318
+ state.workingMemoryNotes = [...existing.slice(-23), value]
319
+ }
320
+
321
+ function inferGoalFromUserMessage(message: string): string | null {
322
+ const text = (message || '').trim()
323
+ if (!text) return null
324
+ if (/^SWARM_MAIN_(MISSION_TICK|AUTO_FOLLOWUP)\b/i.test(text)) return null
325
+ if (/^SWARM_HEARTBEAT_CHECK\b/i.test(text)) return null
326
+ if (/^(ok|okay|cool|thanks|thx|got it|nice|yep|yeah|nope|nah)[.! ]*$/i.test(text)) return null
327
+ return text.slice(0, 600)
328
+ }
329
+
330
+ function inferGoalFromSessionMessages(session: any): string | null {
331
+ const msgs = Array.isArray(session?.messages) ? session.messages : []
332
+ for (let i = msgs.length - 1; i >= 0; i -= 1) {
333
+ const msg = msgs[i]
334
+ if (msg?.role !== 'user') continue
335
+ const inferred = inferGoalFromUserMessage(typeof msg?.text === 'string' ? msg.text : '')
336
+ if (inferred) return inferred
337
+ }
338
+ return null
339
+ }
340
+
341
+ function parseMainLoopMeta(text: string): MainLoopMeta | null {
342
+ const raw = (text || '').trim()
343
+ if (!raw) return null
344
+
345
+ const markerMatch = raw.match(META_LINE_RE)
346
+ const parseCandidate = markerMatch?.[1]
347
+ if (parseCandidate) {
348
+ try {
349
+ const parsed = JSON.parse(parseCandidate)
350
+ return normalizeMeta(parsed)
351
+ } catch {
352
+ // fall through
353
+ }
354
+ }
355
+
356
+ // Fallback: parse any one-line JSON that appears to be the meta payload.
357
+ for (const line of raw.split('\n')) {
358
+ const trimmed = line.trim()
359
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue
360
+ if (!trimmed.includes('follow_up') && !trimmed.includes('next_action') && !trimmed.includes('consume_event_ids')) continue
361
+ try {
362
+ const parsed = JSON.parse(trimmed)
363
+ return normalizeMeta(parsed)
364
+ } catch {
365
+ // skip malformed candidate lines
366
+ }
367
+ }
368
+
369
+ return null
370
+ }
371
+
372
+ function normalizeMeta(raw: any): MainLoopMeta {
373
+ const status = raw?.status === 'blocked' || raw?.status === 'ok' || raw?.status === 'progress' || raw?.status === 'idle'
374
+ ? raw.status
375
+ : undefined
376
+
377
+ const consumeIds = Array.isArray(raw?.consume_event_ids)
378
+ ? raw.consume_event_ids
379
+ .map((v: unknown) => (typeof v === 'string' ? v.trim() : ''))
380
+ .filter(Boolean)
381
+ : undefined
382
+
383
+ const followUp = typeof raw?.follow_up === 'boolean'
384
+ ? raw.follow_up
385
+ : typeof raw?.follow_up === 'string'
386
+ ? raw.follow_up.trim().toLowerCase() === 'true'
387
+ : undefined
388
+
389
+ return {
390
+ status,
391
+ summary: typeof raw?.summary === 'string' ? raw.summary.trim().slice(0, 800) : undefined,
392
+ next_action: typeof raw?.next_action === 'string' ? raw.next_action.trim().slice(0, 600) : undefined,
393
+ follow_up: followUp,
394
+ delay_sec: clampInt(raw?.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900),
395
+ goal: typeof raw?.goal === 'string' ? raw.goal.trim().slice(0, 600) : undefined,
396
+ consume_event_ids: consumeIds,
397
+ }
398
+ }
399
+
400
+ function consumeEvents(state: MainLoopState, ids: string[] | undefined) {
401
+ if (!ids?.length) return
402
+ const remove = new Set(ids)
403
+ state.pendingEvents = state.pendingEvents.filter((event) => !remove.has(event.id))
404
+ }
405
+
406
+ function buildPendingEventLines(state: MainLoopState): string {
407
+ if (!state.pendingEvents.length) return 'Pending events:\n- none'
408
+ const lines = state.pendingEvents
409
+ .slice(-10)
410
+ .map((event) => `- ${event.id} | ${event.type} | ${event.text}`)
411
+ .join('\n')
412
+ return `Pending events (oldest → newest):\n${lines}`
413
+ }
414
+
415
+ function buildTimelineLines(state: MainLoopState): string {
416
+ if (!state.timeline.length) return 'Recent mission timeline:\n- none'
417
+ const lines = state.timeline
418
+ .slice(-5)
419
+ .map((entry) => {
420
+ const ts = new Date(entry.at).toISOString().slice(11, 19)
421
+ const status = entry.status ? ` [${entry.status}]` : ''
422
+ return `- ${ts} ${entry.source}${status}: ${entry.note}`
423
+ })
424
+ .join('\n')
425
+ return `Recent mission timeline:\n${lines}`
426
+ }
427
+
428
+ function buildGoalContractLines(state: MainLoopState): string[] {
429
+ const contract = state.goalContract
430
+ if (!contract?.objective) return []
431
+ const lines = [
432
+ `contract_objective: ${contract.objective}`,
433
+ ]
434
+ if (contract.constraints?.length) lines.push(`contract_constraints: ${contract.constraints.join(' | ')}`)
435
+ if (typeof contract.budgetUsd === 'number') lines.push(`contract_budget_usd: ${contract.budgetUsd}`)
436
+ if (typeof contract.deadlineAt === 'number') lines.push(`contract_deadline_iso: ${new Date(contract.deadlineAt).toISOString()}`)
437
+ if (contract.successMetric) lines.push(`contract_success_metric: ${contract.successMetric}`)
438
+ return lines
439
+ }
440
+
441
+ function missionNeedsScreenshotArtifactEvidence(state: MainLoopState): boolean {
442
+ const haystack = [
443
+ state.goal || '',
444
+ state.goalContract?.objective || '',
445
+ state.goalContract?.successMetric || '',
446
+ state.nextAction || '',
447
+ ...(state.planSteps || []),
448
+ state.currentPlanStep || '',
449
+ ].join(' ')
450
+ if (!SCREENSHOT_GOAL_HINT.test(haystack)) return false
451
+ return DELIVERY_GOAL_HINT.test(haystack) || SCHEDULE_GOAL_HINT.test(haystack)
452
+ }
453
+
454
+ function missionHasScreenshotArtifactEvidence(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): boolean {
455
+ const candidates: string[] = [
456
+ state.summary || '',
457
+ additionalText || '',
458
+ ]
459
+ if (Array.isArray(session?.messages)) {
460
+ for (let i = session.messages.length - 1; i >= 0 && candidates.length < 16; i--) {
461
+ const text = typeof session.messages[i]?.text === 'string' ? session.messages[i].text! : ''
462
+ if (text && text.trim()) candidates.push(text)
463
+ }
464
+ }
465
+ return candidates.some((value) => UPLOAD_ARTIFACT_HINT.test(value) || SENT_ARTIFACT_HINT.test(value))
466
+ }
467
+
468
+ function getMissionCompletionGateReason(session: MainLoopSessionEvidenceLike | null | undefined, state: MainLoopState, additionalText = ''): string | null {
469
+ if (!missionNeedsScreenshotArtifactEvidence(state)) return null
470
+ if (missionHasScreenshotArtifactEvidence(session, state, additionalText)) return null
471
+ return 'Mission requires screenshot artifact evidence (upload link or explicit sent screenshot confirmation) before completion.'
472
+ }
473
+
474
+ function upsertMissionTask(session: any, state: MainLoopState, now: number): string | null {
475
+ if (!state.goal) return state.missionTaskId || null
476
+
477
+ const tasks = loadTasks()
478
+ let task = state.missionTaskId ? tasks[state.missionTaskId] : null
479
+ if (!task) {
480
+ task = Object.values(tasks).find((t: any) =>
481
+ t?.sessionId === session.id
482
+ && t?.title?.startsWith('Mission:')
483
+ && t?.status !== 'archived'
484
+ ) as any || null
485
+ }
486
+
487
+ const title = `Mission: ${state.goal.slice(0, 140)}`
488
+ const statusMap = {
489
+ idle: 'backlog',
490
+ progress: 'running',
491
+ blocked: 'failed',
492
+ ok: 'completed',
493
+ } as const
494
+ let mappedStatus = statusMap[state.status]
495
+ const completionGateReason = mappedStatus === 'completed'
496
+ ? getMissionCompletionGateReason(session, state)
497
+ : null
498
+ if (completionGateReason) mappedStatus = 'running'
499
+
500
+ let changed = false
501
+ const contractLines = buildGoalContractLines(state)
502
+ const planLines = state.planSteps.length
503
+ ? [`plan_steps: ${state.planSteps.join(' -> ')}`]
504
+ : []
505
+ if (state.currentPlanStep) planLines.push(`current_plan_step: ${state.currentPlanStep}`)
506
+ if (state.reviewNote) planLines.push(`latest_review: ${state.reviewNote}`)
507
+
508
+ const baseDescription = [
509
+ 'Autonomous mission goal tracked from main loop.',
510
+ `Goal: ${state.goal}`,
511
+ state.nextAction ? `Next action: ${state.nextAction}` : '',
512
+ completionGateReason ? `Completion gate: ${completionGateReason}` : '',
513
+ ...contractLines,
514
+ ...planLines,
515
+ ].filter(Boolean).join('\n')
516
+
517
+ if (!task) {
518
+ const id = crypto.randomBytes(4).toString('hex')
519
+ task = {
520
+ id,
521
+ title,
522
+ description: baseDescription,
523
+ status: mappedStatus,
524
+ agentId: session.agentId || 'default',
525
+ sessionId: session.id,
526
+ result: state.summary || null,
527
+ error: state.status === 'blocked' ? (state.summary || 'Blocked') : null,
528
+ createdAt: now,
529
+ updatedAt: now,
530
+ startedAt: mappedStatus === 'running' ? now : null,
531
+ completedAt: mappedStatus === 'completed' ? now : null,
532
+ queuedAt: null,
533
+ archivedAt: null,
534
+ comments: [],
535
+ images: [],
536
+ validation: null,
537
+ }
538
+ tasks[id] = task
539
+ changed = true
540
+ } else {
541
+ if (task.title !== title) {
542
+ task.title = title
543
+ changed = true
544
+ }
545
+ const nextDescription = baseDescription
546
+ if (task.description !== nextDescription) {
547
+ task.description = nextDescription
548
+ changed = true
549
+ }
550
+ if (task.status !== mappedStatus) {
551
+ task.status = mappedStatus
552
+ changed = true
553
+ if (mappedStatus === 'running' && !task.startedAt) task.startedAt = now
554
+ if (mappedStatus === 'completed') task.completedAt = now
555
+ }
556
+ const nextResult = state.summary || task.result || null
557
+ if (task.result !== nextResult) {
558
+ task.result = nextResult
559
+ changed = true
560
+ }
561
+ const nextError = mappedStatus === 'failed'
562
+ ? (state.summary || state.nextAction || 'Blocked')
563
+ : null
564
+ if (task.error !== nextError) {
565
+ task.error = nextError
566
+ changed = true
567
+ }
568
+ if (changed) task.updatedAt = now
569
+ tasks[task.id] = task
570
+ }
571
+
572
+ if (changed) {
573
+ saveTasks(tasks)
574
+ }
575
+ return task?.id || null
576
+ }
577
+
578
+ function maybeStoreMissionMemoryNote(
579
+ session: any,
580
+ state: MainLoopState,
581
+ now: number,
582
+ source: string,
583
+ force = false,
584
+ ) {
585
+ if (!Array.isArray(session?.tools) || !session.tools.includes('memory')) return
586
+ if (!state.goal) return
587
+ if (!force && state.lastMemoryNoteAt && (now - state.lastMemoryNoteAt) < MEMORY_NOTE_MIN_INTERVAL_MS) return
588
+
589
+ const summary = state.summary || 'No summary'
590
+ const next = state.nextAction || 'No next action'
591
+ const title = `Mission ${state.status}: ${state.goal.slice(0, 72)}`
592
+ const content = [
593
+ `source: ${source}`,
594
+ `status: ${state.status}`,
595
+ `momentum: ${state.momentumScore}/100`,
596
+ `goal: ${state.goal}`,
597
+ ...buildGoalContractLines(state),
598
+ state.planSteps.length ? `plan_steps: ${state.planSteps.join(' -> ')}` : '',
599
+ state.currentPlanStep ? `current_plan_step: ${state.currentPlanStep}` : '',
600
+ `summary: ${summary}`,
601
+ `next_action: ${next}`,
602
+ state.reviewNote ? `review: ${state.reviewNote}` : '',
603
+ typeof state.reviewConfidence === 'number' ? `review_confidence: ${state.reviewConfidence}` : '',
604
+ state.missionTaskId ? `mission_task_id: ${state.missionTaskId}` : '',
605
+ ].filter(Boolean).join('\n')
606
+
607
+ try {
608
+ const memDb = getMemoryDb()
609
+ const latest = memDb.getLatestBySessionCategory?.(session.id, 'mission')
610
+ if (latest) {
611
+ const sameTitle = normalizeMemoryText(latest.title) === normalizeMemoryText(title)
612
+ const sameContent = normalizeMemoryText(latest.content) === normalizeMemoryText(content)
613
+ if (sameTitle && sameContent) {
614
+ state.lastMemoryNoteAt = now
615
+ return
616
+ }
617
+ }
618
+ memDb.add({
619
+ agentId: session.agentId || null,
620
+ sessionId: session.id,
621
+ category: 'mission',
622
+ title,
623
+ content,
624
+ } as any)
625
+ state.lastMemoryNoteAt = now
626
+ } catch (err: any) {
627
+ appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err?.message || String(err), 240)}`, now)
628
+ }
629
+ }
630
+
631
+ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: boolean }): string {
632
+ const hasMemoryTool = opts?.hasMemoryTool === true
633
+ const goal = state.goal || 'No explicit goal yet. Continue with the strongest actionable objective from recent context.'
634
+ const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
635
+ const contractLines = buildGoalContractLines(state)
636
+ return [
637
+ 'SWARM_MAIN_AUTO_FOLLOWUP',
638
+ `Mission goal: ${goal}`,
639
+ `Next action to execute now: ${nextAction}`,
640
+ `Current status: ${state.status}`,
641
+ `Mission task id: ${state.missionTaskId || 'none'}`,
642
+ `Momentum score: ${state.momentumScore}/100`,
643
+ ...contractLines,
644
+ state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
645
+ state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
646
+ state.reviewNote ? `Last review: ${state.reviewNote}` : '',
647
+ buildPendingEventLines(state),
648
+ buildTimelineLines(state),
649
+ 'Act autonomously. Use available tools to execute work, verify results, and keep momentum.',
650
+ state.autonomyMode === 'assist'
651
+ ? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
652
+ : 'Autonomous mode: execute safe next actions without waiting for confirmation; ask only when blocked by permissions, credentials, or policy.',
653
+ 'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
654
+ hasMemoryTool
655
+ ? 'Use memory_tool actively: recall relevant prior notes before acting, and store a concise note after each meaningful step.'
656
+ : 'memory_tool is unavailable in this session. Keep concise progress summaries in your status/meta output.',
657
+ 'If you are blocked by missing credentials, permissions, or policy limits, say exactly what is blocked and the smallest unblock needed.',
658
+ 'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
659
+ 'If no meaningful action remains right now, reply exactly HEARTBEAT_OK.',
660
+ 'Otherwise include a concise human update, then append exactly one [MAIN_LOOP_META] JSON line.',
661
+ 'Optionally append one [MAIN_LOOP_PLAN] JSON line when you create/revise a plan.',
662
+ 'Optionally append one [MAIN_LOOP_REVIEW] JSON line when you review recent execution results.',
663
+ '[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
664
+ '[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
665
+ '[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
666
+ ].join('\n')
667
+ }
668
+
669
+ export function isMainSession(session: any): boolean {
670
+ return session?.name === MAIN_SESSION_NAME
671
+ }
672
+
673
+ export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
674
+ const now = Date.now()
675
+ const state = normalizeState(session?.mainLoopState, now)
676
+ const goal = state.goal || inferGoalFromSessionMessages(session) || null
677
+ const hasMemoryTool = Array.isArray(session?.tools) && session.tools.includes('memory')
678
+
679
+ const promptGoal = goal || 'No explicit mission captured yet. Infer the mission from recent user instructions and continue proactively.'
680
+ const promptSummary = state.summary || 'No prior mission summary yet.'
681
+ const promptNextAction = state.nextAction || 'No queued action. Determine one.'
682
+ const contractLines = buildGoalContractLines(state)
683
+
684
+ return [
685
+ 'SWARM_MAIN_MISSION_TICK',
686
+ `Time: ${new Date(now).toISOString()}`,
687
+ `Mission goal: ${promptGoal}`,
688
+ `Current status: ${state.status}`,
689
+ `Mission paused: ${state.paused ? 'yes' : 'no'}`,
690
+ `Autonomy mode: ${state.autonomyMode}`,
691
+ `Mission task id: ${state.missionTaskId || 'none'}`,
692
+ `Momentum score: ${state.momentumScore}/100`,
693
+ ...contractLines,
694
+ state.planSteps.length ? `Current plan steps: ${state.planSteps.join(' -> ')}` : '',
695
+ state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
696
+ state.reviewNote ? `Last review: ${state.reviewNote}` : '',
697
+ `Last summary: ${toOneLine(promptSummary, 500)}`,
698
+ `Last next action: ${toOneLine(promptNextAction, 500)}`,
699
+ buildPendingEventLines(state),
700
+ buildTimelineLines(state),
701
+ 'You are running the main autonomous mission loop. Continue executing toward the goal with initiative.',
702
+ state.autonomyMode === 'assist'
703
+ ? 'Assist mode is active: execute safe internal work and ask before irreversible external side effects.'
704
+ : 'Autonomous mode is active: execute safe next actions without waiting for confirmation; only ask when blocked.',
705
+ 'Use tools where needed, verify outcomes, and avoid vague status-only replies.',
706
+ 'Do not ask broad exploratory questions when a safe next action exists. Pick a reasonable assumption, execute, and adapt from evidence.',
707
+ 'Do not ask clarifying questions unless blocked by missing credentials, permissions, or safety constraints.',
708
+ hasMemoryTool
709
+ ? 'Use memory_tool actively: recall relevant prior notes before acting, and store concise notes about progress, constraints, and next step after each meaningful action.'
710
+ : 'If memory_tool is unavailable, keep concise state in summary/next_action and continue execution.',
711
+ 'Use a planner-executor-review loop: keep a concrete step plan, execute one meaningful step, then self-review and either continue or re-plan.',
712
+ 'For screenshot/image delivery goals (including scheduled captures), do not report status "ok" until a real artifact exists (upload link or explicit sent-file confirmation).',
713
+ 'If nothing important changed and no action is needed now, reply exactly HEARTBEAT_OK.',
714
+ 'Otherwise: provide a concise human-readable update, then append exactly one [MAIN_LOOP_META] JSON line.',
715
+ 'Optionally append one [MAIN_LOOP_PLAN] JSON line when creating/updating plan steps.',
716
+ 'Optionally append one [MAIN_LOOP_REVIEW] JSON line after execution review.',
717
+ '[MAIN_LOOP_META] {"status":"progress|ok|blocked|idle","summary":"...","next_action":"...","follow_up":true|false,"delay_sec":45,"goal":"optional","consume_event_ids":["evt_..."]}',
718
+ '[MAIN_LOOP_PLAN] {"steps":["..."],"current_step":"..."}',
719
+ '[MAIN_LOOP_REVIEW] {"note":"...","confidence":0.0,"needs_replan":false}',
720
+ 'The [MAIN_LOOP_META] JSON must be valid, on one line, and only appear once.',
721
+ `Fallback prompt context: ${fallbackPrompt || 'SWARM_HEARTBEAT_CHECK'}`,
722
+ ].join('\n')
723
+ }
724
+
725
+ export function stripMainLoopMetaForPersistence(text: string, internal: boolean): string {
726
+ if (!internal) return text
727
+ if (!text) return ''
728
+ return text
729
+ .split('\n')
730
+ .filter((line) => !line.includes('[MAIN_LOOP_META]') && !line.includes('[MAIN_LOOP_PLAN]') && !line.includes('[MAIN_LOOP_REVIEW]'))
731
+ .join('\n')
732
+ .trim()
733
+ }
734
+
735
+ export function getMainLoopStateForSession(sessionId: string): MainLoopState | null {
736
+ const sessions = loadSessions()
737
+ const session = sessions[sessionId]
738
+ if (!session || !isMainSession(session)) return null
739
+ return normalizeState(session.mainLoopState)
740
+ }
741
+
742
+ export function setMainLoopStateForSession(sessionId: string, patch: Partial<MainLoopState>): MainLoopState | null {
743
+ const sessions = loadSessions()
744
+ const session = sessions[sessionId]
745
+ if (!session || !isMainSession(session)) return null
746
+ const now = Date.now()
747
+ const state = normalizeState(session.mainLoopState, now)
748
+
749
+ if (typeof patch.goal === 'string') state.goal = patch.goal.trim().slice(0, 600) || null
750
+ if (patch.goal === null) state.goal = null
751
+ if (patch.goalContract !== undefined) state.goalContract = normalizeGoalContract(patch.goalContract)
752
+ if (patch.status === 'idle' || patch.status === 'progress' || patch.status === 'blocked' || patch.status === 'ok') state.status = patch.status
753
+ if (typeof patch.summary === 'string') state.summary = patch.summary.trim().slice(0, 800) || null
754
+ if (patch.summary === null) state.summary = null
755
+ if (typeof patch.nextAction === 'string') state.nextAction = patch.nextAction.trim().slice(0, 600) || null
756
+ if (patch.nextAction === null) state.nextAction = null
757
+ if (Array.isArray(patch.planSteps)) state.planSteps = normalizeStringList(patch.planSteps, 10, 220)
758
+ if (typeof patch.currentPlanStep === 'string') state.currentPlanStep = patch.currentPlanStep.trim().slice(0, 220) || null
759
+ if (patch.currentPlanStep === null) state.currentPlanStep = null
760
+ if (typeof patch.reviewNote === 'string') state.reviewNote = patch.reviewNote.trim().slice(0, 320) || null
761
+ if (patch.reviewNote === null) state.reviewNote = null
762
+ if (typeof patch.reviewConfidence === 'number' && Number.isFinite(patch.reviewConfidence)) {
763
+ state.reviewConfidence = Math.max(0, Math.min(1, patch.reviewConfidence))
764
+ }
765
+ if (patch.reviewConfidence === null) state.reviewConfidence = null
766
+ if (typeof patch.missionTaskId === 'string') state.missionTaskId = patch.missionTaskId.trim() || null
767
+ if (patch.missionTaskId === null) state.missionTaskId = null
768
+ if (typeof patch.momentumScore === 'number') state.momentumScore = clampInt(patch.momentumScore, state.momentumScore, 0, 100)
769
+ if (typeof patch.paused === 'boolean') state.paused = patch.paused
770
+ if (patch.autonomyMode === 'assist' || patch.autonomyMode === 'autonomous') state.autonomyMode = patch.autonomyMode
771
+ if (Array.isArray(patch.pendingEvents)) state.pendingEvents = pruneEvents(patch.pendingEvents, now)
772
+ if (Array.isArray(patch.timeline)) state.timeline = pruneTimeline(patch.timeline, now)
773
+ if (typeof patch.followupChainCount === 'number') state.followupChainCount = clampInt(patch.followupChainCount, state.followupChainCount, 0, 100)
774
+ if (typeof patch.metaMissCount === 'number') state.metaMissCount = clampInt(patch.metaMissCount, state.metaMissCount, 0, 100)
775
+ if (Array.isArray(patch.workingMemoryNotes)) state.workingMemoryNotes = normalizeStringList(patch.workingMemoryNotes, 24, 260)
776
+ if (typeof patch.lastMemoryNoteAt === 'number') state.lastMemoryNoteAt = patch.lastMemoryNoteAt
777
+ if (patch.lastMemoryNoteAt === null) state.lastMemoryNoteAt = null
778
+ if (typeof patch.lastPlannedAt === 'number') state.lastPlannedAt = patch.lastPlannedAt
779
+ if (patch.lastPlannedAt === null) state.lastPlannedAt = null
780
+ if (typeof patch.lastReviewedAt === 'number') state.lastReviewedAt = patch.lastReviewedAt
781
+ if (patch.lastReviewedAt === null) state.lastReviewedAt = null
782
+
783
+ state.momentumScore = computeMomentumScore(state)
784
+ state.updatedAt = now
785
+ session.mainLoopState = state
786
+ sessions[sessionId] = session
787
+ saveSessions(sessions)
788
+ return state
789
+ }
790
+
791
+ export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput): number {
792
+ const text = toOneLine(input.text)
793
+ if (!text) return 0
794
+
795
+ const sessions = loadSessions()
796
+ const now = Date.now()
797
+ let changed = 0
798
+
799
+ for (const session of Object.values(sessions) as any[]) {
800
+ if (!isMainSession(session)) continue
801
+ if (input.user && session.user && session.user !== input.user) continue
802
+
803
+ const state = normalizeState(session.mainLoopState, now)
804
+ const appended = appendEvent(state, input.type || 'event', text, now)
805
+ if (!appended) continue
806
+ appendTimeline(state, input.type || 'event', text, now, state.status)
807
+ state.momentumScore = computeMomentumScore(state)
808
+ state.updatedAt = now
809
+ session.mainLoopState = state
810
+ changed += 1
811
+ }
812
+
813
+ if (changed > 0) {
814
+ saveSessions(sessions)
815
+ log.info('main-loop', `Queued event for ${changed} main session(s)`, {
816
+ type: input.type,
817
+ text,
818
+ user: input.user || null,
819
+ })
820
+ }
821
+
822
+ return changed
823
+ }
824
+
825
+ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
826
+ const sessions = loadSessions()
827
+ const session = sessions[input.sessionId]
828
+ if (!session || !isMainSession(session)) return null
829
+
830
+ const now = Date.now()
831
+ const state = normalizeState(session.mainLoopState, now)
832
+ const hasMemoryTool = Array.isArray(session.tools) && session.tools.includes('memory')
833
+ state.pendingEvents = pruneEvents(state.pendingEvents, now)
834
+ let forceMemoryNote = false
835
+
836
+ const userGoal = inferGoalFromUserMessage(input.message)
837
+ const userGoalContract = parseGoalContractFromText(input.message)
838
+ if (!input.internal) {
839
+ if (userGoal) {
840
+ state.goal = userGoal
841
+ if (userGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
842
+ state.status = 'progress'
843
+ appendEvent(state, 'user_instruction', `User goal updated: ${userGoal}`, now)
844
+ appendTimeline(state, 'user_goal', `Goal updated: ${userGoal}`, now, state.status)
845
+ appendWorkingMemoryNote(state, `goal:${userGoal}`)
846
+ forceMemoryNote = true
847
+ } else if (userGoalContract?.objective) {
848
+ state.goal = userGoalContract.objective
849
+ state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
850
+ state.status = 'progress'
851
+ appendTimeline(state, 'user_goal_contract', `Goal contract updated: ${userGoalContract.objective}`, now, state.status)
852
+ appendWorkingMemoryNote(state, `contract:${userGoalContract.objective}`)
853
+ forceMemoryNote = true
854
+ }
855
+ state.followupChainCount = 0
856
+ }
857
+
858
+ if (state.paused && input.internal) {
859
+ appendTimeline(state, 'paused_skip', `Skipped internal tick from ${input.source} because mission is paused.`, now, state.status)
860
+ state.momentumScore = computeMomentumScore(state)
861
+ state.updatedAt = now
862
+ session.mainLoopState = state
863
+ sessions[input.sessionId] = session
864
+ saveSessions(sessions)
865
+ return null
866
+ }
867
+
868
+ if (input.error) {
869
+ appendEvent(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 400)}`, now)
870
+ appendTimeline(state, 'run_error', `Run error (${input.source}): ${toOneLine(input.error, 220)}`, now, 'blocked')
871
+ state.status = 'blocked'
872
+ appendWorkingMemoryNote(state, `blocked:${toOneLine(input.error, 120)}`)
873
+ forceMemoryNote = true
874
+ }
875
+
876
+ for (const event of input.toolEvents || []) {
877
+ if (!event?.error) continue
878
+ appendEvent(
879
+ state,
880
+ 'tool_error',
881
+ `Tool ${event.name || 'unknown'} error: ${toOneLine(event.output || event.input || 'unknown error', 400)}`,
882
+ now,
883
+ )
884
+ appendTimeline(
885
+ state,
886
+ 'tool_error',
887
+ `Tool ${event.name || 'unknown'} error encountered.`,
888
+ now,
889
+ 'blocked',
890
+ )
891
+ forceMemoryNote = true
892
+ }
893
+
894
+ let followup: MainLoopFollowupRequest | null = null
895
+ const shouldAutoKickFromUserGoal = !input.internal
896
+ && !input.error
897
+ && (!!userGoal || !!userGoalContract?.objective)
898
+ && !state.paused
899
+ && state.autonomyMode === 'autonomous'
900
+
901
+ if (shouldAutoKickFromUserGoal) {
902
+ followup = {
903
+ message: buildFollowupPrompt(state, { hasMemoryTool }),
904
+ delayMs: 1500,
905
+ dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
906
+ }
907
+ appendTimeline(state, 'followup', 'Queued autonomous kickoff follow-up from new user goal.', now, state.status)
908
+ }
909
+
910
+ if (input.internal) {
911
+ state.lastTickAt = now
912
+ const trimmedText = (input.resultText || '').trim()
913
+ const isHeartbeatOk = /^HEARTBEAT_OK$/i.test(trimmedText)
914
+ const meta = parseMainLoopMeta(trimmedText)
915
+ const planMeta = parseMainLoopPlan(trimmedText)
916
+ const reviewMeta = parseMainLoopReview(trimmedText)
917
+
918
+ if (planMeta) {
919
+ if (planMeta.steps?.length) {
920
+ state.planSteps = planMeta.steps
921
+ state.lastPlannedAt = now
922
+ appendWorkingMemoryNote(state, `plan:${planMeta.steps.join(' -> ')}`)
923
+ }
924
+ if (planMeta.current_step) {
925
+ state.currentPlanStep = planMeta.current_step
926
+ state.lastPlannedAt = now
927
+ }
928
+ appendTimeline(state, 'plan', `Plan updated${planMeta.current_step ? ` at step: ${planMeta.current_step}` : ''}.`, now, state.status)
929
+ }
930
+
931
+ if (reviewMeta) {
932
+ if (reviewMeta.note) {
933
+ state.reviewNote = reviewMeta.note
934
+ appendWorkingMemoryNote(state, `review:${reviewMeta.note}`)
935
+ }
936
+ if (typeof reviewMeta.confidence === 'number') state.reviewConfidence = reviewMeta.confidence
937
+ state.lastReviewedAt = now
938
+ if (reviewMeta.needs_replan === true && state.planSteps.length > 0) {
939
+ appendEvent(state, 'review_replan', 'Execution review requested replanning.', now)
940
+ }
941
+ appendTimeline(state, 'review', reviewMeta.note || 'Execution review updated.', now, state.status)
942
+ }
943
+
944
+ if (meta) {
945
+ state.metaMissCount = 0
946
+ if (meta.goal) {
947
+ state.goal = meta.goal
948
+ const metaGoalContract = parseGoalContractFromText(meta.goal)
949
+ if (metaGoalContract) state.goalContract = mergeGoalContracts(state.goalContract, metaGoalContract)
950
+ }
951
+ if (meta.status) state.status = meta.status
952
+ if (meta.summary) state.summary = meta.summary
953
+ if (meta.next_action) state.nextAction = meta.next_action
954
+ if (meta.summary) appendWorkingMemoryNote(state, `summary:${toOneLine(meta.summary, 180)}`)
955
+ if (meta.next_action) appendWorkingMemoryNote(state, `next:${toOneLine(meta.next_action, 180)}`)
956
+ appendTimeline(
957
+ state,
958
+ 'meta',
959
+ `Meta update: status=${meta.status || state.status}; summary=${toOneLine(meta.summary || state.summary || 'none', 140)}`,
960
+ now,
961
+ meta.status || state.status,
962
+ )
963
+ consumeEvents(state, meta.consume_event_ids)
964
+
965
+ if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < MAX_FOLLOWUP_CHAIN) {
966
+ state.followupChainCount += 1
967
+ const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
968
+ followup = {
969
+ message: buildFollowupPrompt(state, { hasMemoryTool }),
970
+ delayMs: delaySec * 1000,
971
+ dedupeKey: `main-loop-followup:${input.sessionId}`,
972
+ }
973
+ appendTimeline(state, 'followup', `Queued chained follow-up in ${delaySec}s.`, now, state.status)
974
+ } else if (meta.follow_up === false || isHeartbeatOk) {
975
+ state.followupChainCount = 0
976
+ }
977
+ if (state.status === 'ok' || state.status === 'blocked') {
978
+ forceMemoryNote = true
979
+ }
980
+ } else if (!isHeartbeatOk && trimmedText) {
981
+ state.metaMissCount = Math.min(100, state.metaMissCount + 1)
982
+ state.summary = toOneLine(trimmedText, 700)
983
+ appendWorkingMemoryNote(state, `inferred:${toOneLine(trimmedText, 160)}`)
984
+ if (state.status === 'idle') state.status = 'progress'
985
+ appendEvent(state, 'meta_missing', 'Main-loop reply missing [MAIN_LOOP_META] contract; state inferred from text.', now)
986
+ appendTimeline(state, 'meta_missing', 'Missing [MAIN_LOOP_META]; inferred state from plain text.', now, state.status)
987
+ } else if (isHeartbeatOk) {
988
+ state.metaMissCount = 0
989
+ appendTimeline(state, 'heartbeat_ok', 'Heartbeat returned HEARTBEAT_OK.', now, state.status)
990
+ }
991
+ }
992
+
993
+ if (input.internal && state.status === 'ok') {
994
+ const completionGateReason = getMissionCompletionGateReason(session, state, input.resultText || '')
995
+ if (completionGateReason) {
996
+ state.status = 'progress'
997
+ if (!state.nextAction || /^no queued action/i.test(state.nextAction)) {
998
+ state.nextAction = 'Wait for the next schedule run and verify a screenshot artifact link is delivered.'
999
+ }
1000
+ appendEvent(state, 'completion_gate', completionGateReason, now)
1001
+ appendTimeline(state, 'completion_gate', 'Holding completion until screenshot artifact evidence is observed.', now, state.status)
1002
+ appendWorkingMemoryNote(state, `gate:${toOneLine(completionGateReason, 180)}`)
1003
+ }
1004
+ }
1005
+
1006
+ state.missionTaskId = upsertMissionTask(session, state, now)
1007
+ const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
1008
+ maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
1009
+ state.momentumScore = computeMomentumScore(state)
1010
+
1011
+ state.updatedAt = now
1012
+ session.mainLoopState = state
1013
+ sessions[input.sessionId] = session
1014
+ saveSessions(sessions)
1015
+
1016
+ return followup
1017
+ }