@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -0,0 +1,119 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-runtime-settings-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: tempDir,
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ describe('runtime settings defaults', () => {
36
+ it('backfills explicit runtime defaults for clean installs', () => {
37
+ const output = runWithTempDataDir(`
38
+ const storageMod = await import('./src/lib/server/storage.ts')
39
+ const runtimeMod = await import('./src/lib/server/runtime-settings.ts')
40
+ const storage = storageMod.default || storageMod
41
+ const runtime = runtimeMod.default || runtimeMod
42
+ console.log(JSON.stringify({
43
+ settings: storage.loadSettings(),
44
+ runtime: runtime.loadRuntimeSettings(),
45
+ }))
46
+ `)
47
+
48
+ assert.equal(output.settings.loopMode, 'bounded')
49
+ assert.equal(output.settings.agentLoopRecursionLimit, 60)
50
+ assert.equal(output.settings.orchestratorLoopRecursionLimit, 80)
51
+ assert.equal(output.settings.legacyOrchestratorMaxTurns, 16)
52
+ assert.equal(output.settings.ongoingLoopMaxIterations, 250)
53
+ assert.equal(output.settings.ongoingLoopMaxRuntimeMinutes, 60)
54
+ assert.equal(output.settings.delegationMaxDepth, 3)
55
+ assert.equal(output.settings.shellCommandTimeoutSec, 30)
56
+ assert.equal(output.settings.claudeCodeTimeoutSec, 1800)
57
+ assert.equal(output.settings.cliProcessTimeoutSec, 1800)
58
+ assert.equal(output.settings.heartbeatIntervalSec, 1800)
59
+ assert.equal(output.settings.heartbeatAckMaxChars, 300)
60
+ assert.equal(output.settings.heartbeatShowOk, false)
61
+ assert.equal(output.settings.heartbeatShowAlerts, true)
62
+ assert.equal(output.settings.heartbeatTarget, null)
63
+ assert.equal(output.settings.heartbeatPrompt, null)
64
+ assert.equal(output.runtime.agentLoopRecursionLimit, 60)
65
+ assert.equal(output.runtime.orchestratorLoopRecursionLimit, 80)
66
+ assert.equal(output.runtime.legacyOrchestratorMaxTurns, 16)
67
+ })
68
+
69
+ it('clamps invalid persisted runtime settings into the supported range', () => {
70
+ const output = runWithTempDataDir(`
71
+ const storageMod = await import('./src/lib/server/storage.ts')
72
+ const runtimeMod = await import('./src/lib/server/runtime-settings.ts')
73
+ const storage = storageMod.default || storageMod
74
+ const runtime = runtimeMod.default || runtimeMod
75
+
76
+ storage.saveSettings({
77
+ loopMode: 'invalid',
78
+ agentLoopRecursionLimit: 999,
79
+ orchestratorLoopRecursionLimit: -5,
80
+ legacyOrchestratorMaxTurns: 0,
81
+ ongoingLoopMaxIterations: 999999,
82
+ ongoingLoopMaxRuntimeMinutes: -1,
83
+ delegationMaxDepth: 99,
84
+ shellCommandTimeoutSec: 0,
85
+ claudeCodeTimeoutSec: 999999,
86
+ cliProcessTimeoutSec: 'abc',
87
+ heartbeatIntervalSec: 999999,
88
+ heartbeatAckMaxChars: -50,
89
+ heartbeatShowOk: 'yes',
90
+ heartbeatShowAlerts: 'off',
91
+ heartbeatTarget: ' ',
92
+ heartbeatPrompt: ' ',
93
+ })
94
+
95
+ console.log(JSON.stringify({
96
+ settings: storage.loadSettings(),
97
+ runtime: runtime.loadRuntimeSettings(),
98
+ }))
99
+ `)
100
+
101
+ assert.equal(output.settings.loopMode, 'bounded')
102
+ assert.equal(output.settings.agentLoopRecursionLimit, 200)
103
+ assert.equal(output.settings.orchestratorLoopRecursionLimit, 1)
104
+ assert.equal(output.settings.legacyOrchestratorMaxTurns, 1)
105
+ assert.equal(output.settings.ongoingLoopMaxIterations, 5000)
106
+ assert.equal(output.settings.ongoingLoopMaxRuntimeMinutes, 0)
107
+ assert.equal(output.settings.delegationMaxDepth, 12)
108
+ assert.equal(output.settings.shellCommandTimeoutSec, 1)
109
+ assert.equal(output.settings.claudeCodeTimeoutSec, 7200)
110
+ assert.equal(output.settings.cliProcessTimeoutSec, 1800)
111
+ assert.equal(output.settings.heartbeatIntervalSec, 86400)
112
+ assert.equal(output.settings.heartbeatAckMaxChars, 0)
113
+ assert.equal(output.settings.heartbeatShowOk, true)
114
+ assert.equal(output.settings.heartbeatShowAlerts, false)
115
+ assert.equal(output.settings.heartbeatTarget, null)
116
+ assert.equal(output.settings.heartbeatPrompt, null)
117
+ assert.equal(output.runtime.ongoingLoopMaxRuntimeMs, null)
118
+ })
119
+ })
@@ -1,15 +1,6 @@
1
1
  import type { LoopMode } from '@/types'
