@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
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
1
3
  import { genId } from '@/lib/id'
2
4
  import type { EvalScenario, EvalRun, EvalSuiteResult } from './types'
3
5
  import { getScenario, EVAL_SCENARIOS } from './scenarios'
@@ -5,8 +7,15 @@ import { scoreCriteria } from './scorer'
5
7
  import { saveEvalRun } from './store'
6
8
  import { loadSessions, saveSessions, loadAgents, loadCredentials, decryptKey } from '../storage'
7
9
  import { executeSessionChatTurn } from '../chat-execution'
10
+ import { WORKSPACE_DIR } from '../data-dir'
8
11
  import type { Session } from '@/types'
9
12
 
13
+ export function resolveEvalSessionCwd(runId: string): string {
14
+ const dir = path.join(WORKSPACE_DIR, 'evals', runId)
15
+ fs.mkdirSync(dir, { recursive: true })
16
+ return dir
17
+ }
18
+
10
19
  export async function runEvalScenario(scenarioId: string, agentId: string): Promise<EvalRun> {
11
20
  const scenario = getScenario(scenarioId)
12
21
  if (!scenario) throw new Error(`Unknown eval scenario: ${scenarioId}`)
@@ -18,6 +27,7 @@ export async function runEvalScenario(scenarioId: string, agentId: string): Prom
18
27
  const runId = genId()
19
28
  const sessionId = `eval-${runId}`
20
29
  const now = Date.now()
30
+ const sessionCwd = resolveEvalSessionCwd(runId)
21
31
 
22
32
  const run: EvalRun = {
23
33
  id: runId,
@@ -36,7 +46,7 @@ export async function runEvalScenario(scenarioId: string, agentId: string): Prom
36
46
  const evalSession: Session = {
37
47
  id: sessionId,
38
48
  name: `Eval: ${scenario.name}`,
39
- cwd: process.cwd(),
49
+ cwd: sessionCwd,
40
50
  user: 'eval-runner',
41
51
  provider: (agent.provider as Session['provider']) ?? 'anthropic',
42
52
  model: (agent.model as string) ?? '',
@@ -1,8 +1,9 @@
1
1
  import Database from 'better-sqlite3'
2
2
  import path from 'path'
3
3
  import type { EvalRun } from './types'
4
+ import { DATA_DIR } from '../data-dir'
4
5
 
5
- const DB_PATH = path.join(process.cwd(), 'data', 'eval-runs.db')
6
+ const DB_PATH = path.join(DATA_DIR, 'eval-runs.db')
6
7
 
7
8
  let db: Database.Database | null = null
8
9
 
@@ -1,11 +1,17 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
+ import {
4
+ DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
5
+ DEFAULT_HEARTBEAT_INTERVAL_SEC,
6
+ DEFAULT_HEARTBEAT_SHOW_ALERTS,
7
+ DEFAULT_HEARTBEAT_SHOW_OK,
8
+ } from '@/lib/heartbeat-defaults'
3
9
  import { loadAgents, loadSessions, loadSettings } from './storage'
4
10
  import { enqueueSessionRun, getSessionRunState } from './session-run-manager'
5
11
  import { log } from './logger'
6
- import { buildMainLoopHeartbeatPrompt, getMainLoopStateForSession, isMainSession } from './main-agent-loop'
7
12
  import { WORKSPACE_DIR } from './data-dir'
8
13
  import { drainSystemEvents } from './system-events'
14
+ import { buildIdentityContinuityContext } from './identity-continuity'
9
15
 
10
16
  const HEARTBEAT_TICK_MS = 5_000
11
17
 
@@ -188,6 +194,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
188
194
  if (!agent) return fallbackPrompt
189
195
 
190
196
  const identityContext = buildIdentityContext(session, agent)
197
+ const continuityContext = buildIdentityContinuityContext(session, agent)
191
198
  // Drain system events accumulated since last heartbeat
192
199
  const events = drainSystemEvents(session.id)
193
200
  const eventBlock = events.length > 0
@@ -219,6 +226,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
219
226
  'AGENT_HEARTBEAT_TICK',
220
227
  `Time: ${new Date().toISOString()}`,
221
228
  identityContext,
229
+ continuityContext,
222
230
  description ? `Description: ${description}` : '',
223
231
  eventBlock ? `Events since last heartbeat:\n${eventBlock}` : '',
224
232
  dynamicGoal
@@ -242,14 +250,6 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
242
250
  ].filter(Boolean).join('\n')
243
251
  }
244
252
 
245
- function applyMomentumMultiplier(intervalSec: number, momentumScore: number): number {
246
- let multiplier = 1.0
247
- if (momentumScore >= 80) multiplier = 0.5
248
- else if (momentumScore < 40) multiplier = 2.0
249
- const adjusted = Math.round(intervalSec * multiplier)
250
- return Math.max(30, Math.min(7200, adjusted))
251
- }
252
-
253
253
  function resolveInterval(obj: Record<string, any>, currentSec: number): number {
254
254
  // Prefer heartbeatInterval (duration string) over heartbeatIntervalSec (raw number)
255
255
  if (obj.heartbeatInterval !== undefined && obj.heartbeatInterval !== null) {
@@ -281,7 +281,7 @@ function resolveNum(obj: Record<string, any>, key: string, current: number): num
281
281
 
282
282
  function heartbeatConfigForSession(session: any, settings: Record<string, any>, agents: Record<string, any>): HeartbeatConfig {
283
283
  // Global defaults — 30 min interval (was 120s)
284
- let intervalSec = resolveInterval(settings, 1800)
284
+ let intervalSec = resolveInterval(settings, DEFAULT_HEARTBEAT_INTERVAL_SEC)
285
285
  const globalPrompt = (typeof settings.heartbeatPrompt === 'string' && settings.heartbeatPrompt.trim())
286
286
  ? settings.heartbeatPrompt.trim()
287
287
  : DEFAULT_HEARTBEAT_PROMPT
@@ -289,9 +289,9 @@ function heartbeatConfigForSession(session: any, settings: Record<string, any>,
289
289
  let enabled = intervalSec > 0
290
290
  let prompt = globalPrompt
291
291
  let model: string | null = resolveStr(settings, 'heartbeatModel', null)
292
- let ackMaxChars = resolveNum(settings, 'heartbeatAckMaxChars', 300)
293
- let showOk = resolveBool(settings, 'heartbeatShowOk', false)
294
- let showAlerts = resolveBool(settings, 'heartbeatShowAlerts', true)
292
+ let ackMaxChars = resolveNum(settings, 'heartbeatAckMaxChars', DEFAULT_HEARTBEAT_ACK_MAX_CHARS)
293
+ let showOk = resolveBool(settings, 'heartbeatShowOk', DEFAULT_HEARTBEAT_SHOW_OK)
294
+ let showAlerts = resolveBool(settings, 'heartbeatShowAlerts', DEFAULT_HEARTBEAT_SHOW_ALERTS)
295
295
  let target: string | null = resolveStr(settings, 'heartbeatTarget', null)
296
296
 
297
297
  // Agent layer overrides
@@ -378,7 +378,7 @@ async function tickHeartbeats() {
378
378
  for (const session of Object.values(sessions) as any[]) {
379
379
  if (!session?.id) continue
380
380
  if (!Array.isArray(session.plugins) || session.plugins.length === 0) continue
381
- if (session.sessionType && session.sessionType !== 'human' && session.sessionType !== 'orchestrated') continue
381
+ if (session.sessionType && session.sessionType !== 'human') continue
382
382
 
383
383
  // Check if this session or its agent has explicit heartbeat opt-in
384
384
  const agent = session.agentId ? agents[session.agentId] : null
@@ -395,10 +395,6 @@ async function tickHeartbeats() {
395
395
  const cfg = heartbeatConfigForSession(session, settings, agents)
396
396
  if (!cfg.enabled) continue
397
397
 
398
- // Apply momentum-based multiplier to heartbeat interval
399
- const momentumScore = session.mainLoopState?.momentumScore ?? 40
400
- cfg.intervalSec = applyMomentumMultiplier(cfg.intervalSec, momentumScore)
401
-
402
398
  // For sessions with explicit opt-in, use a shorter idle threshold (just intervalSec * 2).
403
399
  // For inherited/global heartbeats, keep the 180s minimum to avoid noisy auto-fire.
404
400
  const defaultIdleSec = explicitOptIn
@@ -410,38 +406,22 @@ async function tickHeartbeats() {
410
406
  const idleMs = now - lastUserAt
411
407
  if (idleMs < userIdleThresholdSec * 1000) continue
412
408
 
413
- if (isMainSession(session)) {
414
- const loopState = getMainLoopStateForSession(session.id)
415
- if (loopState?.paused) continue
416
- // Only suppress idle main sessions when heartbeat is inherited (not explicitly enabled)
417
- if (!explicitOptIn) {
418
- const loopStatus = loopState?.status || 'idle'
419
- const pendingEvents = loopState?.pendingEvents?.length || 0
420
- if ((loopStatus === 'ok' || loopStatus === 'idle') && pendingEvents === 0) continue
421
- }
422
- }
423
-
424
409
  const last = state.lastBySession.get(session.id) || 0
425
410
  if (now - last < cfg.intervalSec * 1000) continue
426
411
 
427
412
  const runState = getSessionRunState(session.id)
428
413
  if (runState.runningRunId) continue
429
414
 
430
- let heartbeatMessage: string
431
- if (isMainSession(session)) {
432
- heartbeatMessage = buildMainLoopHeartbeatPrompt(session, cfg.prompt)
433
- } else {
434
- const rawHeartbeatFileContent = readHeartbeatFile(session)
435
- const heartbeatFileContent = isHeartbeatContentEffectivelyEmpty(rawHeartbeatFileContent) ? '' : rawHeartbeatFileContent
436
- const hasGoal = !!(agent?.heartbeatGoal || agent?.description || agent?.systemPrompt || agent?.soul)
437
- const hasCustomPrompt = cfg.prompt !== DEFAULT_HEARTBEAT_PROMPT
438
- // Skip heartbeat only if there's truly nothing to drive it:
439
- // no agent goal, no HEARTBEAT.md content, AND no custom prompt configured
440
- if (!hasGoal && !heartbeatFileContent && !hasCustomPrompt) {
441
- continue
442
- }
443
- heartbeatMessage = buildAgentHeartbeatPrompt(session, agent, cfg.prompt, heartbeatFileContent)
415
+ const rawHeartbeatFileContent = readHeartbeatFile(session)
416
+ const heartbeatFileContent = isHeartbeatContentEffectivelyEmpty(rawHeartbeatFileContent) ? '' : rawHeartbeatFileContent
417
+ const hasGoal = !!(agent?.heartbeatGoal || agent?.description || agent?.systemPrompt || agent?.soul)
418
+ const hasCustomPrompt = cfg.prompt !== DEFAULT_HEARTBEAT_PROMPT
419
+ // Skip heartbeat only if there's truly nothing to drive it:
420
+ // no agent goal, no HEARTBEAT.md content, AND no custom prompt configured
421
+ if (!hasGoal && !heartbeatFileContent && !hasCustomPrompt) {
422
+ continue
444
423
  }
424
+ const heartbeatMessage = buildAgentHeartbeatPrompt(session, agent, cfg.prompt, heartbeatFileContent)
445
425
 
446
426
  const enqueue = enqueueSessionRun({
447
427
  sessionId: session.id,
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
4
+
5
+ describe('heartbeat-source', () => {
6
+ it('treats scheduled heartbeat polls as heartbeat traffic', () => {
7
+ assert.equal(isHeartbeatSource('heartbeat'), true)
8
+ assert.equal(isInternalHeartbeatRun(true, 'heartbeat'), true)
9
+ })
10
+
11
+ it('treats wake-triggered heartbeat polls as heartbeat traffic', () => {
12
+ assert.equal(isHeartbeatSource('heartbeat-wake'), true)
13
+ assert.equal(isInternalHeartbeatRun(true, 'heartbeat-wake'), true)
14
+ })
15
+
16
+ it('does not classify other sources as heartbeat traffic', () => {
17
+ assert.equal(isHeartbeatSource('task'), false)
18
+ assert.equal(isHeartbeatSource('chat'), false)
19
+ assert.equal(isInternalHeartbeatRun(false, 'heartbeat'), false)
20
+ assert.equal(isInternalHeartbeatRun(true, 'task'), false)
21
+ })
22
+ })
@@ -0,0 +1,7 @@
1
+ export function isHeartbeatSource(source: string | null | undefined): boolean {
2
+ return source === 'heartbeat' || source === 'heartbeat-wake'
3
+ }
4
+
5
+ export function isInternalHeartbeatRun(internal: boolean | null | undefined, source: string | null | undefined): boolean {
6
+ return internal === true && isHeartbeatSource(source)
7
+ }
@@ -0,0 +1,77 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import type { Session } from '@/types'
4
+ import { buildIdentityContinuityContext, refreshSessionIdentityState } from './identity-continuity'
5
+
6
+ test('buildIdentityContinuityContext merges agent and session continuity', () => {
7
+ const block = buildIdentityContinuityContext(
8
+ {
9
+ name: 'Thread A',
10
+ conversationTone: 'technical',
11
+ identityState: {
12
+ personaLabel: 'Debugger',
13
+ relationshipSummary: 'Working with the user on a production issue.',
14
+ },
15
+ } as Partial<Session>,
16
+ {
17
+ name: 'Swarmy',
18
+ description: 'Helpful coding agent',
19
+ identityState: {
20
+ boundaries: ['Do not pretend work is complete without evidence.'],
21
+ continuityNotes: ['User prefers concise explanations.'],
22
+ },
23
+ },
24
+ )
25
+
26
+ assert.match(block, /Identity Continuity/)
27
+ assert.match(block, /Current persona: Debugger/)
28
+ assert.match(block, /Observed tone: technical/)
29
+ assert.match(block, /User prefers concise explanations/)
30
+ })
31
+
32
+ test('refreshSessionIdentityState derives fallback continuity fields', () => {
33
+ const session = {
34
+ id: 's1',
35
+ name: 'Checkout Bug',
36
+ cwd: process.cwd(),
37
+ user: 'Taylor',
38
+ provider: 'openai',
39
+ model: 'gpt-4.1',
40
+ claudeSessionId: null,
41
+ codexThreadId: null,
42
+ opencodeSessionId: null,
43
+ messages: [{ role: 'user', text: 'Help', time: 1 }],
44
+ createdAt: 1,
45
+ lastActiveAt: 1,
46
+ conversationTone: 'focused',
47
+ connectorContext: { threadId: 'thread-9', senderName: 'Taylor' },
48
+ } as Session
49
+
50
+ const state = refreshSessionIdentityState(session, {
51
+ name: 'Swarmy',
52
+ description: 'Helpful coding agent',
53
+ }, 100)
54
+
55
+ assert.equal(state.personaLabel, 'Swarmy thread thread-9')
56
+ assert.equal(state.relationshipSummary, 'Ongoing conversation with Taylor.')
57
+ assert.equal(state.toneStyle, 'focused')
58
+ assert.equal(state.updatedAt, 100)
59
+ })
60
+
61
+ test('buildIdentityContinuityContext prefers thread persona labels from connector context', () => {
62
+ const block = buildIdentityContinuityContext(
63
+ {
64
+ name: 'Connector Session',
65
+ connectorContext: {
66
+ threadId: 'thread-9',
67
+ threadPersonaLabel: 'Checkout Incident',
68
+ },
69
+ } as Partial<Session>,
70
+ {
71
+ name: 'Swarmy',
72
+ description: 'Helpful coding agent',
73
+ },
74
+ )
75
+
76
+ assert.match(block, /Current persona: Checkout Incident/)
77
+ })
@@ -0,0 +1,127 @@
1
+ import type { Agent, IdentityContinuityState, Session } from '@/types'
2
+
3
+ function normalizeText(value: unknown, maxChars: number): string | null {
4
+ if (typeof value !== 'string') return null
5
+ const normalized = value.replace(/\s+/g, ' ').trim()
6
+ return normalized ? normalized.slice(0, maxChars) : null
7
+ }
8
+
9
+ function normalizeList(value: unknown, maxItems: number, maxChars: number): string[] {
10
+ if (!Array.isArray(value)) return []
11
+ const seen = new Set<string>()
12
+ const out: string[] = []
13
+ for (const raw of value) {
14
+ const normalized = normalizeText(raw, maxChars)
15
+ if (!normalized) continue
16
+ const key = normalized.toLowerCase()
17
+ if (seen.has(key)) continue
18
+ seen.add(key)
19
+ out.push(normalized)
20
+ if (out.length >= maxItems) break
21
+ }
22
+ return out
23
+ }
24
+
25
+ export function normalizeIdentityContinuityState(raw: unknown): IdentityContinuityState | null {
26
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
27
+ const record = raw as Record<string, unknown>
28
+ const state: IdentityContinuityState = {
29
+ selfSummary: normalizeText(record.selfSummary, 320),
30
+ relationshipSummary: normalizeText(record.relationshipSummary, 320),
31
+ personaLabel: normalizeText(record.personaLabel, 120),
32
+ toneStyle: normalizeText(record.toneStyle, 120),
33
+ boundaries: normalizeList(record.boundaries, 6, 180),
34
+ continuityNotes: normalizeList(record.continuityNotes, 8, 220),
35
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
36
+ ? Math.trunc(record.updatedAt)
37
+ : null,
38
+ }
39
+ return state
40
+ }
41
+
42
+ function fallbackSelfSummary(agent?: Partial<Agent> | null): string | null {
43
+ const description = normalizeText(agent?.description, 220)
44
+ if (description) return `${agent?.name || 'Agent'}: ${description}`
45
+ const soul = normalizeText(agent?.soul, 220)
46
+ if (soul) return `${agent?.name || 'Agent'}: ${soul}`
47
+ const name = normalizeText(agent?.name, 80)
48
+ return name ? `${name}: persistent companion agent` : null
49
+ }
50
+
51
+ function fallbackPersonaLabel(session?: Partial<Session> | null, agent?: Partial<Agent> | null): string | null {
52
+ const threadPersona = normalizeText(session?.connectorContext?.threadPersonaLabel, 120)
53
+ if (threadPersona) return threadPersona
54
+ const threadTitle = normalizeText(session?.connectorContext?.threadTitle, 120)
55
+ if (threadTitle) return threadTitle
56
+ const threadId = normalizeText(session?.connectorContext?.threadId, 80)
57
+ if (threadId) return `${agent?.name || 'Agent'} thread ${threadId}`
58
+ const sessionName = normalizeText(session?.name, 120)
59
+ if (sessionName && !/^new chat$/i.test(sessionName)) return sessionName
60
+ return null
61
+ }
62
+
63
+ function fallbackRelationshipSummary(session?: Partial<Session> | null): string | null {
64
+ const sender = normalizeText(session?.connectorContext?.senderName, 80)
65
+ if (sender) return `Ongoing conversation with ${sender}.`
66
+ const user = normalizeText(session?.user, 80)
67
+ if (user && user !== 'user') return `Ongoing conversation with ${user}.`
68
+ return 'Ongoing conversation with the user.'
69
+ }
70
+
71
+ export function buildIdentityContinuityContext(
72
+ session?: Partial<Session> | null,
73
+ agent?: Partial<Agent> | null,
74
+ ): string {
75
+ const agentState = normalizeIdentityContinuityState(agent?.identityState)
76
+ const sessionState = normalizeIdentityContinuityState(session?.identityState)
77
+ const selfSummary = sessionState?.selfSummary || agentState?.selfSummary || fallbackSelfSummary(agent)
78
+ const relationshipSummary = sessionState?.relationshipSummary || agentState?.relationshipSummary || fallbackRelationshipSummary(session)
79
+ const personaLabel = sessionState?.personaLabel || fallbackPersonaLabel(session, agent)
80
+ const toneStyle = sessionState?.toneStyle || normalizeText(session?.conversationTone, 80) || agentState?.toneStyle
81
+ const boundaries = sessionState?.boundaries?.length
82
+ ? sessionState.boundaries
83
+ : agentState?.boundaries?.length
84
+ ? agentState.boundaries
85
+ : []
86
+ const continuityNotes = [
87
+ ...(agentState?.continuityNotes || []),
88
+ ...(sessionState?.continuityNotes || []),
89
+ ].slice(-6)
90
+
91
+ const lines: string[] = []
92
+ if (selfSummary) lines.push(`Core self: ${selfSummary}`)
93
+ if (personaLabel) lines.push(`Current persona: ${personaLabel}`)
94
+ if (relationshipSummary) lines.push(`Relationship context: ${relationshipSummary}`)
95
+ if (toneStyle) lines.push(`Observed tone: ${toneStyle}`)
96
+ if (boundaries.length) lines.push(`Boundaries: ${boundaries.join(' | ')}`)
97
+ if (continuityNotes.length) lines.push(`Continuity notes: ${continuityNotes.join(' | ')}`)
98
+ if (!lines.length) return ''
99
+ return `## Identity Continuity\n${lines.join('\n')}`
100
+ }
101
+
102
+ export function refreshSessionIdentityState(
103
+ session: Session,
104
+ agent?: Partial<Agent> | null,
105
+ now = Date.now(),
106
+ ): IdentityContinuityState {
107
+ const existing = normalizeIdentityContinuityState(session.identityState) || {}
108
+ const agentState = normalizeIdentityContinuityState(agent?.identityState) || {}
109
+ const boundaries = existing.boundaries?.length ? existing.boundaries : (agentState.boundaries || [])
110
+ const continuityNotes = [
111
+ ...(agentState.continuityNotes || []),
112
+ ...(existing.continuityNotes || []),
113
+ ].slice(-8)
114
+
115
+ const next: IdentityContinuityState = {
116
+ selfSummary: existing.selfSummary || agentState.selfSummary || fallbackSelfSummary(agent),
117
+ relationshipSummary: existing.relationshipSummary || agentState.relationshipSummary || fallbackRelationshipSummary(session),
118
+ personaLabel: existing.personaLabel || fallbackPersonaLabel(session, agent),
119
+ toneStyle: normalizeText(session.conversationTone, 80) || existing.toneStyle || agentState.toneStyle,
120
+ boundaries,
121
+ continuityNotes,
122
+ updatedAt: now,
123
+ }
124
+
125
+ session.identityState = next
126
+ return next
127
+ }