2
2
  import {
3
- DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
4
- DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
5
- DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
6
- DEFAULT_DELEGATION_MAX_DEPTH,
7
- DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
8
- DEFAULT_LOOP_MODE,
9
- DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
10
- DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
11
- DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
12
- DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
3
+ normalizeRuntimeSettingFields,
13
4
  } from '@/lib/runtime-loop'
14
5
  import { loadSettings } from './storage'
15
6
 
@@ -26,92 +17,21 @@ export interface RuntimeSettings {
26
17
  cliProcessTimeoutMs: number
27
18
  }
28
19
 
29
- function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
30
- const parsed = typeof value === 'number'
31
- ? value
32
- : typeof value === 'string'
33
- ? Number.parseInt(value, 10)
34
- : Number.NaN
35
- if (!Number.isFinite(parsed)) return fallback
36
- const int = Math.trunc(parsed)
37
- return Math.max(min, Math.min(max, int))
38
- }
39
-
40
- function parseLoopMode(value: unknown): LoopMode {
41
- return value === 'ongoing' ? 'ongoing' : DEFAULT_LOOP_MODE
42
- }
43
-
44
20
  export function loadRuntimeSettings(): RuntimeSettings {
45
21
  const settings = loadSettings()
46
- const loopMode = parseLoopMode(settings.loopMode)
47
-
48
- const agentLoopRecursionLimit = parseIntSetting(
49
- settings.agentLoopRecursionLimit,
50
- DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
51
- 1,
52
- 200,
53
- )
54
- const orchestratorLoopRecursionLimit = parseIntSetting(
55
- settings.orchestratorLoopRecursionLimit,
56
- DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
57
- 1,
58
- 300,
59
- )
60
- const legacyOrchestratorMaxTurns = parseIntSetting(
61
- settings.legacyOrchestratorMaxTurns,
62
- DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
63
- 1,
64
- 300,
65
- )
66
- const delegationMaxDepth = parseIntSetting(
67
- settings.delegationMaxDepth,
68
- DEFAULT_DELEGATION_MAX_DEPTH,
69
- 1,
70
- 12,
71
- )
72
- const ongoingLoopMaxIterations = parseIntSetting(
73
- settings.ongoingLoopMaxIterations,
74
- DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
75
- 10,
76
- 5000,
77
- )
78
- const ongoingLoopMaxRuntimeMinutes = parseIntSetting(
79
- settings.ongoingLoopMaxRuntimeMinutes,
80
- DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
81
- 0,
82
- 1440,
83
- )
84
-
85
- const shellCommandTimeoutSec = parseIntSetting(
86
- settings.shellCommandTimeoutSec,
87
- DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
88
- 1,
89
- 600,
90
- )
91
- const claudeCodeTimeoutSec = parseIntSetting(
92
- settings.claudeCodeTimeoutSec,
93
- DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
94
- 5,
95
- 7200,
96
- )
97
- const cliProcessTimeoutSec = parseIntSetting(
98
- settings.cliProcessTimeoutSec,
99
- DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
100
- 10,
101
- 7200,
102
- )
22
+ const normalized = normalizeRuntimeSettingFields(settings)
103
23
 
104
24
  return {
105
- loopMode,
106
- agentLoopRecursionLimit,
107
- orchestratorLoopRecursionLimit,
108
- legacyOrchestratorMaxTurns,
109
- delegationMaxDepth,
110
- ongoingLoopMaxIterations,
111
- ongoingLoopMaxRuntimeMs: ongoingLoopMaxRuntimeMinutes > 0 ? ongoingLoopMaxRuntimeMinutes * 60_000 : null,
112
- shellCommandTimeoutMs: shellCommandTimeoutSec * 1000,
113
- claudeCodeTimeoutMs: claudeCodeTimeoutSec * 1000,
114
- cliProcessTimeoutMs: cliProcessTimeoutSec * 1000,
25
+ loopMode: normalized.loopMode as LoopMode,
26
+ agentLoopRecursionLimit: normalized.agentLoopRecursionLimit,
27
+ orchestratorLoopRecursionLimit: normalized.orchestratorLoopRecursionLimit,
28
+ legacyOrchestratorMaxTurns: normalized.legacyOrchestratorMaxTurns,
29
+ delegationMaxDepth: normalized.delegationMaxDepth,
30
+ ongoingLoopMaxIterations: normalized.ongoingLoopMaxIterations,
31
+ ongoingLoopMaxRuntimeMs: normalized.ongoingLoopMaxRuntimeMinutes > 0 ? normalized.ongoingLoopMaxRuntimeMinutes * 60_000 : null,
32
+ shellCommandTimeoutMs: normalized.shellCommandTimeoutSec * 1000,
33
+ claudeCodeTimeoutMs: normalized.claudeCodeTimeoutSec * 1000,
34
+ cliProcessTimeoutMs: normalized.cliProcessTimeoutSec * 1000,
115
35
  }
116
36
  }
117
37
 
@@ -0,0 +1,187 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { WORKSPACE_DIR } from './data-dir'
4
+
5
+ type SchedulePayload = Record<string, unknown>
6
+
7
+ export interface NormalizeScheduleOptions {
8
+ cwd?: string | null
9
+ now?: number
10
+ }
11
+
12
+ export type NormalizeScheduleResult =
13
+ | { ok: true; value: SchedulePayload }
14
+ | { ok: false; error: string }
15
+
16
+ const SCRIPT_FILE_EXT = /\.(py|js|mjs|cjs|ts|tsx|sh|bash|zsh|rb|php|pl)$/i
17
+ const DIRECT_SCRIPT_RUNNERS = new Set(['python', 'python3', 'python3.11', 'node', 'bash', 'sh', 'zsh', 'ruby', 'tsx', 'ts-node'])
18
+ const VALID_STATUSES = new Set(['active', 'paused', 'completed', 'failed'])
19
+
20
+ function trimString(value: unknown): string {
21
+ return typeof value === 'string' ? value.trim() : ''
22
+ }
23
+
24
+ function normalizeScheduleType(value: unknown): 'cron' | 'interval' | 'once' {
25
+ if (value === 'cron' || value === 'interval' || value === 'once') return value
26
+ return 'interval'
27
+ }
28
+
29
+ function normalizePositiveInt(value: unknown): number | null {
30
+ const parsed = typeof value === 'number'
31
+ ? value
32
+ : typeof value === 'string'
33
+ ? Number.parseInt(value, 10)
34
+ : Number.NaN
35
+ if (!Number.isFinite(parsed)) return null
36
+ const intValue = Math.trunc(parsed)
37
+ return intValue > 0 ? intValue : null
38
+ }
39
+
40
+ function isWithinDirectory(parent: string, child: string): boolean {
41
+ const relative = path.relative(path.resolve(parent), path.resolve(child))
42
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
43
+ }
44
+
45
+ function resolveRelativePath(baseDir: string, candidate: string): string | null {
46
+ const trimmed = trimString(candidate)
47
+ if (!trimmed) return null
48
+ if (path.isAbsolute(trimmed)) {
49
+ const resolvedAbsolute = path.resolve(trimmed)
50
+ return isWithinDirectory(baseDir, resolvedAbsolute) ? resolvedAbsolute : null
51
+ }
52
+ const resolved = path.resolve(baseDir, trimmed)
53
+ return isWithinDirectory(baseDir, resolved) ? resolved : null
54
+ }
55
+
56
+ function tokenizeCommand(command: string): string[] {
57
+ return String(command || '').match(/(?:[^\s"'`]+|"[^"]*"|'[^']*')+/g) || []
58
+ }
59
+
60
+ function unquoteToken(token: string): string {
61
+ if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
62
+ return token.slice(1, -1)
63
+ }
64
+ return token
65
+ }
66
+
67
+ function looksLikeScriptPath(token: string): boolean {
68
+ return SCRIPT_FILE_EXT.test(token) || token.includes('/') || token.includes(path.sep)
69
+ }
70
+
71
+ function extractScriptPathFromCommand(command: string): string | null {
72
+ const tokens = tokenizeCommand(command).map(unquoteToken).filter(Boolean)
73
+ if (!tokens.length) return null
74
+
75
+ const commandName = path.basename(tokens[0] || '').toLowerCase()
76
+ let startIndex = 1
77
+ if (commandName === 'npx' && tokens[1]) {
78
+ const nestedRunner = path.basename(tokens[1]).toLowerCase()
79
+ if (nestedRunner === 'tsx' || nestedRunner === 'ts-node') startIndex = 2
80
+ } else if (commandName === 'deno' && tokens[1] === 'run') {
81
+ startIndex = 2
82
+ } else if (!DIRECT_SCRIPT_RUNNERS.has(commandName)) {
83
+ startIndex = 0
84
+ }
85
+
86
+ for (let index = startIndex; index < tokens.length; index += 1) {
87
+ const candidate = tokens[index]
88
+ if (!candidate || candidate.startsWith('-')) continue
89
+ if (!looksLikeScriptPath(candidate)) continue
90
+ return candidate
91
+ }
92
+
93
+ return null
94
+ }
95
+
96
+ function deriveTaskPrompt(payload: SchedulePayload): string {
97
+ const explicitTaskPrompt = trimString(payload.taskPrompt)
98
+ if (explicitTaskPrompt) return explicitTaskPrompt
99
+
100
+ const command = trimString(payload.command)
101
+ if (command) {
102
+ return `Execute the command \`${command}\` from this schedule's working directory and report the result, including any errors.`
103
+ }
104
+
105
+ const filePath = trimString(payload.path)
106
+ if (!filePath) return ''
107
+
108
+ const action = trimString(payload.action).toLowerCase()
109
+ if (action === 'run_script') {
110
+ return `Run the script at \`${filePath}\` from this schedule's working directory and report the result, including any errors.`
111
+ }
112
+
113
+ return `Use the file at \`${filePath}\` to complete this scheduled task and report the result.`
114
+ }
115
+
116
+ function validateScheduleArtifacts(payload: SchedulePayload, baseDir: string): string | null {
117
+ const action = trimString(payload.action).toLowerCase()
118
+ const filePath = trimString(payload.path)
119
+ const command = trimString(payload.command)
120
+
121
+ if (action === 'run_script' && !filePath) {
122
+ return 'run_script schedules require a path.'
123
+ }
124
+
125
+ if (filePath) {
126
+ const resolved = resolveRelativePath(baseDir, filePath)
127
+ if (!resolved) return `schedule path must stay inside ${baseDir}: ${filePath}`
128
+ if (!fs.existsSync(resolved)) return `schedule path not found: ${filePath}`
129
+ }
130
+
131
+ if (!command) return null
132
+ const commandScriptPath = extractScriptPathFromCommand(command)
133
+ if (!commandScriptPath) return null
134
+ const resolved = resolveRelativePath(baseDir, commandScriptPath)
135
+ if (!resolved) return `schedule command references a path outside ${baseDir}: ${commandScriptPath}`
136
+ if (!fs.existsSync(resolved)) return `schedule command references a missing file: ${commandScriptPath}`
137
+ return null
138
+ }
139
+
140
+ export function normalizeSchedulePayload(payload: SchedulePayload, opts: NormalizeScheduleOptions = {}): NormalizeScheduleResult {
141
+ const now = typeof opts.now === 'number' ? opts.now : Date.now()
142
+ const baseDir = path.resolve(trimString(opts.cwd) || WORKSPACE_DIR)
143
+ const normalized: SchedulePayload = {
144
+ ...payload,
145
+ scheduleType: normalizeScheduleType(payload.scheduleType),
146
+ }
147
+ const action = trimString(normalized.action)
148
+ const command = trimString(normalized.command)
149
+ const filePath = trimString(normalized.path)
150
+ if (action) normalized.action = action
151
+ if (command) normalized.command = command
152
+ if (filePath) normalized.path = filePath
153
+
154
+ const status = trimString(normalized.status).toLowerCase()
155
+ normalized.status = VALID_STATUSES.has(status) ? status : 'active'
156
+
157
+ const agentId = trimString(normalized.agentId)
158
+ if (!agentId) {
159
+ return { ok: false, error: 'Error: schedules require a target agentId.' }
160
+ }
161
+ normalized.agentId = agentId
162
+
163
+ const taskPrompt = deriveTaskPrompt(normalized)
164
+ if (!taskPrompt) {
165
+ return { ok: false, error: 'Error: schedules require a taskPrompt, command, or action/path payload.' }
166
+ }
167
+ normalized.taskPrompt = taskPrompt
168
+
169
+ const validationError = validateScheduleArtifacts(normalized, baseDir)
170
+ if (validationError) return { ok: false, error: `Error: ${validationError}` }
171
+
172
+ if (normalized.nextRunAt == null) {
173
+ if (normalized.scheduleType === 'once') {
174
+ const runAt = normalizePositiveInt(normalized.runAt)
175
+ if (runAt != null) normalized.nextRunAt = runAt
176
+ } else if (normalized.scheduleType === 'interval') {
177
+ const intervalMs = normalizePositiveInt(normalized.intervalMs)
178
+ if (intervalMs != null) normalized.nextRunAt = now + intervalMs
179
+ }
180
+ }
181
+
182
+ return { ok: true, value: normalized }
183
+ }
184
+
185
+ export function extractScheduleCommandScriptPath(command: string): string | null {
186
+ return extractScriptPathFromCommand(command)
187
+ }
@@ -6,6 +6,7 @@ import { pushMainLoopEventToMainSessions } from './main-agent-loop'
6
6
  import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
7
7
  import { enqueueSystemEvent } from './system-events'
8
8
  import { requestHeartbeatNow } from './heartbeat-wake'
9
+ import { processDueWatchJobs } from './watch-jobs'
9
10
 
10
11
  const TICK_INTERVAL = 60_000 // 60 seconds
11
12
  let intervalId: ReturnType<typeof setInterval> | null = null
@@ -73,6 +74,7 @@ function computeNextRuns() {
73
74
 
74
75
  async function tick() {
75
76
  const now = Date.now()
77
+ await processDueWatchJobs(now)
76
78
  const schedules = loadSchedules()
77
79
  const agents = loadAgents()
78
80
  const tasks = loadTasks()
@@ -0,0 +1,85 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import type { Session } from '@/types'
4
+ import { buildSessionArchiveMarkdown, buildSessionArchivePayload } from './session-archive-memory'
5
+
6
+ test('buildSessionArchivePayload summarizes session transcript and metadata', () => {
7
+ const session = {
8
+ id: 'session-1',
9
+ name: 'Support Thread',
10
+ cwd: process.cwd(),
11
+ user: 'Alice',
12
+ provider: 'openai',
13
+ model: 'gpt-4.1',
14
+ claudeSessionId: null,
15
+ codexThreadId: null,
16
+ opencodeSessionId: null,
17
+ createdAt: Date.parse('2026-03-05T00:00:00.000Z'),
18
+ lastActiveAt: Date.parse('2026-03-05T10:00:00.000Z'),
19
+ sessionType: 'human',
20
+ messages: [
21
+ { role: 'user', text: 'Can you help me debug this issue?', time: 1 },
22
+ { role: 'assistant', text: 'Yes, show me the stack trace.', time: 2, toolEvents: [{ name: 'files', input: '{}' }] },
23
+ ],
24
+ identityState: { personaLabel: 'Debugger' },
25
+ } as Session
26
+
27
+ const payload = buildSessionArchivePayload(session, { name: 'Swarmy' })
28
+
29
+ assert.ok(payload)
30
+ assert.equal(payload?.title, 'Session archive: Support Thread')
31
+ assert.match(payload?.content || '', /Transcript excerpt:/)
32
+ assert.match(payload?.content || '', /Swarmy/)
33
+ assert.equal(payload?.metadata.tier, 'archive')
34
+ assert.equal(payload?.references[0]?.type, 'session')
35
+ })
36
+
37
+ test('buildSessionArchiveMarkdown creates a portable markdown snapshot', () => {
38
+ const session = {
39
+ id: 'session-3',
40
+ name: 'Architecture Review',
41
+ cwd: process.cwd(),
42
+ user: 'Alice',
43
+ provider: 'openai',
44
+ model: 'gpt-4.1',
45
+ claudeSessionId: null,
46
+ codexThreadId: null,
47
+ opencodeSessionId: null,
48
+ createdAt: Date.parse('2026-03-05T00:00:00.000Z'),
49
+ lastActiveAt: Date.parse('2026-03-05T10:00:00.000Z'),
50
+ sessionType: 'human',
51
+ messages: [
52
+ { role: 'user', text: 'Summarize the new connector policy.', time: 1 },
53
+ { role: 'assistant', text: 'It now uses scoped sessions and freshness resets.', time: 2 },
54
+ ],
55
+ identityState: { personaLabel: 'Reviewer' },
56
+ } as Session
57
+
58
+ const payload = buildSessionArchivePayload(session, { name: 'Swarmy' })
59
+ assert.ok(payload)
60
+
61
+ const markdown = buildSessionArchiveMarkdown(session, payload!, { name: 'Swarmy' })
62
+ assert.match(markdown, /^# Session archive: Architecture Review/m)
63
+ assert.match(markdown, /## Archive Snapshot/)
64
+ assert.match(markdown, /## Transcript Excerpt/)
65
+ assert.match(markdown, /\*\*Swarmy\*\*/)
66
+ })
67
+
68
+ test('buildSessionArchivePayload skips trivial sessions', () => {
69
+ const session = {
70
+ id: 'session-2',
71
+ name: 'Too Short',
72
+ cwd: process.cwd(),
73
+ user: 'Bob',
74
+ provider: 'openai',
75
+ model: 'gpt-4.1',
76
+ claudeSessionId: null,
77
+ codexThreadId: null,
78
+ opencodeSessionId: null,
79
+ createdAt: 1,
80
+ lastActiveAt: 1,
81
+ messages: [{ role: 'user', text: 'hi', time: 1 }],
82
+ } as Session
83
+
84
+ assert.equal(buildSessionArchivePayload(session), null)
85
+ })